Merhaba arkadaşlar, bu makalemde Aspect Oriented Programming (AOP) nedir ve birbirleri ile kesişen ilgiler (Cross-Cutting) nelerdir gibi kavramları açıklayarak AOP serisine bir giriş yapmak istiyorum.

Uzun zamandır giriş yapmayı düşündüğüm bir konuydu aslında AOP fakat fırsat bulup bir türlü başlayamamıştım. Umarım keyifli bir makale serisi çıkar. :)

aop

AOP Nedir ve Neden İhtiyaç Duyarız?

Aspect Oriented Programming yaklaşımı, temelinde birbirleri ile kesişen ilgilerin (Cross-Cutting Concerns) ayrılması üzerinedir. Peki nedir bu birbirleri ile kesişen ilgiler diyecek olursak:

Geliştirdiğimiz modüllere baktığımızda aslında her business işleminde ortak olarak gerçekleştirdiğimiz bazı işlemler vardır. Örneğin kullanıcının yetkisi var mı?, Gelen isteği logla, Exception Handling işlemini yap gibi ve sonrasında ilgili modülün business kısmını gerçekleştirmekteyiz. İşte bu işlemler bizim birbirleri ile kesişen ilgilerimiz (Cross-Cutting-Concerns) oluyor.

Bu modüle birde kodsal açıdan örnek vermek gerekirse:

        public Product GetProduct(int productId)
        {
            try
            {
                // Kullanıcı yetkilerini kontrol et.
                // Yapılan işlemi logla.

                var product = new Product();
                // Product'ı cache'e ilgili productId ile ekle.

                return product;
            }
            catch (Exception ex)
            {
                // Exception handling işlemleri.
                throw;
            }
        }

Örnek metot üzerinde de gördüğümüz üzere genelde business işlemi sırasında yaptığımız temel kontrol ve işlemler. AOP ise bize birbirleri ile kesişen bu ilgilerin birbirlerinden farklı olsada ayrılmaları gerektiğini söylemektedir.

Aspect Oriented Programming’in odak noktası ise konuların ayrımıdır (Separation of Concerns). Object Oriented Programming’de de olduğu üzere oluşturmuş olduğumuz sınıfları, her sınıfın kendi sorumluluğunu yerine getirmesi (Single Responsibility) gibi prensiplere uyarak geliştirdiğimizde oluşan kodun okunabilirliğini (Readability), tekrar kullanabilirliğini (Reusability) ve genişletilebilirliğini (Extensibility) sağlamaktayız. AOP ise bize bu anlamda kodun okunabilirliğini ve tekrar kullanılabilirliğini en üst seviyede sağlamayı amaçlamaktadır konuların ayrımı (Separation of Concerns) altında.

Object Oriented Programming’de soyutlamaya (Abstraction) giderekte kodun tekrar kullanılabilirliğini, okunabilirliğini ve genişletilebilirliğini ve sınıfların birbirine sıkı sıkıya bağlı (Tightly Coupled) olmamasını sağlayabiliriz ama bir noktaya kadar. O nokta geldiğinde ise AOP kesişen ilgilerimizi ele alarak onları tamamen bağımsız (Independent) hale getirmektedir. Bu sayede her bir kesişen ilgimiz, kendi içerisinde bağımsız olarak geliştirilebilir ve tekrar kullanılabilir hale gelmektedir.

Yukarıdaki örnek üzerine AOP uygulayarak yeni bir örnek vermek gerekirse:

        [ExceptionHandling]
        [CheckUserAuthorization]
        [LogProcess]
        [Cache(TimeOut = 10000)]
        public Product GetProduct(int productId)
        {
            var product = new Product();
            return product;
        }

Kesişen ilgilerimizi (Cross-Cutting-Concerns) AOP ile ayırdıktan sonraki hali gerçekten şaka gibi değil mi? :) Okunabilirlik ve tekrar kullanılabilirliği nasılda değiştiğini netçe görebiliyoruz. Açıkçası bu şekilde yapılar geliştirmek, insana kod yazarken de ayrı bir zevk veriyor doğrusu.

Evet, işin terminoloji kısımlarında her şey güzel. Fakat biz bu kesişen ilgilerimizi nasıl ayıracağız sorununa gelirsek, bu konuda yardımımıza Interceptor yapıları çıkmakta. Interceptor’ler bize metot çağrıldığında, öncesinde ve sonrasında araya girerek belirli işlemleri yapabilmemizi sağlayan yapılardır.

Interceptor yapılarını oluşturabilmek için Reflection sınıfından bolca yararlanacağız. :) İşin aslı, bu yapıları kolaylıkla kullanabilmek için bir çok 3rd party uygulama bulunmakta. Örneğin: Castle Windsor, Enterprise Library gibi kütüphaneler. Ben en azından giriş makalemde işin mutfağında neler olup bitiyoru sizlere göstermeden hazır kütüphaneler aracılığı ile bu yapıları kullanmak istemiyorum. Bu yüzden işin eğlenceli kısmı olan kendi interceptor’ümüzü oluşturacağız. Bu mutfağın bir kokusunu alalım ve neler olup bitiyor bir bilelim en azından. :)

Şimdi işin en eğlenceli kısmına geçme vakti. Örneğimizde bizim kesişen ilgilerimiz aşağıdaki modüller olsun:

  • Caching
  • Loging

Bu modüllerimiz’i birbirlerinden bağımsız concern’lara ayırarak aspect’ler aracılığı ile metotların çağırımında, nasıl araya girerek işlemlerimizi gerçekleştirebileceğimizi görelim.

AOPGiris isimli bir konsol uygulaması oluşturuyorum. Konsol uygulamasının içerisine Aspect isimli bir klasör oluşturuyorum ve Aspect’lerimizi burada geliştireceğiz. IAspect isimli bir interface ekliyorum ve bu interface’i implemente ederek aspect modlarımızı oluşturacağız.

namespace AOPGiris.Aspect
{
    public interface IAspect
    {
    }
}

Şimdi ise IAspect interface’ini implemente ederek, aspect modlarımızı oluşturacağız. Bu modlar ise bize metot çağrımında öncesinde veya sonrasında araya girebilmemizi sağlayacaklar.

namespace AOPGiris.Aspect
{
    public interface IAfterAspect : IAspect
    {
        object OnAfter(object value);
    }
}

IAfterAspect metot işlendikten sonra çalışacak olan aspect modudur ve geriye herhangi bir işlem değeri dönmektedir.

namespace AOPGiris.Aspect
{
    public interface IAfterVoidAspect : IAspect
    {
        void OnAfter(object value);
    }
}

IAfterVoidAspect ise imzasından da anlaşılacağı üzere metot işlendikten sonra çalışıp geriye değer dönmemektedir.

namespace AOPGiris.Aspect
{
    interface IBeforeAspect : IAspect
    {
        object OnBefore();
    }
}

IBeforeAspect ise metot işlenmeden önce çalışacak olan aspect modumuzdur.

namespace AOPGiris.Aspect
{
    public interface IBeforeVoidAspect : IAspect
    {
        void OnBefore();
    }
}

IBeforeVoidAspect ise metot işlenmeden önce çalışıp geriye değer dönmeyecek olan aspect modumuzdur. Şimdi sıra geldi kesişen ilgilerimizin aspectlerini oluşturmak için türeceğimiz abstract sınıfımızı oluşturmaya.

using System;

namespace AOPGiris.Aspect
{
    public abstract class AspectBase : Attribute, IAspect
    {

    }
}

AspectBase sınıfını, Attribute soyut sınıfından türetiyoruz ve oluşturmuş olduğumuz IAspect arayüzünüde implemente ediyoruz. Alt yapımız hazır olduğuna göre şimdi modüllerimiz’i oluşturmaya başlayabiliriz.

İlgili aspect’leri oluşturmadan önce modül işlemleri sırasında bize unique key’ler oluşturabilme gibi işlemler için ilgili metot’un ismini, çağırım sırasında gönderilen parametreleri verecek olan yardımcı sınıf amacı ile AspectContext isimli bir sınıf oluşturuyorum.

using System;

namespace AOPGiris.Aspect
{
    public class AspectContext
    {
        private readonly static Lazy<AspectContext> _Instance = new Lazy<AspectContext>(() => new AspectContext());

        private AspectContext()
        {
        }

        public static AspectContext Instance
        {
            get
            {
                return _Instance.Value;
            }
        }

        public string MethodName { get; set; }
        public object[] Arguments { get; set; }
    }
}

Lazy sınıfı aracılığı ile pratik olarak Singleton desenini uyguluyorum ve bize modül işlemleri sırasında gerekli olacak property’leri tanımlıyorum. Aspect klasörünün içerisine Attributes isimli yeni bir klasör oluşturuyorum ve ilk modülümüz olan caching işlemleri için kullanacak olduğumuz CacheAttribute sınıfını kodlamaya başlıyorum.

using System;

namespace AOPGiris.Aspect.Attributes
{
    public class CacheAttribute : AspectBase, IBeforeAspect, IAfterVoidAspect
    {
        public int DurationInMinute { get; set; }

        public object OnBefore()
        {
            string cacheKey = string.Format("{0}_{1}", AspectContext.Instance.MethodName, string.Join("_", AspectContext.Instance.Arguments));

            if(true)
            {
                // gerekli cache key ile kontrol ederek varsa cache'de çağırım öncesi metot'u execute
                // etmeden cache üzerinden ilgili veriyi geri dön.

                Console.WriteLine("{0} isimli cache key ile cache üzerinden geliyorum!", cacheKey);
                return true;
            }
        }

        public void OnAfter(object value)
        {
            string cacheKey = string.Format("{0}_{1}", AspectContext.Instance.MethodName, string.Join("_", AspectContext.Instance.Arguments));

            // cache key ile ilgili veriyi DurationInMinute kullanarak Cache'e ekle veya güncelle.
        }
    }
}

IBeforeAspect ve IAfterVoidAspect kullanarak oluşturmuş olduğum CacheAttribute’ü çağırım öncesi metot execute edilmeden araya girerek cache kontrolü yapar ve varsa cache üzerinden getirerek geriye değeri döner. OnAfter’da ise ilgili işlem execute edildikten sonra veri cache’e eklenir veya güncellenir. Cache işlemimiz bu kadar :) Şimdi sıra Loging işlemine. Attributes klasörü içerisinde şimdide LogAttribute sınıfı oluşturuyorum ve kodlamaya devam ediyoruz.

using System;
using System.Text;

namespace AOPGiris.Aspect.Attributes
{
    public class LogAttribute : AspectBase, IBeforeVoidAspect, IAfterVoidAspect
    {
        public void OnBefore()
        {
            StringBuilder sbLogMessage = new StringBuilder();

            sbLogMessage.AppendLine(string.Format("Method Name: {0}", AspectContext.Instance.MethodName));
            sbLogMessage.AppendLine(string.Format("Arguments: {0}", string.Join(",", AspectContext.Instance.Arguments)));

            // log işlemini gerçekleştir.
            Console.WriteLine("Loging: {0}", sbLogMessage.ToString());
        }

        public void OnAfter(object value)
        {
            // sonrasında log tutmak istenirse.
        }
    }
}

IBeforeVoidAspect ve IAfterVoidAspect kullanarak oluşturmuş olduğum LogAttribute sınıfıda öncesinde loglama işlemini gerçekleştiriyoruz ve geriye herhangi bir değer dönmemize gerek olmadığı için IBeforeVoidAspect türünü implemente ettik. OnAfter’da ise ilgili veriyi işlem sonrası loglama yapabiliriz.

Aspectler bu kadar. Şimdi sıra geldi asıl sıkıntılı kısıma :) Araya girmemizi sağlayacak olan proxy sınıfını tasarlayacağız. Transparent Proxy sınıfı bizim gerçek sınıfımızı temsil ediyor olacak ve ilgili metot’u invoke etmeden önce veya sonrasında istediğimiz işlemleri yapabiliyor olacağız. Bu işlemi yapan hazır kütüphaneler olduğundan bahsetmiştik, EL içindeki Unity veya Castle Windsor gibi.

Bu kütüphanelerde gerçek nesnelerimizin çalışma zamanında birer Transparent Proxy’lerini oluşturarak araya girme işlemlerini gerçekleştirmektedirler. Bu işlemide kendimiz handle edeceğimiz için başlayalım generic bir TransparentProxy sınıfını kodlamaya.

using AOPGiris.Aspect;
using System;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Proxies;

namespace AOPGiris
{
    public class TransparentProxy<T, TI> : RealProxy where T : TI, new() // İlgili tipin örneğini alabilmek için new() constraint'ini ekliyoruz.
    {
        private TransparentProxy()
            : base(typeof(TI)) // RealProxy sınıfına, gerçek nesnemizi temsil edecek olan tipi veriyoruz, ilgili interface'imiz.
        {

        }

        // İlgili tipte RealProxy aracılığı ile proxy'mizi oluşturuyoruz.
        public static TI GenerateProxy()
        {
            var instance = new TransparentProxy<T,TI>();
            return (TI)instance.GetTransparentProxy();
        }

        // İlgili metot çağırıldığında çalışacak olan metotdur.
        public override IMessage Invoke(IMessage msg)
        {
            var methodCallMessage = msg as IMethodCallMessage;
            ReturnMessage returnMessage = null;

            try
            {
                // tipimiz üzerinden metot infoya erişerek ilgili attribute olarak eklenmiş
                // aspect'lerimizi buluyoruz.
                var realType = typeof(T);
                var mInfo = realType.GetMethod(methodCallMessage.MethodName);
                var aspects = mInfo.GetCustomAttributes(typeof(IAspect), true);

                // Hatırlarsanız aspect'lerimiz içerisinde kullanmıştık. Gelen parametreleri dolduruyoruz context'e.
                FillAspectContext(methodCallMessage);

                object response = null;

                // Before aspectlerimizi çalıştırıyoruz önce ve geriye değer dönen varsa respons'a eşitliyoruz.
                CheckBeforeAspect(response, aspects);

                object result = null;

                // Response boş değilse, buradaki veri cache üzerinden de geliyor olabilir ve tekrardan invoke etmeye
                // gerek yok, direkt olarak geriye response dönebiliriz bu durumda.
                if (response != null)
                {
                    returnMessage = new ReturnMessage(response, null, 0, methodCallMessage.LogicalCallContext, methodCallMessage);
                }
                else
                {
                    // Response boş ise ilgili metot'u artık invoke ederek çalıştırıyor ve sonucunu alıyoruz.
                    result = methodCallMessage.MethodBase.Invoke(new T(), methodCallMessage.InArgs);
                    returnMessage = new ReturnMessage(result, null, 0, methodCallMessage.LogicalCallContext, methodCallMessage);
                }

                CheckAfterAspect(result, aspects);

                // After aspectlerimizi'de çalıştırdıktan sonra artık geriye çıktıyı dönebiliriz.
                return returnMessage;
            }
            catch (Exception ex)
            {
                return new ReturnMessage(ex, methodCallMessage);
            }
        }

        private void FillAspectContext(IMethodCallMessage methodCallMessage)
        {
            AspectContext.Instance.MethodName = methodCallMessage.MethodName;
            AspectContext.Instance.Arguments = methodCallMessage.InArgs;
        }

        private void CheckBeforeAspect(object response, object[] aspects)
        {
            foreach (IAspect loopAttribute in aspects)
            {
                if (loopAttribute is IBeforeVoidAspect)
                {
                    ((IBeforeVoidAspect)loopAttribute).OnBefore();
                }
                else if (loopAttribute is IBeforeAspect)
                {
                    response = ((IBeforeAspect)loopAttribute).OnBefore();

                }
            }
        }

        private void CheckAfterAspect(object result, object[] aspects)
        {
            foreach (IAspect loopAttribute in aspects)
            {
                if (loopAttribute is IAfterVoidAspect)
                {
                    ((IAfterVoidAspect)loopAttribute).OnAfter(result);
                }
            }
        }
    }
}

TransparentProxy sınıfımız’da hazır durumda. İlgili bilgileri metot yorum satırlarında bulabilirsiniz. Artık ilgili sınıflarımız için RealProxy aracılığı ile temsilcilik yapacak olan Generic Transparent Proxy sınıfımız hazır ve kod içerisinde de görebileceğiniz gibi aspect’lerimizide implemente etmiş durumdayız.

Şimdi kodlamış olduğumuz aspect’leri test edebilmemiz için bir ProductService isminde sınıf oluşturuyorum ve içerisine GetProduct metodunu oluşturarak aspect’lerimizi işaretliyorum.

using System.Collections.Generic;
using AOPGiris.Aspect.Attributes;

namespace AOPGiris
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public double Price { get; set; }
    }
    public class ProductService
    {
        // In memory olarak bir kaç product ekliyorum.
        private static Dictionary<int, Product> _InMemDb = new Dictionary<int, Product>();

        public ProductService()
        {
            _InMemDb.Add(1, new Product() { Id = 1, Name = "MacBook Air", Price = 3000 });
            _InMemDb.Add(2, new Product() { Id = 2, Name = "Sony Xperia Z Ultra", Price = 1400 });
        }

        [Cache(DurationInMinute = 10)]
        [Log]
        public Product GetProduct(int productId)
        {
            return _InMemDb[productId];
        }
    }
}

ProductService sınıfı için yeni bir örneği alındığında in memory olarak bir dictionary üzerinde Product nesnelerini tutuyorum. GetProduct metodunda ise gördüğümüz gibi sadece dictionary üzerinden nesneyi geri return ediyorum. Caching ve Loging işlemlerini bizim için aspect’lerimiz halledecek. Ne kadarda hoş duruyor değil mi? :)

Şimdi konsol uygulamamız üzerinde TransparentProxy sınıfımız aracılığı ile çağırarak bir bakalım aspect’lerimiz nasıl çalışacaklar?

using System;

namespace AOPGiris
{
    class Program
    {
        static void Main(string[] args)
        {
            // Servis örneğini oluşturuyoruz.
            var productService = TransparentProxy<ProductService, IProductService>.GenerateProxy();

            // Servis üzerinden GetProduct metotunu çağırıyoruz.
            var product = productService.GetProduct(1);

            Console.WriteLine("Id: {0}, Name: {1}, Price: {2}", product.Id, product.Name, product.Price);
            Console.ReadLine();
        }
    }
}

ve sonuç…

aspect_result

Gördüğümüz gibi GetProduct metot’u için eklemiş olduğumuz Cache ve Log aspect’leri çalışmış bulunmaktadır. Özünde yapmış olduğumuz işlemde GetProduct için business kodlarının karmaşıklaşmasını kesişen ilgilerimizi ayırarak sağlamış bulunmaktayız. Kod satırları artık okunabilirliği yüksek bir seviyede ve kesişen ilgilerimiz tekrar kullanılabilir haldedir.

Aktaracaklarım şimdilik bu kadar. Bu başlık altında metotlar invoke olurken mutfakta neler olup bitiyor, Interceptor’ler bu işlemi nasıl işliyor gibi sorularıda görmüş olduk aslında. AOP serisinin ilerleyen makalelerinde ise 3rd party kütüphaneler aracılığı bu işlemleri nasıl gerçekleştirebiliriz konularınada değineceğim. Çünkü her zaman kendimiz custom olarak yazabilme ve işleyebilme olanaklarına sahip olamayabiliriz. Bunun en büyük sebeplerinden birisi ise: standardizasyon. Eğer büyük bir ekipte çalışıyorsanız bazı konuların önemli olduğu gibi en büyük unsurlardan biriside standardizasyondur.

Bunun için gelecek makalelerimde bu kütüphaneler nasıl kullanılıyor konularınada değiniyor olacağım. Örnek projeyi ekte bulabilirsiniz.

AOPGiris