Makale Özeti

Policy Injection yapmak için sadece lazım olan System.Remoting isim uzayı ve 150 satrlık bir kod bloğudur. Policy Injection inceliyoruz: neden ihtiyaç duyulur, nasıl yapılır, faydaları nedir?

Makale

Programlamada kodlar uzadıkça kodların anlaşılabilirliği düşmektedir ve çoğu zamanda içinden çıkılmaz bir hal almaktadır. Tek bir "basit" kayıt işlemini yapan kod bloğu bile tüm uygulamada geçerli olan hata yakalama, loglama, yetki kontrolü gibi çapraz modüllere ait kodlardan dolayı "karmaşıktır". Örneğim aşağıda bu "basit" kayıt işlemine bir örnek verilmiştir.

 public  void  SaveOrderToDB (Order  order, User  user)
 {
     LogManager .BeginPerformanceCounter ("SaveOrderToDB" );
     if  (order == null  || user == null )
         throw  new  NullReferenceException ();
     if  (!user.IsInRole ("Sales" ))
         throw  new  SecurityException ();
     var  totalAmount = order.Unit *order.UnitAmount ;
     var  balance = GetBalanceFormAccount (order.AccountId );
     if  (totalAmount > balance)
         throw  new  ApplicationException ();
     try 
     {
         var  dbContext = new  DbContext ();
         dbContext.Orders .Add (order);
         dbContext.SaveChanges ();
     }
     catch  (Exception  exception)
     {
         LogManager .EndPerformanceCounter ("SaveOrderToDB" );
         LogManager .SaveException (exception);
         throw ;
     }
     LogManager .EndPerformanceCounter ("SaveOrderToDB" );
 }

Burada görüldüğü üzere müşterinin hesabından yapılan basit bir ödeme işleminde bile kodu karmaşık hale getiren bir çok çapraz modül söz konusudur. Öncelikle fonksiyona girerken loglama modlü çalıştırılıyor sonra parametrelerin doğrulanması daha sonra işlemi yapan kullanıcının yetki doğrulaması yapılıyor. Daha sonra hata kontrolleri ve en son fonksiyondan cıkmadan önce tekrar loglama yapılıyor. Aslında yapılmak istenen sadece veri tabanına doğru bir kayıt eklemektir. Buda 26 satırlık fonksyonumuzda ki 9 satırda ya yapılmaktadır. Poliçe enjekte veya Cephe Yönelimli (Asspect Oriented) programlama yaklaşımı bu çapraz modülleri mümkün olduğunca bir noktadan yönetilmesini ve fonksiyonun içinde ki kod bloğundan uzaklaştırılmasını amaçlamaktadır. Eğer bu fonksiyonu Policy Injection kullanarak yazmış olsaydık aşağıdaki şekilde görülecekti:

 [PerformanceCounterHandler ]
 [ExceptionHandler ]
 [CheckUserIsInRoleHandler ("Sales" )]
 [CheckArgumentsHandler ]
 public  void  SaveOrderToDB (Order  order, User  user)
 {
     var  totalAmount = order.Unit  * order.UnitAmount ;
     var  balance = GetBalanceFormAccount (order.AccountId );
     if  (totalAmount > balance)
         throw  new  ApplicationException ();
     var  dbContext = new  DbContext ();
     dbContext.Orders .Add (order);
     dbContext.SaveChanges ();            
 }

Görüldüğü üzere kodlama daha temiz ve sade oldu. Bu fonksiyonda tüm gerekli çapraz modüller fonksiyona bir poliçe olarak eklenmektedir. Fonksiyona yapılan çağrıda resimde görüldüğü şekilde bir sorumluluk zinciri takip edilmektedir. Önce Performans counter modülü sonra sırası ile diğer modüller çalışacak en son metot çalışacaktır. Daha sonra sonuç aynı yolu izleyerek geri döndürülecektir. Bu yapıya sorumluluk zinciri denmektedir. Yukarıda ki örneğimizde sorumluluk zinciri şu şekilde çalışacaktır: Performans Counter -> Exception Handler -> Sheck User Is In Role -> CheckArguments -> Metot -> Check Arguments -> Check User Is In Role -> Exception Handler -> Performans Counter. Bu sıralama sizi korkutmasın performans olarak kaybınız mili saniyelerin altındadır. Kod sadeliği ve yönetim kolaylığı olarak kazançınız çok daha fazla olacaktır.

cor1

Peki projemizde ki çapraz modülleri fonksiyonlarımıza birer poliçe olarak nasıl enjekte edebiliriz ?

Her şeyden önce poliçe enjekte edebilmek için nesnenin her hangi bir metotu çağrıldığında tetiklenecek bir yönetim fonksiyonuna ihtiyaçımız var. Yani sınıfa ait property'ler değiştiği zaman fırlatılan PropertyOnChanged olayına benzer, fonksiyon çağrılarında tetiklenecek bir mekanizmaya ihtiyacımız var. İhtiyaçımıza proxy sınıfı cevap vermektedi. Proxy sınıfı içlerinde gerçek nesneyi barındıran gerçek nesneye yapılan her çağrıyı izleyen sınıflardır. Proxy içende barındırdığı nesneye bir çağrı geldiğinde Invoke fonksiyonu tetiklenmekte ve gerçek nesneye ait çağrı bu fonksiyon içinde yapılmaktadır. Proxy'ler yardımı ile nesneye gelen metot çağrılarını tek bir yerden kontrol edebiliriz. Fakat nesneye bir proxy oluşturabilmek için nesnelerimizi MarshalByRefObject sınıfından türetmeliyiz. Böylece proxy metot çağrılarını takip edebilecektir.

 public  class  PITest2 : MarshalByRefObject 
 {
     [PerformanceCounterHandler ]
     [ExceptionHandler ]
     [CheckUserInRoleHandler ("Sales" )]
     [CheckArgumentsHandler ]
     public  void  SaveOrderToDB (Order  order, User  user)
     {

Nesnemizi proxy oluşturabilecek duruma getirdik.

Sonraki problemimiz çapraz modülleri merkezi bir noktaya çekmektir. Çapraz modüllerin her birini bir poliçe haline getirebiliriz. Policeden kasıt çalıştığı an ki ortam bilgilerini toplayan ve tek bir atomik iş yapan uygulama bloğu sınıflarıdır. Örneğim hata yakalama işlemi için uygulamada bulunan tüm fonksiyonlarda çalışacak bir ExceptionHandlingManager yazabilirsiniz. Bu durumda her bir fonksiyonun içinde ExceptionHandlingManager sınıfınıza en az bir defa çağrıda bulunmanız gerkecektir. Yada hata kayalama işlemini yapan tek bir police yapar ve hata yakalama gereksimi olan fonksiyonların bu police ile çalışmasını sağlayabilirsiniz. Böylece hata yakalama sisteminizde bir değişiklik yapmak için sadece poliçenizi değiştirmeniz yeterli olacaktır. Ayrıca fonksiyonların içinde ki gereksiz kod bloklarından kurtulmuş olacaksınız.

 public  class  ExceptionHandlingCallHandler  : CallHandler 
 {
 public  override  ReturnMessage  Invoke (IMethodCallMessage  message, object  realObject)
 {
     var  result = NextHandler .Invoke (message, realObject);
     if  (result.Exception  != null )
     {
         var  errorMessage = string .Format ("{0}{1}{2}" , DateTime .Now , result.Exception .Message ,
                                             result.Exception .StackTrace );
         Console .WriteLine (errorMessage, ConsoleColor.Red, true );
         Trace .TraceError (errorMessage);
     }
     return  result;
 }
 }

Yukarıda örnek bir hata yakalama policesini görmektesiniz. Police sorumluluk zincirinde yer alan kendsinden sonra ki policeleri çağırmakta eğer bu zincir üzerinde bir hata oluşursa bunu ekrana basmaktadır. Şimdi nesnemizin nasıl kullanıldığına bakalım:

 PITest2  instance = PolicyInjector <PITest2 >.CreateObject ();
 var  order = new  Order  {AccountId  = 10, Unit  = 3, UnitAmount  = 5};
 var  user = new  User  {UserName  = "saleperson1" , Roles  = new [] {"Sales" }};
 instance.SaveOrderToDB (order,user);

Kullanımda ki tek fark nesneyi doğrudan oluşturmak yerine PolicyInjector Factory sınıfı ile oluşturmamızdır.

Şimdi PolicyInjector sınıfını inceleyelim. Nesnelere poliçeleri enjekte edebilmek için nesnelerin tek bir merkezden üretilmesi gerekmektedir. PolicyInjector bizim yerimize sınıflarımızın proxy'lerini oluşturan, metot çağrılarında ki sorumluluk zincirini oluşturan ve metot çağrısı sırasında poliçeleri enjecte eden Factory sınıfımızdır. Yaptığı iş çok sihirlide olsa kendisi oldukça basit ve 150 satırlık bir sınıfdır.

 public  class  PolicyInjector <TObject > : RealProxy  where  TObject  : new ()
 {
     private  readonly  TObject  _realObject ;
     public  PolicyInjector (TObject  realObject)
         : base (typeof (TObject ))
     {
         _realObject  = realObject;
     }
     public  static  TObject  CreateObject ()
     {
         var  reelInstance = new  TObject ();
         return  Wrap (reelInstance);
     }
     public  static  TObject  Wrap (TObject  reelInstance)
     {
         var  proxyObject = new  PolicyInjector <TObject >(reelInstance);
         var  transparentProxyObject = (TObject )proxyObject.GetTransparentProxy ();
         return  transparentProxyObject;
     }
     public  override  IMessage  Invoke (IMessage  msg)
     {
         var  message = (IMethodCallMessage )msg;
         var  chain = CreateChainOfResponsibility (message);
         var  result = chain.Invoke (message, _realObject );
         return  result;
     }
     private  CallHandler  CreateChainOfResponsibility (IMethodMessage  message)
     {
         var  chain = new  List <PolicyPear >();
         AddToChainFromInterfaceHandlers (message, chain);
         AddToChainFromClassHandlers (message, chain);
         // add method call 
         chain.Add (new  PolicyPear  { CallHandler  = new  MethodCallHandler () });
         // organize chain 
         for  (var  i  = 1; i < chain.Count; i++)
         {
             chain[i-1].CallHandler.SetChain(next:chain[ i ].CallHandler, 
                 handlerAttribute: chain[i - 1].HandlerAttribute );
         }
         return  chain[0].CallHandler ;
     }
     private  void  AddToChainFromInterfaceHandlers (IMethodMessage  message, ICollection <PolicyPear > chain)
     {
         var  realType = _realObject .GetType ();
         foreach  (var attributes  in 
             realType.GetInterfaces ()
             .SelectMany (@interface => (
                 from  method in  realType.GetInterfaceMap (@interface).InterfaceMethods 
                 where  method.Name  == message.MethodBase .Name 
                 select  method.GetCustomAttributes (typeof (CallHandlerAttribute ), true ))))
         {
              attributes.Cast <CallHandlerAttribute >()
                 .OrderBy (c => c.CallHandlerType .MetadataToken ).ToList ()
                 .ForEach (attribute =>
                     chain.Add (new  PolicyPear 
                     {
                         CallHandler=Activator.CreateInstance(attribute.CallHandlerType) as CallHandler,
                         HandlerAttribute  = attribute
                     })
                 );
         }
     }
     private  void  AddToChainFromClassHandlers (IMethodMessage  message, ICollection <PolicyPear > chain)
     {
         var  realType = _realObject .GetType ();
         var  attributes =
             realType.GetMethod (message.MethodName ).GetCustomAttributes (typeof (CallHandlerAttribute ), true );
         if  ((attributes == null ) || (attributes.Length  <= 0)) return ;
         bool  needReserve = chain.Count  == 0;
         var  list = attributes.Cast <CallHandlerAttribute >()
             .ToList ();
         if  (needReserve) list.Reverse ();
         list.ForEach (attribute => chain.Add (new  PolicyPear 
         {
             CallHandler  = Activator .CreateInstance (attribute.CallHandlerType ) as  CallHandler ,
             HandlerAttribute  = attribute
         }));
     }
     private  sealed  class  MethodCallHandler  : CallHandler 
     {
         public  override  ReturnMessage  Invoke (IMethodCallMessage  message, object  realObject)
         {
             var  methodRetval = message.MethodBase .Invoke (realObject, message.InArgs );
             return  new  ReturnMessage (methodRetval, null , 0, message.LogicalCallContext , message);
         }
     }
     private  sealed  class  PolicyPear 
     {
         public  CallHandler  CallHandler  { get ; set ; }
         public  CallHandlerAttribute  HandlerAttribute  { get ; set ; }
     }
 }
 public  abstract  class  CallHandler 
 {
     internal  void  SetChain (CallHandler  next, CallHandlerAttribute  handlerAttribute)
     {
         NextHandler  = next;
         CallHandlerAttribute  = handlerAttribute;
     }
     public  CallHandler  NextHandler  { get ; private  set ; }
     protected  CallHandlerAttribute  CallHandlerAttribute  { get ; private  set ; }
     public  abstract  ReturnMessage  Invoke (IMethodCallMessage  message, object  realObject);
 }
 [AttributeUsage (AttributeTargets.Method  )]
 public  class  CallHandlerAttribute  : Attribute 
 {
     public  CallHandlerAttribute (Type  callHandlerType)
     {
         if  (callHandlerType !=  null  && !typeof (CallHandler ).IsAssignableFrom (callHandlerType))
             throw  new  TypeLoadException (string .Format ("{0} is not assingable from {1}" , callHandlerType.Name ,
                                                         typeof (CallHandler ).Name ));
         CallHandlerType  = callHandlerType;
     }
     public  Type  CallHandlerType  { get ; private  set ; }
 }

Tüm bu sihirli işleri yapan kodların hepsi bu kadar!

PolicyInjector yeni bir nesne oluştururken nesnenin proxy örneğini geri döndermektedir. Proxy örneği üzerinde yapılan tüm metot çağrıları PolicyInjector sınıfımızın Invoke metoduna düşmektedir. Invoke metotu içinde önce metotun poliçelerine göre bir sorumluluk zinciri oluşturulmaktadır. Sorumluluk zinciri oluşturulurken önce interface üzerinde ki metot tamına ait poliçeler zincire eklenmektedir. Daha sonra metota ait poliçelerde zincire eklenmektedir. Zincir oluşturulurken her bir poliçeye bir sonra ki poliçe atanmaktadır. Zincir oluşturulduktan sonra zincirin ilk elemanı çağrılmaktadır. Zincir üzerinde ki her bir police kendi işlevini yapmakta ve bir sonraki policeyi çağırmaktadır. Sorumluluk zincir üzerinde ilerlemekte ve en son metodu çağırmaktadır. Metot çalıştıktan sonra sorumluluk zincir üzerinde sondan başa doğru hareket etmekte ve PolicyInjector Invoke metotu içinde ki result değişkenine sonuç atanmaktadır. Invoke fonksiyonu da bu sonucu metodu çağıran koda dönmektedir.

Böylelikle 150 satırda projenizde ki karmaşayı ortadan kaldıraçak bir PolicyInjector sınıfına ulaşmış olduk. Policy Injection metotu ile çapraz modülleri policeler haline getirip çok ciddi bir kod temizliği ve yönetim kolaylığı sağlayabilirsiniz.

Yardımlarından dolayı Engin Özer Beye teşekkürler...

Örnek proje