Makale Özeti

Geliştirdiğimiz uygulamaların kurumsal bir mimariye uygun ve plug and play olmasını istiyorsak uygulamamız gereken prensiplerin başında "Single Responsibility" ve "Loose Coupling" gelmelidir. Biraz daha açıklayacak olursak uygulamamız içindeki her bir bileşen tek bir amaca hizmet etmelidir ve bileşenler arasındaki bağlantı sıkı sıkıya değil de biraz daha hafif bir tarzda bağlantı olmalıdır. Bu ne demektir biraz açıklamak gerekirse bir sınıf başka bir sınıfın içerisindeki metodları direkt çağırıyorsa o sınıfa bağlı demektir. İleride çağrılan sınıftaki metodun yapısında bir değişiklik yapılması uygulama geliştirici için ciddi kodlama maliyeti doğuracaktır. Bilhassa bir paket uygulama geliştirdiğinizi düşünürseniz her müşterinin aynı fonksiyona ihtiyacı olduğu ancak hepsinin bu metodun içindeki hesaplamayı farklı şekilde yaptığı durumlar güzel bir örnek teşkil etmektedir. Bu tarz örneklerde her müşteri için kodu değiştirmeniz hepsi için farklı bir proje oluşturmanız, hepsi için kodu farklı derlemeniz gerekecektir ki bu hiç tavsiye edilen birşey değildir. Zaten durum bir süre sonra içinden çıkılamaz bir hal alacaktır. Dependency Injection ise uygulama arasındaki bu tarz bağımlılıkları azaltmayı hedeflemekte ve gerekli yerlerde bağımlılığı kendisi bir config üzerinden gerekli tipe karar vererek enjekte etmeyi hedeflemektedir. İşte bu makalemde Microsoft Enterprise Library ile gelen Unity Application Block'un nasıl kullanılabileceğinden bahsedeceğim.

Makale

Merhabalar,

Bu makalemde sizlerle Dependency Injection ve Unity Application Block hakkında bilgiler paylaşmaya çalışacağım.

Geliştirdiğimiz uygulamaların kurumsal bir mimariye uygun ve plug and play olmasını istiyorsak uygulamamız gereken prensiplerin başında "Single Responsibility" ve "Loose Coupling" gelmelidir. Biraz daha açıklayacak olursak uygulamamız içindeki her bir bileşen tek bir amaca hizmet etmelidir ve bileşenler arasındaki bağlantı sıkı sıkıya değil de biraz daha hafif bir tarzda bağlantı olmalıdır. Bu ne demektir biraz açıklamak gerekirse bir sınıf başka bir sınıfın içerisindeki metodları direkt çağırıyorsa o sınıfa bağlı demektir. İleride çağrılan sınıftaki metodun yapısında bir değişiklik yapılması uygulama geliştirici için ciddi kodlama maliyeti doğuracaktır. Bilhassa bir paket uygulama geliştirdiğinizi düşünürseniz her müşterinin aynı fonksiyona ihtiyacı olduğu ancak hepsinin bu metodun içindeki hesaplamayı farklı şekilde yaptığı durumlar güzel bir örnek teşkil etmektedir. Bu tarz örneklerde her müşteri için kodu değiştirmeniz hepsi için farklı bir proje oluşturmanız, hepsi için kodu farklı derlemeniz gerekecektir ki bu hiç tavsiye edilen birşey değildir. Zaten durum bir süre sonra içinden çıkılamaz bir hal alacaktır.

Dependency Injection ise uygulama arasındaki bu tarz bağımlılıkları azaltmayı hedeflemekte ve gerekli yerlerde bağımlılığı kendisi bir config üzerinden gerekli tipe karar vererek enjekte etmeyi hedeflemektedir. İşte bu makalemde Microsoft Enterprise Library ile gelen Unity Application Block'un nasıl kullanılabileceğinden bahsedeceğim.

Şu anda Enterprise Library'nin en güncel versiyonu 5.0'dır ve http://www.microsoft.com/download/en/details.aspx?displaylang=en&id=15104 adresinden indirilebilir.

Öncelikle bir konsol uygulaması oluşturarak başlayalım. EnterpriseLibrary'i kurduktan sonra gerekli referansları projemize eklememiz gerekecek.

Bu referansları projemize ekledikten sonra artık uygulamamızı oluşturmaya başlayalım. Yapacağımız örnekte bir UI, bir servis (iş) ve de bir Veri katmanı projesi olsun. Bu 3 proje arasındaki bağımlılığı nasıl azaltacağımızı ve unity'i nasıl kullanacağımızı inceleyeceğiz.

UI katmanımız Servis katmanımız ile direkt olarak bir bağlantı kurmayacak ve bu bağlantı Contract adı verilen başka bir aracı class library ile gerçekleşecek. Buralardaki bağlantı kurma görevlerini sağlamak için kod yazmayacağız ancak interface tanımlamaları yapacağız. UI katmanımızda ise servis katmanının referansı yerine Contract'ın referansı bulunacak. Dolayısıyla UI katmanımız servisin kendisine sağladığı fonksiyonları, özellikleri bilecek ancak bu işlemlerin nasıl yapıldığı konusunda bilgi sahibi olmayacak. Aynı şekilde Servis ve Data katmanı arasındaki bağlantı da bu şekilde olacak.Şimdi projelerimizi oluşturup hiyerarşimize bir bakalım.

Gördüğünüz gibi UI, Service ve Data katmanlarımız birbirlerine sıkı sıkıya bağlı değil ve interfaceler aracılığıyla birbirlerinin hangi işleri yapabileceklerini biliyorlar ancak nasıl yaptıkları ile ilgilenmiyorlar. Şimdi esas soru benim elimde class yok sadece interface var ben bu interface'den implement olmuş class'ın yapısını biliyorum. Burada class'ı nasıl çağıracağım konusu çözülmesi gereken esas konu. İşte dependency injection bu noktada devreye giriyor. Temel olarak bir interface'in tip olarak gösterildiği property'e eriştiğimizde o interface'den implement olmuş herhangi bir class'ı kullanmalıyız. İşte bu class'ı bu property üzerine unity enjekte edecek. Yani bu class'a olan bağımlılık esasında yok ama çalışma zamanında enjekte edilerek oluşturulacak.

İsterseniz bunu unity ile nasıl yapabileceğimizi inceleyelim.

Öncelikle unity için app.config veya web.config veya benzer bir config dosyamızda bazı konfigurasyon tanımlamaları yapmalıyız. Şimdi bu config yapısını inceleyelim.

Config yapımızda root element Unity, bunun altında typeAliases, sectionExtensions ve containers bulunuyor. typeAliases config dosyamıza bundan sonra kullanacağımız tipleri uzun uzun yazmak yerine bir alias tanımlamamıza yarıyor. sectionExtensions ise unity içerisinde bir extension kullanacaksak bunu tanımlamamıza yarıyor (Örneğin interception). Interception'a bu makalemizin ikinci kısmında değineceğiz. containers içerisinde ise IOC container'da kullanacağımız tipleri ve bu tiplerin hangi tiplerle denk geleceğini tanımlıyoruz. Yani Dependency injection'ın yapılabilmesi için gerekli tanımlamalar burada yapılıyor. Uygulamamızda kullanacağımız config dosyasını oluşturmaya başlayalım.

<?xml version="1.0"?>
<configuration>
  <configSections>
    <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,Microsoft.Practices.Unity.Configuration"/>
  </configSections>
  <unity>
    <typeAliases>
      <!-- alias tanımlaması bu şekilde yapılıyor. Config dosyamızda bu tanımlamadan sonra alias tanımlamasını kullanarak bu tiplere erişebileceğiz.-->
      <typeAlias alias="INotificationService" type="ContractsService.INotificationService,ContractsService"/>
      <typeAlias alias="NotificationService" type="Service.NotificationService,Service"/>
    </typeAliases>
    <containers>
      <container>
        <types>
          <!-- Alias tanımlaması ile mapping örneği -->
          <!-- INotificationService kullanılan yerlerde NotificationService tipinin ayağa kaldırılması için -->
          <type type="INotificationService" mapTo="NotificationService">
            <!-- NotificationService içindeki UserData isimli property'nin NotificationService ayağa kalktığında tanımlı mappingle ayağa kaldırılması için -->
            <property name="UserData" >
              <dependency />
            </property>      
          </type>
          <!-- Alias tanımlaması olmadan yapılmış mapping örneği -->
          <type type="ContractsData.IUserData,ContractsData" mapTo="Data.UserData,Data">
            <!-- UserData ayağa kalktığında herhangi bir propertynin ayağa kalkmasını istemiyoruz.-->
          </type>
        </types>
      </container>
    </containers>
  </unity>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
  </startup>
</configuration>

Gördüğünüz gibi config tanımlaması çok zor değil. İsterseniz şimdi Unity'i nasıl kullanacağımızı görelim.

Öncelikle uygulamamızın içinden kullanabileceğimiz UnityHelper adında bir class yazalım.

    public static class UnityHelper
    {
        static UnityContainer container = new UnityContainer();
        static UnityHelper()
        {
            UnityConfigurationSection section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");
            section.Configure(container);
        }
        public static object Resolve(Type key)
        {
            return container.Resolve(key);
        }
        public static T Resolve<T>()
        {
            return container.Resolve<T>();
        }
        public static T Resolve<T>(IDictionary args)
        {
            ResolverOverride[] ov = new ResolverOverride[args.Count];
            int i = 0;
            foreach (DictionaryEntry de in args)
            {
                ResolverOverride r = new ParameterOverride(de.Key.ToString(), de.Value);
                ov[i] = r;
                i++;
            }
            return container.Resolve<T>(ov);
        }
    }

Gördüğünüz gibi tüm işlemi UnityContainer yapıyor. Bu class'ın bir instance'ını aldıktan sonra static metodumuzun constructor'unda config dosyasından container'ımızın ayarlarını yapmasını sağlıyoruz. Sonrasında ise Resolve metodunu kullanarak istediğimiz tipe karşılık gelen tipi ayağa kaldırabiliyoruz. Burada resolve değeri parametre olarak bir tip alabilir, bir generic üzerinden çalışabilir veya ayağa kaldırmak istediğimiz nesnenin constructor'una geçireceğimiz parametreleri alabilir.

Şimdi ise class'larımızı aşağıdaki gibi yazalım.

namespace ContractData
{
    public interface IUserData
    {
        string[] GetUsers();
        string GetUserByEmail(string s);
    }
}
namespace ContractService
{
    public interface INotificationService
    {
        void NotifyAllUsers();
        void NotifyUserByEmail(string email);
        void SetDefaultEmailAddress(string email);
        string GetDefaultEmailAddress();
    }
}
namespace Data
{
    public class UserData : IUserData
    {
        public string[] GetUsers()
        {
            return new string[] { "User1", "User2" };
        }
 
        public string GetUserByEmail(string s)
        {
            return "User1";
        }
    }
}
namespace Service
{
    public class NotificationService : INotificationService
    {
        public IUserData UserData { get; set; }
        private string defaultEmailAddress = string.Empty;
        public NotificationService()
        {
 
        }
        public void NotifyAllUsers()
        {
            foreach (string s in this.UserData.GetUsers())
            {
                Console.WriteLine(s + " notified");
            }
        }
 
        public void NotifyUserByEmail(string email)
        {
            string s = this.UserData.GetUserByEmail(email);
            Console.WriteLine(s + " notified");
        }
    
        public void SetDefaultEmailAddress(string email)
        {
            defaultEmailAddress = email;
        }
 
        public string GetDefaultEmailAddress()
        {
            return defaultEmailAddress;
        }
    }
}
    public class UIComponent
    {
        public INotificationService NotificationService { get; set; }
        public UIComponent()
        {
            NotificationService = UI.UnityHelper.Resolve<INotificationService>();
        }
        public void NotifyAll()
        {
            this.NotificationService.NotifyAllUsers();
        }
        public void Notify(string email)
        {
            this.NotificationService.NotifyUserByEmail(email);
        }
    }

Bu class'lar yukarıda göreceğiniz proje hiyerarşisinde uygun projeler içinde yer almaktadır. Gördüğünüz gibi class'lar arasında direkt bir ilişki yok ve ilişki tamamen interface'ler üzerinden kurulmuştur. IOC ile nesnelerimizi ayağa kaldırmak için uygulamada bir yerden yazdığımız UnityHelper class'ının Resolve metodunu çağırmalıyız. Ben burda UI bileşeninin constructor'unu uygun gördüm. Gördüğünüz gibi tipi interface olan property'e hiç bir class referansı atanmıyor ancak resolve dedikten sonra mapping olarak tanımladığımız sınıfın instance'ını kullanabiliyoruz. Örnek kodları download kısmından indirebilirsiniz.

Şimdi bu sınıfı çağıralım

        static void Main(string[] args)
        {
            UI.UIComponent insUIComponent = new UI.UIComponent();
            insUIComponent.Notify("oztamer@hotmail.com");
            Console.Read();
        }

Şimdi uygulamamızı çalıştıralım. Aşağıdaki gibi bir hata alacağız.

Bunun sebebi UI projemizde Service, Data ve ContractData projelerinin referansının bulunmamasıdır. Bu projeleri projemize referans eklersek yine bir bağımlılık yaratmış olacağız işte dependency injection'ın güzelliği biraz da burada ortaya çıkıyor. Bu projelerin dll'lerini uygulamamızın exe'sinin olduğu klasöre veya web uygulaması ise bin klasörüne taşıyoruz ve uygulamamızı tekrar çalıştırıyoruz.

Uygulamamız sorunsuz çalıştı. Gördüğünüz gibi UI projemiz Service ve Data projemize runtime'da dependent olmaya başlıyor ve dependency enjekte ediliyor. Buradaki güzel avantajlardan bir tanesi ise tip tanımlamasını versiyon ve publickeytoken kullanarak yapmanız durumunda uygulamanızı tekrar build etmeden dll'leri değiştirebilmenizdir. Veya gördüğünüz gibi Service.dll'in yeni bir versiyonunu sadece kopyalamanız yetecektir.

Makalemin bundan sonraki kısımlarında singleton ve transient yapılar ile interceptor'u nasıl kullanabileceğimize bu örnek üzerinden değineceğim.

Ornek Kodlar