Dependency Injection ve Ninject
Nesne yönelimli programlama metodolojisi ile yazılımın geliştirildiği ortamlarda ilerleyen süreçlerde nesneler arası bağ kurmak zor olabiliyor. Bir nesnede yapılan değişiklikler veya yerine başka nesneyi koymak, başka yerlerde problemlere yol açabiliyor. Bu problemleri en aza indirmek için de Dependency Injection(DI) gibi yapılara ihtiyaç duyuyoruz.
Dependency Injection, SOLID’ın D harfi olan Dependency Inversion’u uygulayan bir tasarım desenidir.
“Dependency Inversion” prensibi, sınıfları arası geçişlerin “loosely coupled” yani “gevşek bağlı” olması anlamına geliyor. “Dependency Injection” ise bu gevşek bağlılık olayını gerçekleştiren bir tasarım deseni. Tabi teoride böyle olsa da pratikte çoğu zaman ihmal edilebiliyor.
Gevşek veya sıkı bağlılık olayından bahsedecek olursak bunu bir görsel üzerinden izah edelim.
Yukarıdaki görselden anlaşılacağı gibi A sınıfı, B sınıfının bazı metotlarını kullandığından dolayı burada bir bağımlılık söz konusudur. Biraz daha açacak olursak A sınıfının, B sınıfının metotlarını kullanması için onun bir instance yani örneğine ihtiyacı vardır. Bunu da new keyword’u ile oluştururuz. Olay tamamen burada başlıyor, biz A sınıfının içerisinde bir B sınıfının örneğini oluşturduğumuz an B sınıfına bağlı olmuş oluruz.
Biraz daha gerçek hayattan senaryolar verecek olursak;
- Kurumsal mimaride katmanlar arası geçişlerde
- 3. parti kütüphanelerin geçişlerinde(NLog, Log4NET)
- Cross Cutting Concerns operasyonlarında
- Farklı veri tabanları ile çalıştığımızda
- ORM’ler arası geçişlerde
- Aynı katman üzerindeki sınıflar arası operasyonlarda
gibi yerlerde bu tarz bağımlılıklarla sıkça karşılaşabiliyor ve çözüm için DI gibi yapılara ihtiyaç duyabiliyoruz.
Bağımlıkların neler olduğundan ve diğer temel konular hakkında umuyoruz zihnimizde bir şeyler canlanmıştır. Aşağıdaki örnek ile konuyu biraz daha da pekiştirelim.
Gideceğimiz örnekte;
- Business işleri için bir ProductManager sınıfımız olsun.
- Bu sınıfın veri tabanı işlemlerinde ORM olarak Entity Framework kullanılsın, EfProductDal olsun.
- Temel bir silme işlemini yerine getiriyor olsun.
- Silme işleminden sonra da işlemi veri tabanına logluyor olsun. (Parametrelere vs. takılmayalım, konuya odaklanmak amaçlı)
public class ProductManager { private EfProductDal _productDal { get; set; } private DatabaseLogger _databaseLogger { get; set; } public ProductManager() { _productDal= new EfProductDal(); _databaseLogger = new DatabaseLogger(); } public void RemoveProductById(int productId) { _productDal.RemoveProductById(productId); _databaseLogger.Log(); } }
Yukarıdaki koda baktığımız zaman; ProductManager sınıfımız, DatabaseLogger ve EfProductDal sınıflarına göbekten bağlı. Yeni başlayanlar için bir problem görünmezken ilerleyen zamanlarda olası bir değişiklikte kaçınılmaz bir felaket söz konusu.
Basit bir e-ticaret sitesi geliştirdiğimizi ve temel operasyonları yerine getiren en az 10 tane sınıfımız olduğunu varsayalım. Bu sınıfların içerisindeki metotları hesaba katınca belki de yüzlerce metot ortaya çıkacaktır. Hepsinde de log alma, veri tabanına bağlanma gibi temel işlemleri yaptığımızı farz edelim. İlerleyen zamanlarda veri tabanına log atmanın bazı yükler getirdiğini görerek loglarınızı metin(txt) dosyası üzerine atmaya karar verdiniz. Fakat bir sorun var; bütün metotlarınız DatabaseLogger sınıfına göbekten bağlı, işte her şey burada başlıyor. Başlıyorsunuz bütün metotlardan DatabaseLogger sınıfını silmeye ve FileLogger yazmaya durum da şöyle bir hal alıyor.
public class ProductManager { private EfProductDal _productDal { get; set; } private FileLogger _fileLogger { get; set; } public ProductManager() { _productDal = new EfProductDal(); _fileLogger = new FileLogger(); } public void RemoveProductById(int productId) { _productDal.RemoveProductById(productId); _fileLogger.Log(); } }
Basit bir log yapısında bile bir sürü yerlere dokundunuz, ilerleyen zamanlarda ORM değiştirmek istediniz; Entity Framework değil de NHibernate ya da başka işlemler; kullanıcı şifrelerini MD5 yerine SHA256 olarak tutmak istediniz, servislerde değişiklikler yaptınız. Tekrar başlayacaksınız bir sürü kodu düzenlemeye ve proje büyüdükçe bu işten çıkılmaz bir hal alacaktır.
Peki burada yapılması gereken şey ne, ne gibi bir yol izlememiz lazım? İşleri inheritance yani kalıtım üzerinden götürmek daha mantıklı olacaktır. Log alma ve ORM olayını bir interface üzerinden miras alarak ilerleyelim, ne gibi sonuçlar meydana gelecek izleyelim.
public interface ILogger { void Log(); } public class DatabaseLogger : ILogger { public void Log() { //Some Code.. Console.WriteLine("Logged by database!"); } } public class FileLogger : ILogger { public void Log() { //Some Code.. Console.WriteLine("Logged by file!"); } }
Yukarıdaki ILogger adında bir interface oluşturduk, DatabaseLogger ve FileLogger sınıflarına bu ILogger interface’sinden miras aldık. Temel olarak bu 2 sınıfımız da bir ILogger.
public interface IProductDal { void RemoveProductById(int Id); } public class EfProductDal : IProductDal { public void RemoveProductById(int Id) { //Some Entity Framework Code.. Console.WriteLine("Removed product by Entity Framework!"); } } public class NhProductDal : IProductDal { public void RemoveProductById(int Id) { // Some NHibernate Code.. Console.WriteLine("Removed product by NHibernate!"); } }
Yukarıdaki IProductDal adında bir interface oluşturduk, EfProductDal ve NhProductDal sınıflarına bu IProductDal interface’sinden miras aldık. Temel olarak bu 2 sınıfımız da bir IProductDal.
Inheritance işlemlerinden sonra Business kodunu biraz daha düzenleyecek olursak;
public class ProductManager { private IProductDal _productDal { get; set; } private ILogger _fileLogger { get; set; } public ProductManager() { _productDal = new EfProductDal(); _logger = new DatabaseLogger(); } public void RemoveProductsById(int productId) { _productDal.RemoveProductById(productId); _logger.Log(); } }
Bağımlılıkları bir nebze azaltılmış gibi duruyor fakat new anahtar kelimesinin kullanımı devam ediyor, hala bir bağımlılık söz konusu. Burada yapılması gereken şey nesnelerin referanslarını parametre olarak constructor içerisinden almak. (constructor based dependency injection)
Constructor based dependency injection özelliklerinden birisi sıkı olan bağımlılığı gevşek bağlı hale getirmekle birlikte Unit Test işlemlerinde, Moq gibi frameworkler ile testlerimiz için orijinal nesnelerin yerine geçecek olan sahte nesneler üretmemize olanak sağlaması.
Koda tekrar göz atmak istediğimizde;
public class ProductManager { private IProductDal _productDal { get; set; } private ILogger _logger { get; set; } public ProductManager(IProductDal productDal, ILogger logger) { _productDal = productDal; _logger = logger; } public void RemoveProductsById(int productId) { _productDal.RemoveProductById(productId); _logger.Log(); } }
İşte şimdi oldu. Yapımız ne FileLogger ne de DatabaseLogger hiçbirine bağlı değil. ORM için de geçerli bu durum. Hangi nesnenin referansı gelirse tamamen onun gibi davranacak.
Business içerisinde yer alan kodumuzu Main class içerisinde çalıştırmak istediğimizde;
class Program { static void Main(string[] args) { ProductManager productManager = new ProductManager(new EfProductDal(), new FileLogger()); productManager.RemoveProductById(2); } }
ProductManager sınıfının örneğini oluştururken bizden IProductDal ve ILogger interface’lerine sahip olan nesneler istiyor biz de new keyword’u ile oluşturuyoruz.
Buraya kadar her şey güzel, Business kısımlarında bağımlılıkları gevşek bağlı yaptık fakat ProductManager sınıfını birden fazla yerde kullanmak istersek olası Logger ya da ORM değişiminde tekrar birkaç yere müdahale etmek zorunda kalacağız. Bu süreci tek yerden yönetmek için birçok seçenek mevcut. Burada istersek Factory Design Pattern kullanabiliriz istersek de bunun için yazılmış olan framekworkleri kullanabiliriz. Autofac, Ninject, Unity gibi. Bu yazımızda Ninject kullanımından bahsediyor olacağız.
Package Manager Console kısmından install-package ninject diyerek Ninject’i projemize dahil ediyoruz.
Ninject’in run-time sırasında bize birer instance oluşturmasını istiyoruz, bunun için de bizim hangi abstract (interface) karşısına hangi concrete (örnek nesne) geleceğini belirtmemiz lazım Ninject’e. Bunun için bir class oluşturup NinjectModule üzerinden miras almamız gerekiyor. (İlerleyen süreçte IKernel üzerinden verdiğimiz interface’ye karşılık gelen concrete nesneyi isteyeceğiz. IKernel, karşılığını NinjectModule üzerinden alacaktır. NinjectModule bir nevi tanımlamaların bulunduğu bir fabrikamızdır.)
InstanceModule adında bir class oluşturup ve NinjectModule üzerinden miras alma işlemini gerçekleştiriyoruz. Eğer sizde NinjectModule sınıfını gelmiyorsa
using Ninject.Modules; yazmanız gerekiyor.
Miras işleminden sonra bizden Load() metotunu override etmemizi ve gerekli tanımlamaları yapmamızı istiyor..
Tanımlamayı ise Bind<TAbstract>().To<
durum bu şekilde olacaktır;
public class InstanceModule : NinjectModule { public override void Load() { Bind<IProductDal>().To<NhProductDal>(); Bind<ILogger>().To< DatabaseLogger>(); } }
Yukarıdaki tanımlamada eğer IProductDal adında bir interface istenirse NhProductDal, ILogger adında bir interface istenirse DatabaseLogger nesnesi ver demek oluyor.
Hangi elemanları kullanacağımızı yazdık fakat bunu nasıl çağıracağız?
Bunun için de bir class oluşturuyoruz ve adını InstanceFactory koyuyoruz. Ninject arka planda factory design pattern kullandığı için böyle bir isimlendirme kullandık, dilerseniz sizler de başka bir isim de verebilirsiniz.
public class InstanceFactory { }
Bu sınıf üzerinden vereceğimiz tipe göre instance isteyeceğimiz için generic bir GetInstance<T>() adında metot yazıyoruz. Aldığı tipin örneğini döndürmeye yarayacak ve sürekli örneğini oluşturmaktansa metot static olarak tanımlıyoruz.
Bundan sonrasını ise süreci IKernel interface üzerinden ilerletiyoruz, içerisine tanımlamaları yaptığımız sınıfı (InstanceFactory) parametre olarak veriyoruz ve bize istediğimiz tipin örneğini dönüyor.
public class InstanceFactory { private static IKernel _kernel { get; set; } public static T GetInstance() { _kernel = new StandardKernel(new InstanceModule()); return _kernel.Get(); } }
Bunları yaptıktan sonra Main metodunu biraz daha düzenleyelim.
class Program { static void Main(string[] args) { ILogger logger = InstanceFactory.GetInstance(); IProductDal productDal = InstanceFactory.GetInstance(); ProductManager productManager = new ProductManager(productDal, logger); productManager.RemoveProductById(2); Console.ReadKey(); } }
Çalıştırdığımızda ise sonucun
Logged by database!
Ninject ile bu kadarını bile bilmemiz bizi birçok dertten kurtaracaktır.
Sonuç olarak yazımızda
- Sınıflar arası sıkı sıkıya olan bağımlılıkları, “loosely coupled” yani “gevşek bağlı” hale getirdik.
- İstenilen sınıfların değişimini rahat bir şekilde gerçekleştirdik.
- Bakım ve düzenlemeleri kolaylaştırdık.
- Dependency(bağımlılıkların) tek yerden kontrolünü sağladık.
- Constructor based dependency injection ile test edilebilirliği sağladık.
- Ninject kullanımını öğrendik.