Makale Özeti

Aspect Oriented Programming ile gorevlerin ayriklastirilmasi ve kod gurultusunun azaltilmasi

Makale

Aspect Oriented, bir suredir var olan fakat pek populer olmayan bir teknikti. Microsoft'un MVC frameworku cikaracagini duyurmasi ile daha yakindan taninmaya baslandi(Framework icindeki Controllerlarin bir factory/containerdan geliyor olmasi ve controllerlarin tepesindeki [Cache] [Authentication] turu attributelar ile). Aspect Oriented programlamanin temelinde "seperation of concerns" yani gorevlerin ayriligi prensibi var, boylece daha moduler bir yapi ile uygulamalarimizi gelistirmek mumkun.

Gunluk hayatta yaptigimiz bir cok sey icin, her ne kadar OOP'yi cok iyi kullaniyor olsak da, bazi kodlari tekrar tekrar yazmamiz gerekiyor, mesela caching. Metod bazinda bu islemi yapiyorsak her seferinde if(Cache.IsCached("key")) kontrolu yazmamizin gerektigi ortada. Peki bunda sorun ne? Sorun su: Method sadece kendi isini yapmali, caching o metodun icinde yer almamali. Diger bir ornek loglama veya transaction olabilir, dedigim gibi metod isini yapmali, loglama metodun isi degil bir aspecti(cephe diyorlar, kulagima hos gelmedi o yuzden aspect diye devam ediyorum) olmali.

public class Foo
{
    protected EventLog eventLog;
   
public Foo()
    {
        eventLog =
new EventLog();
        eventLog.Source = "Foo Application";
    }
   
public void bar()
    {
        eventLog.WriteEntry("Bar method begin");
        eventLog.WriteEntry("Bar method end");
    }
}



.net bize AOP icin simdilik pek sans vermiyor, ya compile time'da post processing ile(PostSharp bunun ornegidir) ya da runtime de proxy'ing ile(inversion of control yapisi burada one cikiyor), ya da .net'in siniflarindan miras alarak bunu gerceklestirebiliyoruz. Compile time guzel bir yontem, performans kaybini en aza indirebiliyor fakat calisma zamanli degisiklikler karsisinda biraz daha kati(ilk izlenimlerim bu yonde). Runtime calisma mekanizmasi, isi biraz yavaslatabiliyor, sonucta arka reflection calisiyor fakat runtime'da aspect ekleyebilior olunmasi esnekligini arttiriyor. Kodumuzu aspect oriented ile yapiyor olsaydik su sekil bir kod yazacaktik, ayrica exception logging'i de tek satir yazmadan halletmis olacaktik.

[Log(Category="Foo")]
public class
Foo : IFoo
{
   
public Foo()
    {
    }
    [Log]
   
public void bar()
    {

    }
}

Loglama otomatik olarak yapilmis olacakti.

Makalenin bu kisimdan sonrasini Runtime mekanizmalar icin anlatacagim. Bunun icin biraz giris yapalim runtime mekanizmalar nasil calisiyor konusuna.

.net icin yazilmis herhangi bir sinifin ornegini "new" aldigimizda ve bu nesnenin herhangi bir methodunu cagirdigimizda scope'unda ne varsa onlari yapacaktir. Yukarida attribute'umuz var dediginizi duyar gibiyim, ama dikkat edilmesi gereken bir nokta var: Attribute'lar pasif metadatadir. Onu isleyen, analiz eden ve ona gore davranisini belirleyen bir yapi olmadigi surece yazdigimiz attributelar isimize yaramaz. [NonSerializable] attribute unu dusunun. O attribute'u Serialization yapan siniflar isler, okur ufler ve onu serialization yaparken umursamadan gecer.

Nesnelerimizin attributelarini islenebilir hale getirebilmek icin bunlari new ile yaratmak yerine bunlari isleyecek sinifin ona gore yaratmasini saglamaliyiz. Boylece dinamik olarak yarattigi sinifa kendisi belirtilen hooklari atacak ve boylece istedigimiz seyi gerceklestirmis olacagiz. Benim favori AOP frameworkum Castle. Kullanimini orneklerle goreceksiniz. Bu kaplar/containerlar interface ile cagirildiginda runtime da reflection ile yaratilan, bizim belirttigimiz seyleri yapmaya hazir halde olan bir ve belirttigimiz interface'i implemente eden Proxy bir sinif dondururken bir sinif ile cagrildiginda "virtual method"lari intercept(kesebilen) edilebilen bir, bizim belirttigimiz siniftan kalitlayan baska bir sinif donduruyor. Olayi basite indirgeyecek olursak, interface ile cozumle dedigimizde cok basitlestirilmis sekliyle bu sekil bir sinif geliyor.

public interface IService
{

    void DoWork();
}
public class Service:IService
{

    #region IService Members
    public void DoWork()
    {
        System.Threading.Thread.Sleep(10000);
    }
    #endregion
}
public class IServiceProxy_8e259c29_4718_492f_849c_8d511c024d73:IService
{
    private IService target;
    public IService_8e259c29_4718_492f_849c_8d511c024d73()
    {
        target = new Service();
        interceptors = new List<IInterceptor>();
    }
    private readonly IList<IInterceptor> interceptorList;

    #region IService Members
    public void DoWork()
    {
        target.DoWork();
    }
    #endregion
}

Gordugunuz gibi yaptigimiz cagrilar once dinamik olarak yaratilan sinifimiza, daha sonra da esas sinifimiza yonlendirilecek sekilde yapiliyor.

Interceptor dedigimiz yapilan yaptigimiz method cagrilari sirasinda cagriyi durdurup arada baska isleri(aspectleri) gerceklestirebilmemizi saglayan yapilar. Runtime'da yaratilan her sinifin icine eklenen interceptorList bizim yaptigimiz cagrilar sirasinda  tek tek dolasilarak belirtilen aspectlerin gerceklestirilmesini sagliyor. Bu interceptorler isterlerse metod calismasini durdurabilirler. Ornek olarak iki uygulama yapacagim. Biri methodlarin calisma surelerini konsola bastiran bir aspect, digeri de methodlarin calismasini cacheleyip her defasinda methodun calistirmayan baska bir aspect olacak. Ikinci kodu arkadasim "Oguz Kurumlu" yazdi. Tesekkur ediyorum Oguz(microp).

public class StopwatchInterceptor:IInterceptor
{
    #region IInterceptor Members
    public void Intercept(IInvocation invocation)
    {
        Console.WriteLine("Method {0} begins",invocation.Method);
        Stopwatch sw=new Stopwatch();
        sw.Start();
        invocation.Proceed();
        sw.Stop();
        Console.WriteLine("Method ended. Time: {0} ms",sw.ElapsedMilliseconds);
    }
    #endregion
}

 

[Interceptor(typeof(StopwatchInterceptor))]
public class Service:IService
{
    #region IService Members
    public void DoWork()
    {
        Thread.Sleep(10000);
    }
    #endregion
}

 

Bu programi calistirdigimizda(proje dosyasinda suna dikkat edin, containerdan gelen ornek ustunde islem yapiyoruz) program 10 saniye suruyor ve ekrana metod baslangicinda ve bitisinde sureyi ekrana yaziyoruz.

 

2. kodumuz ise Oguz Kurumlu tarafindan yazildi, basitce bir method ayni parametrelerle 2 defa cagrildiginda method'un donus degeri cache'den aliniyor.

public class CacheInterceptor : IInterceptor
{
    #region IInterceptor Members
    public void Intercept(IInvocation invocation)
    {
        object returnValue = CacheCollector.Instance.GetCachesForMethod(invocation.Method, invocation.Arguments);
        if (returnValue != null)
        {
            invocation.ReturnValue = returnValue;
            return;
        }
        try
        {
            invocation.Proceed();
            KeyValuePair<object[], object> keyValue = new KeyValuePair<object[],object>(invocation.Arguments, invocation.ReturnValue);
            CacheCollector.Instance.Add(invocation.Method, keyValue);
        }
        catch
        {
        }
    }
    #endregion

}

ve Oguz IMath interface'i ve MyMath sinifi ustunden gerceklestirdi. Yazdigi kodu biraz degistirdim ve iki ornegi birlestirerek reusability nasil artti onu gostermek istedim

Ornekte bilincli olarak 2000 ms bekletiyoruz, boylece ayni parametrelerle ikinci defa cagirdigimizda sonucun cabuk gelmesini gormus olacagiz.

Ornek uygulamamiz ise su sekilde:

Urettigi cikti ise su sekilde

Gordugunuz gibi math.Sum(2,3) ile math.Sum(3,2) 2000 ms kadar islem yapti, daha sonra ayni parametrelerle yapilan cagrilar daha once hesaplanmis oldugu icin tekrar hesaplanmadi.