Burada sürekli bir takım yazılımsal kavramlardan ve tasarım kalıplarından bahsetmeye çalışıyorum. Tabi bunların hepsinin tam olarak ne ifade ettiklerini anlayabilmek için öncelikle problemleri anlayabiliyor olmak çok büyük bir önem taşıyor. Bu yüzden elimden geldiğince gerçek hayat örnekleriyle anlatmak istediklerimi örneklemeye çalışıyorum.
Bu yazıda sizlere sıkça duyduğumuz Dependency Injection ya da başka bir değişle Inversion Of Control adı verilen kalıptan bahsediyor olacağım. Ancak bunu anlatabilmek için öncelikle Loose Coupling den bahsetmek istiyorum.
Loose Coupling
Önceki yazılarımda nesneye yönelik programlamanın(oop) bir prensibi olan "Single Responsibility" den ve neden gerekli olduğundan bahsetmiştim. Ancak hızlı bir özet geçmemiz gerekirse, yazdığımız sınıfların ya da program parçalarının amaçlarına uygun olarak bölünmesi ve amaçlarının dışında bir görevi üstlenmemeleri bize yazdığımız bu parçaların başka projelerde ya da mevcut program içerisinde daha çok kullanılabilmesinde fayda sağlamaktadır. Bu yüzden yapacağımız tasarımlarda sınıfların daima tek bir sorumluluğu olmalıdır. Böylece bir takım şeyleri tekrar tekrar kullanmaktan kurtulmuş oluruz ve bu da bize elbette zaman kazandırmış olur. Single Responsibility'nin bir sonraki adımı olarak da Loose Coupling düşünülebilir. Yani mümkün oldukça bir sınıf başka sınıflara bağımlı olmamalı, bunun yanında da tek bir amaca hizmet etmelidir. Burada bağımlılıktan kastımı biraz daha açıklamak istiyorum. Diyelim ki bir sınıfınız var. Bu sınıfınızda bir takım işlemler yapıyorsunuz. Örneğin bu sınıf girdi olarak bir nesne alıyorsa, sizin bu sınıfınız bu nesneye bağımlıdır. Çünkü bu nesne olmadan sınıfınızın bir anlamı yoktur. Benzer şekilde, girdi nesnesinin peşindeki diğer nesneler de bağımlılığı arttıracaktır. Bunun bir dezavantajı olarak yazdığınız sınıf da bağımlı olduğu nesnelerin değişmesi durumunda bu değişiklikten etkilenecektir.
Örnek bir entegrasyon senaryosu
Biraz daha konuya ısınmaya başladığımıza göre basit bir ihtiyaç hakkında konuşmaya başlayalım; Müşteriniz ile bir projeye başladınız. Yapmaya çalıştığınız şey ise basit bir stok programı. Müşterinizin ihtiyacına göre yaptığınız analiz doğrultusunda stok bilgilerini, müşterinizin envanter kayıtlarını sakladığı bir yazılımın veritabanından almanız gerekiyor. Ancak veritabanına erişime izin vermiyor da bu yazılım firması size bir web servis ile bu verileri verebileceğini söylüyor. Siz de bunu göz önüne alarak bu altyapı üzerine projenizi yapmaya başlıyorsunuz. Bir zaman sonra, henüz projeniz bitmeden, karşı taraftaki firma web servisi kapatıyor ve daha ileri bir teknoloji olan WCF üzerinden yayın yapmaya başlıyor. Tabi size de bunu bildirdiğinde siz gidip kodunuzu tekrar değiştiriyorsunuz. Bir zaman sonra projenizi teslim ediyorsunuz ve tamamlamış oluyorsunuz, ya da siz öyle sanıyorsunuz. Aradan bir kaç ay geçiyor ve müşteriniz size envanter kayıtlarını tutan programı değiştirdiklerini, eski firma yerine başka bir firma ile çalıştıklarını ve verilerin hepsini yeni yazılımın veritabanına taşıdıklarını söylüyor. Bunun doğal sonucu olarak sizin de verilerinizi artık oradan almanız gerekiyor. Ancak bu sefer durum biraz daha farklı çünkü oradaki firma ihtiyacınız olan verileri sizin veritabanınıza belli aralıklarla taşıyabileceğini söylüyor ve sizin de verileri buradan almanız gerektiğini anlatıyor. Bu durumda yine kolları sıvayıp koda girişiyorsunuz.
Yukarıdaki senaryo eminim aşağı yukarı benzer de olsa pek çok yazılımcının başına gelmektedir. Bu durumda nasıl bir mimari tasarlanmalı ya da nasıl bir yol izlenmeli?
İşte burada işin içine Dependency Injection(DI) dediğimiz kavram giriyor. Yukarıda da anlattığım gibi DI'in en ciddi gereksinim haline geldiği zaman bağımlılıkların yönetilmesi anıdır. Yukarıdaki senaryoda yazılan programın ana modulü stok verisinin haricinde veriyi nasıl alacağına da odaklanırsa yapılan iş içinden çıkılamayacak bir hal alır. Halbuki düşünüldüğü zaman yazılan programın yapması gereken iş ile veriyi nereden aldığının hiç bir ilişkisi yoktur. Burada asıl önemli olan, getirilen veridir ve programın çekirdeğinin de sadece gelecek olan veriyi bilmesi gerekmektedir. Bu durumda programın çekirdeğindeki verilere ihtiyaç duyan sınıfın tek bilmesi gereken bir adaptör nesnesinden veriyi kendi bildiği şekilde alacak olmasıdır. Bundan sonra da veri web servisten mi gelmiş, WCF servisten mi gelmiş yoksa bir veritabanından mı alınmış gibi konularla ilgilenmesine ihtiyacı olmayacaktır.
Şimdi yukarıdaki maddeleri göz önüne alarak bir tasarım yapmaya çalışalım. Bu tasarıma da programın çekirdeğinden başlayalım. Ben burada boş bir solution yaratıp, içerisine bir adet console application ekliyorum. Programımın çekirdeğinin burası olduğunu varsayıyorum. Aşağıdaki gibi bir şekilde projemize başlayalım;
public class StoreWinApplication
{
private IStoreProvider m_StoreProvider;
public IStoreProvider StoreProvider
{
get { return m_StoreProvider; }
set { m_StoreProvider = value; }
}
internal IStoreInformation GetStoreInformation()
{
return StoreProvider.LoadInformation();
}
}
Yukarıdaki sınıfa baktığınızda ihtiyacımız olan veriyi çekebilecek olan bir StoreProvider özelliğine sahip. Bu özellik de IStoreProvider tipinde. Yani bir arayüz olarak dışarıya açılmış durumda. Aynı zamanda GetInformation() methoduna bakacak olursak bizim IStoreProvider arayüzünü tanımlayan özelliğimizi, StoreProvider 'ı çalıştırmakta ve karşılığında yine bir arayüz olan IStoreInformation tipinde bir nesne döndürmektedir. Bunu yaparak şu anda business nesnemizi verinin alınacağı yerden ayırmış olduk. Böylece veritabanı, wcf ya da web servis bağımlılığını da kırmış olduk. Bunu kullanırken de aşağıdaki gibi kullanacağız,
static void Main(string[] args)
{
StoreWinApplication application = new StoreWinApplication();
application.StoreProvider = new DatabaseStoreProvider();
IStoreInformation information = application.GetStoreInformation();
Console.WriteLine(information.Name);
}
yukarıda da gördüğünüz gibi çalışan sınıfımızın StoreProvider özelliğinin değerini dışarıdan DatabaseStoreProvider tipindeki ve IStoreProvider arayüzünü tanımlayan sınıf olarak belirledik. Böylece çalıştırdığımızda da veriyi veritabanından alacaktır. Böylelikle ilerideki bir zamanda eklenecek olan yeni bir veri kaynağının da bu işleme dahil olması çok kolay olmuş oldu.
Ancak hala başka bir sorunumuz var. Her yeni eklenen provider için tekrar tekrar kodumuzu derlememiz gerekiyor. O zaman bunu gönderdiğimizde, müşterinin istediği şekilde değişiklik yapabilmesi için eklediği her bir sınıfla birlikte bizim kodumuzu derlemesi gerekmekte. Ve bunları da yine buradan gördüğünüz üzere çalışma anında düzenleyebilmesi için bir şey yapamamış olduk.
Bu gibi sorunları çözebilmenin elbette bir çok yolu olabilir. Ancak ben bu yazıda sizlere Unity Application Block 'tan bahsetmek istiyorum.
Unity Application Block
Unity http://unity.codeplex.com/ adresinden erişebileceğiniz bir dependency injection container dır ve Microsoft Patterns&Practices tarafından geliştirilmektedir. Hakkında kısaca bahsetmek gerekirse, bizim application config üzerinde (web ise web.config, windows ise app.config) tanımladığımız arayüzlerin ve o arayüzlere karşılık gelen sınıfların çalışma anında bir kutuya yüklenmesini ve daha sonra ihtiyaç halinde ilgili arayüz için yüklenen sınıfın istenen kişiye verilmesini sağlar. Yani yazılımcı ya da müşteri, uygulamanın konfigurasyon dosyasına sizin belirlediğiniz arayüzünüze (bu uygulamada IStoreProvider) karşılık gelecek olan sınıfın tanımını yapıyor ve buna göre o arayüz çağrıldığında size o arayüz için kullanıcının tanımladığı sınıfın örneğini yaratıp verebiliyor. Bu da dikkat ederseniz, bizim az önce hakkında konuştuğumuz problemin ta kendisi!
Bu ihtiyacımıza yönelik güzel bir çözüm de bulduğumuza göre, unity 'yi biraz daha incelemeye koyulalım. Öncelikle konfigurasyon dosyasında nasıl bir değişiklik yapmam gerektiğine bakalım;
<?xmlversion="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,
Microsoft.Practices.Unity.Configuration"/>
</configSections>
<unity>
<containers>
<container>
<type type="StoreClientLibrary.IStoreProvider,StoreClientLibrary"
mapTo="ServiceStoreProviderLibrary.ServiceStoreProvider, ServiceStoreProviderLibrary"/>
</container>
</containers>
</unity>
Konfigurasyon dosyasına baktığımızda en üstte yeni bir config section eklememiz gerektiğini görüyoruz. Burada Unity'nin gerekli bilgileri unity xml düğümünde okuyacağını bildiriyor. Bundan sonraki kısım ise az önce de bahsettiğim hangi tip için hangi nesnenin oluşturulacağı bilgisini tutuyor. Burada birden fazla container yaratıp bu container ların herbirini istediğiniz gibi hem uygulama ayağa kalkarken hem de çalışma anında değiştirebilmeniz de mümkün. Peki bu container'ı nasıl kullanacağımıza bir göz atalım;
IUnityContainer container = new UnityContainer();
UnityConfigurationSection unityConfig =
(UnityConfigurationSection)ConfigurationManager.GetSection("unity");
unityConfig.Configure(container);
Öncelikle bir UnityContainer yaratıyoruz. Ardından app.config üzerinden ilgili container a ait olan konfigurasyon bilgisini yüklüyoruz ve container ımızı uygun şekilde ayarlaması için configuration section a veriyoruz. Böylece konfigurasyon dosyasında tanımlamış olduğumuz tüm tipler container içerisine yüklenmiş oluyor. Bundan sonra aşağıdaki gibi istediğimiz tipi çağırabiliriz,
IStoreProvider provider = container.Resolve<IStoreProvider>();
Artık bu container içerisinden istediğimiz tipi kaldırıp yerine istediğimiz tipi tekrar koyabiliriz. Tabi böylece container ın bilgilerini konfigurasyon dosyasından çekmek zorunda değiliz. Aksine veritabanında da bunları saklayabiliriz. Şimdi de refactor edilmiş kod StoreWinApplication sınıfına bir göz atalım;
public class StoreWinApplication
{
private IUnityContainer container = new UnityContainer();
private IStoreProvider m_StoreProvider;
public IStoreProvider StoreProvider
{
get
{
if (m_StoreProvider == null)
{
m_StoreProvider = container.Resolve<IStoreProvider>();
}
return m_StoreProvider;
}
set { m_StoreProvider = value; }
}
public StoreWinApplication()
{
UnityConfigurationSection unityConfig =
(UnityConfigurationSection)ConfigurationManager.GetSection("unity");
unityConfig.Configure(container);
}
internal IStoreInformation GetStoreInformation()
{
return string.Format("Store Information: {0}", StoreProvider.LoadInformation());
}
}
Burada artık container ımızı sınıfımız ilk ayağa kalktığında yaratıyoruz ve ondan sonra StoreProvider özelliğini yükleyebilir hale getirip yüklemiyoruz. İlk çağırım ile birlikte StoreProvider özelliği konfigurasyon dosyasından okunarak yükleniyor.
Böylelikle Dependency Injection nedir ve nasıl yapılabilir öğrenmiş olduk. Bunun yanında Unity Application Block ile ilgili olarak basit bir ön bilgiye de sah ip olmuş olduk. Tabi ki Unity ile yapılabilecek pek çok şey var. Ancak onu da başka bir yazıya bırakıyorum. Bu yazıda yapılan örneği de incelemek için buraya tıklayıp bilgisayarınıza indirebilirsiniz. Örnek projede Unity'nin son versiyonunun ihtiyaç duyulan bazı dll leri mevcut. Ancak unity hakkında daha fazla bilgi almak isterseniz http://unity.codeplex.com/ adresini ziyaret edebilirsiniz.