IoC Prensibi Nedir? Örnek Bir Proje ile Kullanımı ve Avantajları
IoC(Inversion Of Control), uygulamanın yaşam döngüsü boyunca birbirine bağımlılığı az (loose coupling) olan nesneler oluşturmayı amaçlayan bir yazılım geliştirme prensibidir. Nesnelerin yaşam döngüsünden sorumludur, yönetimini sağlar. IoC kullanan sınıfa bir interface inject edildiğinde, ilgili interface metotları kullanılabilir olur. Böylece IoC kullanan sınıf sadece kullanacağı metotları bilir, sınıf içerisinde daha fazla metot olsa bile interface’de belirtilen metotlara erişebilecektir.
Sınıf içerisinde yapılacak herhangi bir değişiklikte IoC kullanan sınıf etkilenmeyeceği için yeniden yazılabilir ve test edilebilir yazılım geliştirmemizi sağlar. IoC nesne bağlamalar genellikle uygulama başlangıcında yapılandırılmaktadır. Bu anlamda tek bir yerden yapılan IoC yapılandırmalarının değiştirilmesi ve yönetimi de oldukça kolaydır.
IoC kullanmanın avantajlarını şöyle sıralayabiliriz:
- Loosely coupled (bağımlılığı az) sınıflar oluşturma
- Kolay unit test yazma
- Yönetilebilirlik
- Modüler programlar
- Farklı implementasyonlar arası kolay geçiş
Basit bir örnekle IoC prensibini bir projede nasıl uygulayabileceğimize bakalım. Örneği C# dilinde yapacağım ancak buradaki yaklaşım ve uygulanan kalıplar genel kullanımlar olduğu için çok benzer mantıkla farklı dillerde de IoC prensibi uygulanabilir.
.Net Core Web API projesi oluşturup birlikte adım adım ilerleyelim ve yukardaki diagramı takip edelim. API hizmetini 2 farklı database üzerinden verme opsiyonumuz olduğunu düşünelim.
1- Sıkı Bağlı Sınıflar (Tightly Coupled Class)
Veritabanı bağlantılarını yöneteceğimiz MsSQLConnection ve OracleConnection isimli 2 farklı sınıfa ihtiyacımız olacak.
public class OracleConnection { public string GetData() { return "Oracle"; } }
public class MsSQLConnection { public string GetData() { return "MsSQL"; } }
Her iki sınıf da basit bir şekilde verinin hangi veritabanından geldiğini döndüren bir metoda sahipler.
ConnectionHelper sınıfı DAL katmanından veriyi okuyup controller’a vermekle görevli business logic işlevi gören sınıfımız olsun. (Burada farklı iş kuralları da yazılabilir)
public class ConnectionHelper { private OracleConnection _oConnection; public ConnectionHelper() { _oConnection = new OracleConnection(); } public string GetData() { return _oConnection.GetData(); } }
Yukarda görüldüğü gibi iki sınıf arasında sıkı bir bağ bulunuyor. OracleConnection sınıfına referans verdik nesne örneği oluşturduk ve metodunu kullandık. Veriyi oracle’dan çektiğimizi görelim.
Burada kullandığımız sınıfı OracleConnection yerine MsSQLConnection olarak değiştirdiğimiz anda Helper sınıfı içerisinde(ve varsa başka sınıflarda) kullanıldığı tüm yerlerde de kodda manuel değişiklikler yapmak gerekecektir. Bu yüzden OracleConnection sınıfında yapılan değişiklikler helper sınıfımıza da yansıyacaktır. Temel problemimiz de aslında bu olacak.
2- Factory Pattern ile IoC İmplementasyonu
Bağımlılıkları azaltmak için adım adım örneğimizi ilerletiyorum. Bir factory sınıfı oluşturalım ve bir Connection örneği dönmesini sağlayalım. Client, Factory’den kendisine IConnection tipinde bir instance oluşturup vermesini ister, Factory bu nesneyi nasıl oluşturacağını bilir ve nesneyi Client’a döndürür.
public class ConnectionFactory { public static OracleConnection GetCon() { return new OracleConnection(); } }
Artık helper sınıfı Connection nesne örneğini dışarıdan alacak şekilde tasarlanmış oldu.
public class ConnectionHelper { private OracleConnection _oConnection; public ConnectionHelper() { _oConnection = ConnectionFactory.GetCon(); } public string GetData() { return _oConnection.GetData(); } }
3- Abstraction Oluşturarak DIP İmplementasyonu
IConnection adında bir interface oluşturalım. İçerisine GetData metodunu ekleyelim, hatırlarsanız bu metot, örneğimizde sıkı bağlı sınıflar içerisinde kullanılıyor.
public interface IConnection { string GetData(); }
Oracle ve MsSQL sınıflarımız bu interface’i implement etsin.
public class OracleConnection : IConnection { public string GetData() { return "Oracle"; } }
public class MsSQLConnection : IConnection { public string GetData() { return "MsSQL"; } }
ConnectionFactory sınıfı geriye somut bir nesne döndürüyorken bu interface sayesinde artık soyut nesne (IConnection) döndürecek.
public class ConnectionFactory { public static IConnection GetCon() { return new OracleConnection(); } }
Aşağıdaki şekilde IConnection interface’ini kullandığımız için ConnectionHelper’ın Oracle ve MsSQL sınıfları hakkında bilgi sahibi olmasını engellemiş olduk.
public class ConnectionHelper { private IConnection _connection; public ConnectionHelper() { _connection = ConnectionFactory.GetCon(); } public string GetData() { return _connection.GetData(); } }
4- DI İmplementasyonu
Dependency Injection tasarım kalıbını kullanarak Connection’ı elde edelim. Inject etmek için property, metot ve constructor yolu ile olmak üzere 3 yöntem bulunmaktadır. Constructor yolu ile elde ederek devam edelim. Burada iki sınıf arasındaki bağı biraz daha gevşetmiş olduk.
public class ConnectionHelper { private IConnection _connection; public ConnectionHelper(IConnection connection) { _connection = connection; } public string GetData() { return _connection.GetData(); } }
Database kaynağımızı MsSQL’e çevirmek istiyorsak artık bunu Helper sınıfının nesnesinin oluştuğu yerde MsSQLConnection sınfını parametre olarak vererek yapmak gerekiyor.
[Route("api/[controller]")] [ApiController] public class ValuesController : ControllerBase { private ConnectionHelper _connectionHelper; public ValuesController() { _connectionHelper = new ConnectionHelper(new MsSQLConnection()); } [HttpGet] public ActionResult Get() { return _connectionHelper.GetData(); } }
Çalıştırdığımızda verinin MsSQL üzerinden geldiğini görüyoruz.
Tek bir yerden yöneterek kodun genel yapısını bozmadan değişime açık bir kod geliştirmiş olduk. Projenin son hali :
5- Dependency Injection Container
Projemizde dependency injection kullanmak için birçok 3. parti kütüphane bulunmaktadır. Bu kütüphanelerden birini yükleyerek projede kullanabiliriz. Örneğimizi .NET Core’da yaptığımız ve .NET Core içerisinde de hali hazırda bir dependency injection mekanizması bulunduğu için ekstra bir kütüphane kurmadan örneğimize devam ediyoruz.
Dependency injection uygulanan nesnelerin yaşam sürelerini(lifetime) belirlemek için 3 farklı seçenek bulunuyor.
- Transient: Obje her istenildiğinde yeni bir instance oluşturularak verilir.
- Scoped: Yeni bir istek geldiğinde yeni bir obje oluşturulur.
- Singleton: İlk istek geldiğinde bir tane instance oluşturulur, sonrasında gelen tüm isteklerde her zaman aynı instance verilir.
Bölüm 4’e kadar henüz bir container kullanmıyorduk. Şimdi default gelen container ile nasıl bir yol izleriz ona bakalım. Startup sınıfında bulunan ConfigureServices metodunun içerisinde 3 farklı yöntemle nesne örneği oluşturmasını sağlayabiliriz. Startup sınıfı .NET Core projelerinin başlangıç noktasıdır, farklı platformda kod yazıyosanız bu yapılandırmayı benzeri bir sınıf veya metotta yapabilirsiniz.
Görüldüğü gibi artık bağımlılığı tek bir noktaya indirdik. Az sonra Controller tarafında doğrudan IConnection ile çalışacak şekilde yapımızı kuracağız. Dolayısıyla business vb. katmanlarda artık OracleConnection gibi bir sınıfa bağımlı kalmayacağız. Sonraki aşamalarda MsSQLConnection gibi farklı bir bağlantı sınıfı kullanmamız gerekirse yapmamız gereken tek değişiklik yukarıdaki kod parçasındaki ifadeyi services.AddSingleton<IConnection, MsSQLConnection> ile güncellemek olacaktır.
Controller tarafında tek yapmamız gereken bu bağımlılığı constructor içerisine inject etmek. Görüldüğü üzere projemizde bağımlılık yönetimini, nesne yaşam döngüsünü bizim yerimize container üstlenmiş oldu. IoC Container kullanarak yazdığım proje ise aşağıdaki gibidir.
[Route("api/[controller]")] [ApiController] public class ValuesController : ControllerBase { private readonly IConnection _connection; public ValuesController(IConnection connection) { _connection = connection; } [HttpGet] public ActionResult Get() { return _connection.GetData(); } }
Kodlara github hesabımdan ulaşabilirsiniz.
İyi Çalışmalar..