Aspect Oriented Programlamaya Giriş
Solid prensiplerinin ilki olan single responsibilty’e göre yazılan her fonksiyon bir tek bir amaca yönelik yazılmalı ve bu amacı gerçekleştirmelidir. En basit örnekle toplama fonksiyonunun fonksiyonelliği o toplama işlemini yapmasıdır veya mail gönderen bir fonksiyonun fonksiyonelliği maili göndermektir. Ancak bir fonksiyon içerisinde iki tip ihtiyaç bulunur. Bunlar fonksiyonel ihtiyaçlar ve fonksiyonel olmayan ihtiyaçlardır. Fonksiyonel ihtiyaçlar o metodun gerçekten yapması gereken işi yapan ihtiyaçlardır. Örneğin SendMail fonksiyonunun mail göndermesi o fonksiyonun yapması beklenen iş ve bunun için yazılmış kodlar fonksiyonel ihtiyaçlardır. Fonksiyonel olmayan ihtiyaçlar ise metodun yaptığı işle direkt olarak alakalı olmayan olan kod satırlarıdır. Fonksiyonel olmayan kodlara örnek olarak logging, caching ve transaction managementi gösterebiliriz. Peki Aspect Oriented programlama(AOP) nedir? Aslında AOP’un bize söylediği şey, fonksiyonel olmayan ihtiyaçların aspect attributelar içerisine alınıp fonksiyonun içerisinde sadece fonksiyonel olan kodları bırakmak bu sadece daha temiz ve okunaklı fonksiyonlar elde etmektir.
1.CROSS-CUTTING CONCERNS
AOP’nın ortaya çıkma sebeplerinden en önemlisi cross-cutting concern’lerdir. Cross-cutting corcernler , bir sistemin birden çok bölümünde kullanılan işlevsellik parçalarıdır. Cross-cutting corcernler AOP olmadan kodlandığı zaman fonksiyonel ihtiyaçlar ve fonksiyonel olmayan ihtiyaçlar birbirine karışır ve core logic ile cross-cutting concernler bir arada bulunur. Bu da kodu okunması daha zor bir hale sokar.
AOP’UN SAĞLADIĞI FAYDALAR
- Aspect Oriented Programlanın sağladığı faydaların başında temiz, okunabilir ve bakımı kolay kod yazmak gelir.
- Core logic ve Cross cutting concernler birbirinden ayrıldığı için Aop’a göre yazılmış kodların refactorü daha hızlı ve kolay olur.
- Cross cutting concernler konularına göre birer jenerik aspectte toplanacağından Aop ile yazılan kodlarda kod tekrarı azaltılmış olur.
2.ASPECT TİPLERİ
Aspect oriented programlama için kullanılan bir çok tool vardır. Ancak en çok kullanılanları Postsharp ve Castle dynamicProxy’dir. Yazının ilerleyen kısımlarında kullanılacak aspect metotları için ben postSharpı tercih ettim. Kodu implemente edebilmek için nuget package managerden postSharpın indirilmesi gerekir.
Sıklıkla kullanılan 2 tip aspect vardır. Bunlar boundary aspect ve interceptor aspecttir.
2.1 Boundary Aspect
Boundary aspectler verildiği metotlar çalışmadan hemen önce ve çalışması bittikten hemen sonra tetiklenen metotlara sahip olan aspectlerdir. En çok kullanılan 4 metodu
OnEnty : Aspectin verilen metoda girildiği anda çalışan aspect metodudur.
OnSucces : Aspectin verildiği metot başarı ile sonlandığı anda çalışan aspect metodudur.
OnExit : Aspectin verildiği metot başarı ile de başarısızlık ile de sonlansa sonlandırıldığı anda çalışacak metottur. Try-catch konseptindeki finally gibi düşünülebilir.
OnException : Aspectin verildiği metot exception ile karşılaşılırsa çalışacak metottur. Try-catch konseptindeki catch gibi düşünülebilir.
İlk olarak basit bir loglama aspecti yazarak başlayalım. Öncelikle aspect kullanmadığımız senaryoyu daha sonrasında ise aspect ile yazılmış senaryoyu kodlayalım. Program küçük bir konsol uygulaması olup log atmak yerine ilgili log’u konsola yazdırsın.
MyClass isimli classın tek fonksiyonel ihtiyacı ekrana “Hello World!” yazdırması olsun. Senaryo gereği bu fonksiyonun core logici ekrana “Hello World!” yazdırmasıyken bunun dışındaki kodlar ise sadece loglama için olup ana fonksiyonelliği oluşturmaz. Şuan bu basit örnekte fonksiyonel olmayan ihtiyaçların kodun okunurluğunu azalttığı belli olmayabilir. Ancak gerçek senaryolarda işler daha da karmaşıklaşır.
class Program { static void Main(string[] args) { var myObject = new MyClass(); myObject.MyMethod(); Console.ReadLine(); } } class MyClass { public void MyMethod() { Console.WriteLine("Before the method"); Console.WriteLine("Hello world!"); Console.WriteLine("After the method"); } }
Şimdi fonksiyonel olmayan ihtiyaçları yani loglama işlemlerini aspecte alıp , MyMethod isimli fonksiyonumuzda sadece fonksiyonel ihtiyaçları tutalım. Bunun için MyAspect isimli yeni bir class oluşturarak işe başlayabiliriz. MyAspect classının bir attribute özelliği taşıyabilmesi için OnMethodBoundaryAspect classının kalıtımını alıp [Serilaziable] olarak tanımlanması gerekir. (OnMethodBoundaryAspect ‘ın detayları yazının ilerleyen bölümlerinde verilecektir.)
Daha sonrasında classımızı attribute olarak eklediğimiz methot başlamadan hemen önce çalışacak OnEntry adındaki metodumuzu ve metot çalıştıktan hemen sonra çalışacak OnExit metodumuzu override ederek devam edelim.
[Serializable] public class MyAspect : OnMethodBoundaryAspect { public override void OnEntry(MethodExecutionArgs args) { Console.WriteLine("Log:Before the method"); } public override void OnExit(MethodExecutionArgs args) { Console.WriteLine("Log:After the method"); } }
Aspect classımızı yazdığımıza göre , artık bu classı loglamak istediğimiz metoda attribute olarak verebiliriz.
class MyClass { [MyAspect] public void MyMethod() { Console.WriteLine("Hello world!"); } }
Kodun son hali aşağıdaki gibi olacaktır.
class Program { static void Main(string[] args) { var myObject = new MyClass(); myObject.MyMethod(); Console.ReadLine(); } } class MyClass { [MyAspect] public void MyMethod() { Console.WriteLine("Hello world!"); } } [Serializable] public class MyAspect : OnMethodBoundaryAspect { public override void OnEntry(MethodExecutionArgs args) { Console.WriteLine("Log:Before the method"); } public override void OnExit(MethodExecutionArgs args) { Console.WriteLine("Log:After the method"); } }
Program çıktısı…
2.2 Interceptor Aspect
Aşağıdaki örnekte de kullandığımız inceptor aspect, metot çağırıldığı an araya girip metodu kendi üzerinden args.Proceed(); ile çağıran aspecttir. Yani bu aspect sayesinde metot ile metodun çağırıldığı yere bir middleman aracı koyduğunuzu düşünebilirsiniz. Interceptor aspect daha çok metodun fonksiyonelliğini sarmallayan yapıların metot dışına alınmasında kullanılır.
class Program { static void Main(string[] args) { var MyObject = new MyClass(); MyObject.MyMethod(); Console.ReadLine(); } } [Serializable] public class MyInceptorAspect : MethodInterceptionAspect { public override void OnInvoke(MethodInterceptionArgs args) { Console.WriteLine("interceptor1"); args.Proceed(); Console.WriteLine("interceptor2"); } } public class MyClass { [MyInceptorAspect] public void MyMethod() { Console.WriteLine("Hello Word"); } }
3.ÖRNEK SENARYO
Aspect oriented programlamanın temel mantığını ve basit bir aspect yazımını anlattıktan sonra işi bir adım ileriye götürelim ve ufak bir senaryo üzerinden gidelim. Senaryomuzda bir araba kiralama şirketi için program yazdığımızı düşünelim.Bir kitaptan aldığım bu uzun senaryonun sadece bir fonksiyonelliği üzerinden devam edicem. Senaryo gereği araba kiralayan kişi kiraladığı normal bir araba için 1 lüks araba için 2 puan kazansın. Senaryoyu uzun tutmamak için senaryonun sadece bu fonksiyonelliği üzerinden ilerleyelim
Senaryonun konusundan ziyade cross-cutting concerns ve aspect oriented programlama mantığına değinmeye çalışacağım.
Senaryoda kullanacağımız 3 adet modelimiz olsun.
- Müşterileri temsilen Customer.cs
- Kiralama sözleşmesini temsilen RentalAgremeent.cs
- Aracı temsilen Vehicle.cs
Kullanıcının kiraladığı her araç başına puan topladığı metodu yazalım. Metot basit olarak kullanıcının bir aracı kaç gün kiraladığını, aracın sıradan araç mı lüks araç mı olduğuna göre puanını ve daha sonra da gün ile puanı çarparak kullanıcının puanını hesaplıyor.
public class LoyaltyAccuralService : ILoyaltyAccuralService { public void Accure(RentalAgreement agreement) { var rentalTimeSpan = (agreement.EndDate.Subtract(agreement.StartDate)); var numberOfDays = (int)Math.Floor(rentalTimeSpan.TotalDays); var pointsPerDay = 1; if (agreement.Vehicle.Size >= Size.Luxury) pointsPerDay = 2; var points = numberOfDays * pointsPerDay; Console.WriteLine("Adding {0} point for Customer {1}", points, agreement.Customer.Id); } }
Buraya kadar her şey tamam şimdi uygulamayı test edelim.
class Program { static void Main(string[] args) { TestMethod(); Console.ReadLine(); } static void TestMethod() { var service = new LoyaltyAccuralService(); var rentalAgreement = new RentalAgreement { Customer = new Customer { Id = Guid.NewGuid(), Name = "Matthew d groves", DateOfBirth = new DateTime(1980, 2, 10), DriverLicence = "RR112345" }, Vehicle = new Vehicle { Id = Guid.NewGuid(), Make = "Honda", Model = "Accord", Size = Size.Compact, Vin = "1HABC123" }, StartDate = DateTime.Now.AddDays(-3), EndDate = DateTime.Now }; service.Accure(rentalAgreement); } }
Kodumuz tam da istediğimiz gibi çalışıyor. Ama burada eksik olan bir şeyler var. Kodumuzu izleyebileceğimiz loglamalar(logging), invalid caselere karşı korumalar(defensive programming), retries caseleri vs. Bu eksiklikleri de aşağıdaki gibi temsili bir şekilde kodumuza ekleyelim.
Fark ettiğiniz üzere kod kalabalığı gitgide arttı ve okunması zor bir hale geldi. Bu fonksiyon içerisinde gerekli hesaplamayı yapan kısım yani fonksiyonel ihtiyaçlar, fonksiyonel olmayan ihtiyaçlar arasına gömülüp okunması dolayısıyla da hata gidermesi ve geliştirme yapılması zor bir hale geldi. Aspect Oriented Programlama tam da burada devreye giriyor. Yukarıda da bahsettiğim gibi AOP’un bize söylediği şey fonksiyonel olmayan gereksinimleri fonksiyonel gereksinimlerden ayırarak daha temiz, okunur ve bakımı kolay kod elde etmek.
O zaman başlayalım…
ilk olarak fonksiyondan loglama kısmını çıkarıp Logging aspecte atalım. Metoda verilen argumana göre loglama yapması için de aşağıdaki gibi metodun argumanı üzerinden GetType metoduyla ilerleyebiliriz. Burada elimizde sadece 3 tip olduğundan aşağıdaki gibi bir yapı uygulanabilir. Ancak tip sayısı arttığında ortak bir interfaceden kalıtıp logları ona göre attırmak daha faydalı olacaktır.
[Serializable] public class LoggingAspect : OnMethodBoundaryAspect { //args parametresi ile aspectin konulduğu metotla ilgili tüm bilgilere(metot ismi, parametre isim tipleri vs.) ulaşabiliriz. public override void OnEntry(MethodExecutionArgs args) { Console.WriteLine("{0} started {1}",args.Method.Name,DateTime.Now); foreach (var argument in args.Arguments) { if(argument.GetType() == typeof(RentalAgreement)) { Console.WriteLine("Customer {0}", ((RentalAgreement)argument).Customer.Id); Console.WriteLine("Vehicle {0}", ((RentalAgreement)argument).Customer.Id); } if(argument.GetType()==typeof(Invoice)) Console.WriteLine("Invoice:{0}",((Invoice)argument).Id); } } public override void OnSuccess(MethodExecutionArgs args) { Console.WriteLine("{0} compeled {1}", args.Method.Name, DateTime.Now); } }
Loglamalarımızı koddan ayırdığımıza göre şimdi defensive caselerimizi fonksiyondan ayırabiliriz. Defensive caseler senaryomuzda karşılaşabileceğimiz invalid caseler olup sadece fonksiyona girerken çalışacağı için , bu aspectte OnEntry metodu bizim için yeterli olacaktır. OnEntry üzerinden parametreyi kullanarak , metodun parametrelerine ulaşabilir ve aşağıdaki gibi gerekli kontrollerimizi yapabiliriz.
Aşağıda OnEntry metodumuzun içerisinde metoda gelen parametreleri önce null kontrolünden sonra da int tipinde olanları 0’dan küçük mü kontrolünden geçiriyoruz.
[Serializable] public class DefensiveAspect : OnMethodBoundaryAspect { //args parametresi ile aspectin konulduğu metotla ilgili tüm bilgilere(metot ismi, parametre isim tipleri vs.) ulaşabiliriz. public override void OnEntry(MethodExecutionArgs args) { var parameters = args.Method.GetParameters(); var arguments = args.Arguments; for(int i = 0; i < arguments.Count; i++) { if (arguments[i]==null) { throw new ArgumentNullException(parameters[i].Name); } if(arguments[i].GetType()==typeof(int) && (int)arguments[i]<=0) { throw new ArgumentNullException("",parameters[i].Name); } } } }
ve son olarak da retries kontrollerimizi fonksiyondan çıkartalım. Burada durum biraz farklılaştı. Şuana kadar aspecte attığımız kodlar genel itibariyle fonksiyona girerken ve fonksiyondan çıkarken çalışan kodlardı. Ancak retries kodları fonksiyonun daha içerilerinde biryerde. Burada kullanacağımız aspect , methodboundryaspect yerine methodInterceptionaspect olacak.Yani Interceptor aspect kullanacağız.
Bu aspectin özelliği, ilgili metot çağırıldığında metot direkt olarak değil de bu aspect üzerinden args.Proceed(); ile çağırılır. Yani metot ile metodu çağıran yer arasına bu aspect girer.
[Serializable] public class RetriesAspect : MethodInterceptionAspect { public override void OnInvoke(MethodInterceptionArgs args) { var retries = 3; var succeeded = false; while (!succeeded) { try { //aspectin içerisinden ilgili fonksiyonu çağıran ve fonksiyonun çalışması bitince tekrar kaldığı yerden devam eden kısım. args.Proceed(); succeeded = true; } catch { if (retries >= 0) retries--; else throw; } } } }
ve aspectlerimizi fonksiyona verelim. Görüldüğü üzere fonksiyondan core logicle ilgili olmayan tüm nonfunctional requirementları aspectlere aldık. Kod daha temiz, okunaklı ve maintanance’ı kolay bir hale geldi ayrıca bu aspectler başka fonksiyonlarda da kullanılabilecek yapıda olduklarından kod tekrarından da kurtulduk.
[LoggingAspect] [DefensiveAspect] [RetriesAspect] public class LoyaltyAccuralService : ILoyaltyAccuralService { public void Accure(RentalAgreement agreement) { var rentalTimeSpan = (agreement.EndDate.Subtract(agreement.StartDate)); var numberOfDays = (int) Math.Floor(rentalTimeSpan.TotalDays); var pointsPerDay = 1; if (agreement.Vehicle.Size >= Size.Luxury) pointsPerDay = 2; var points = numberOfDays * pointsPerDay; Console.WriteLine("Adding {0} point for Customer {1}", points, agreement.Customer.Id); } }
sade ve anlaşılır anlatımınız için teşekkürler
Harikaydı. Fazlasıyla açıklayıcı olmuş. Emeğinize sağlık.
Elinize sağlık,gerçekten aydınlatıcı
Elinize sağlık çok açıklayıcı olmuş
Elinize, emeğinize sağlık. Çok sade ve güzel bir anlatım olmuş.