C# ve Statik Arayüz (Static Interface)
Merhaba,
Başlığı gören deneyimli programcılar durumu garipseyecektir. Çünkü bilirler ki bir arayüz’ün (interface) kullanım amacı statik olmayan (non-static) bir nesne’nin (object) sağlaması zorunlu/gerekli üye fonksiyon ve özelliklerinin tanımlanması ve ilgili sınıf’ın (class) türünden ve hiyerarşisinden bağımsız kullanılabilmesini amaçlar. hiyerarşiden bağımsız olması abstract class (özet sınıf)’dan ayrılmasını sağlar.
Arayüzler bu amacı güttüğü için bir instance’a sahip nesneye bağımlıdırlar. Ve statik sınıflar herhangi bir instance’a sahip olmadıklarından arayüzlerin üzerlerinde gerçeklenmesi mantıksızdır çünkü statik sınıfların implement ettiği (gerçeklediği ) statik üyelerin hangi sınıfa ait türden olduğu bilgisi çalışma zamanında (Runtime) yoktur. Çalışma zamanında bize sunulan tek somut veri türün bilgisini tutan Type sınıfıdır. Bu bilgiyi bir köşede tutalım birazdan yapacağımız deneysel birazcık da fantastik sayılabilecek çalışmamızda bu bilgi bize yardım edecek. Ancak bizim yapmayı düşündüğümüz şey bir arayüz’ün statik olarak gerçeklenip türden bağımsız ortak bir tasarım arayüzünden erişebilmek.
Peki Gerekli Mi?
Gerekli olup olmadığını bir örnek senaryo üstünden düşünelim. Diyelim ki ortak bir arayüz metodları sağlamak ve statik olmak zorunda olan bir grup sınıfı handle eden bir sistem yazmanız gerekiyor. Bu sınıfların her işlemde sıfırdan bir örnek (instance) yaratmasını istemiyorsunuz. Özellikle bu sistem birden çok türde olabilen ancak ortak bir arayüz sağlayan ve bunları kullanarak yöneten (manage) bir sistem. Bize her tür için ayrı ayrı uğraştırmayacak ortak bir arayüz lazım. Birçok kişinin aklına çeşitli çözümler gelebilir örneğin. Örneğin kimisi sınıfı statik olarak işaretlemeyip ancak statik gibi davranan bir sınıfı interface ile implement edip bir singleton instance aracılığı ile tek bir örnek üzerinden sunmak olabilir. Ancak belirttiğim gibi hem sınıfımız gerçek anlamda statik olmaktan çıkacak, hem bir şekilde sınıfın bir adet de olsa örneği olmuş olacak hem de bize bir ortak yönetim imkanı sağlamayacaktır. Çünkü singleton design pattern’de (tasarım örüntüsü) örneği alabileceğimiz bir statik property’e ihtiyacımız olacak ve bunu arayüzler aracılığı ile alma şansımız yok.
İşte böyle bir senaryoda hem ortak arayüz sunan hem de bunu statik olarak destekleyen bir yapıya ihtiyaç olabiliyor.
Nasıl?
İhtiyacı belirlediğimize göre bunu yapmanın bir olasılığı olup olmadığı üzerine konuşulabilir. İşte burada bize runtime’da tip bilgisi sağlayabilen diller istediğimizi yapabilme konusunda oldukça yardımcı oluyor. Runtime’da türlere ait tek bilgimizin Type sınıfı olduğunu yukarıda söylemiştim. Bu işte bu sınıfı yoğun olarak kullanacağız ancak kısaca Type sınıfı nedir, neleri sağlar kısaca üzerinde durayım. Type sınıfı .NET’de primitif türler de dahil olmak üzere (Not: Primitif türler de asesen birer sınıftır ve alias edilmişlerdir) tüm nesnelerin tür bilgilerini, buna türle ilgili aklınıza gelebilecek her türlü bilginin (class, struct, enum olup olmadığı, sağladığı metodlar, propertyler, fieldlar (üye değişken), metodların access modifier bilgileri vs vs. liste oldukça uzun) tutulduğu bir yapıdır. Bu sayede çalışma zamanı hangi nesnenin ne olduğu konusunda detaylı bilgi sahibi olunabiliyor. Ancak bu bilgi tek başına birşey ifade etmeyecektir. Bu bilgiyi kullanabilecek bir yapının da var olması gerekir ve bu yapı da zaten mevcuttur. Genel anlamda Reflection sözcüğü ile tanımlanan bu geniş yapı .NET’in önemli bir parçasıdır. Reflection, CLR (Common Language Runtime) tarafından desteklenen ve assemblyler, türler üzerinde dinamik olarak düzenleme ve oynama yapabileceğiniz bir yapı sunar. Öyle ki çalışma zamanı dahi bir assembly oluşturup içerisine türler, metodlar tanımlayabilir ve bunları yine runtime’da çalıştırabilirsiniz. Görüldüğü gibi oldukça esnek bir yapıdır. Ancak bu işlemler oldukça maliyetli olduklarından her durumda kullanılmaları tavsiye edilmez. Eğer bir işi dilin ve framework’ün sağladığı native (yerel) çözümlerle yapabiliyorsanız önce o şekilde yapıp, yapamıyorsanız son çare olarak başvurmak daha doğru olur. Ancak şurası da bir gerçek ki bugün bir çok framework bu reflection özelliklerinden yoğun olarak faydalanıyorlar. Buna en güçlü örnek ORM (Object relational mapping) frameworkleri. Birçok MVC framework’ü de bundan faydalanabiliyor.
Peki reflection’ın bize ne gibi bir faydası dokunabilir?
En başta bahsi geçtiği gibi elimizde Type ve Reflection gibi iki araç var. İlk olarak tür bilgisi elimizde olan bir statik sınıfın üye fonksiyonlarını reflection aracılığı ile çalıştırabiliriz. Ancak iş bu kadar değil. Ortak bir programlama arayüzü sistemi geliştirmemiz gerek.
En başta statik bir sınıfa ait bir metodun çalıştırılması ile başlayalım. Bunun için önceden yazdığım kodları parça parça ilgili konuların altına koyarak devam edeceğim.
public static object Invoke(Type staticClassType, string function, bool isProperty, params object[] args) { MethodInfo staticMethod=null; Type classType = staticClassType; object result; BindingFlags bindFlag = BindingFlags.Static | BindingFlags.Public; if (isProperty) { function = (args.Length>0 ? "set_" : "get_") + function; } staticMethod = classType.GetMethod(function, bindFlag); if (staticMethod == null) throw new Exception(string.Format("{0} is not exists", function)); try { result = staticMethod.Invoke(null, args); } catch (TargetInvocationException e) { throw e.InnerException; } return result; }
Temel olarak bir statik sınıfa ait statik metod’u çağıran Invoke adında bir metodumuz var. 4 adet argümana sahip. İlk olarak çağrılacak statik sınıfın tür bilgisi bu yukarıda bahsettiğimiz Type türünden bir veri. İkinci olarak metod veya property (özellik) adı, üçüncü olarak metodun bir özellik mi yoksa metod mu olduğu ve eğer argüman alan bir fonksiyonsa argüman listesi.
Bu yardımcı fonksiyon öncelikle metodun bir property olup olmadığı bilgisinden ve argüman alıp almadığı bilgisinden yola çıkarak propertynin internal (içsel) getter veya setter metodu için önek vererek hazırlanıyor. Ardından istediğimiz metoda ait MethodInfo bilgisini almak için tür sınıf nesnesine ait GetMethod’u kullanıyoruz. Burada bilinmesi gereken nokta bir BindingFlags değeri alması.
Bu değer bize metoda ilişkin hangi öznitelik bilgilerine sahip bilgilerin getirileceğini belirtir. Bu sayede tüm metod veya metodlar yerine sadece istediğimiz türden metodları almamızı sağlar. Biz hem public access modifiers’a (Erişim Niteleyici) hem de statik olarak implement edilmiş belirttiğimiz isme sahip metodu istediğimizi belirtiyoruz. Ardından metod bilgisini sunan nesneyi Invoke çağrısı ile çağırıyoruz. İlk argüman çağrılacak nesneyi aldığı için statik fonksiyonlar için bu değer null olmalıdır.
Temel olarak işi yapan kısım bu metod ve diğer kısımları bunun üzerine inşa edeceğiz. Harici olarak bu fonksiyona destekleyici 2 fonksiyon daha ekleyelim.
public static E Invoke<T, E>(string function, bool isProperty, params object[] args) { return Invoke<E>(typeof(T), function, isProperty, args); } public static E Invoke<E>(Type staticClassType, string function, bool isProperty, params object[] args) { return (E)Invoke(staticClassType, function, isProperty, args); }
Bu fonksiyonlar bize kolaylık sağlama amaçlı olacak.
Problem #1: Statik sınıfın, statik olarak arayüzü gerçeklediğine (implementation) nasıl güveneceğiz? Bunun için bir kontrol yok mu?
Bu önemi olan bir sorun. Çünkü bize verilen bir türün gerçekten tam olarak arayüzü eksiksik gerçeklemesi gerek ve bizim bunu biliyor olmamız lazım. Bu yüzden bize bunu denetleyen bir yardımcı gerekiyor. Ve bunun için ReflectionHelper adında bir yardımcı sınıf yazarak bu işi halledelim.
static class ReflectionHelper { private static bool _HasMethod(Type classType, string method, bool tStatic, bool tPrivate) { MethodInfo methInfo; BindingFlags bindFlags = BindingFlags.Default; if (tStatic) bindFlags |= BindingFlags.Static; bindFlags |= tPrivate ? BindingFlags.NonPublic : BindingFlags.Public; methInfo = classType.GetMethod(method, bindFlags); if (methInfo == null) return false; return true; } public static bool HasStaticMethod(Type classType, string method, bool isPrivate) { return _HasMethod(classType, method, true, isPrivate); } public static bool ImplementsOfStaticInterface<T>(Type interfaceType, bool throwExceptionOnMismatch) { return ImplementsOfStaticInterface(typeof(T), interfaceType, throwExceptionOnMismatch); } public static bool ImplementsOfStaticInterface(Type classType, Type interfaceType, bool throwExceptionOnMismatch) { Type baseType = interfaceType; MethodInfo[] methods; Type[] ifaces; if (!baseType.IsInterface) throw new Exception(baseType.Name + " is not an interface"); ifaces = classType.GetInterfaces(); foreach (Type iface in ifaces) { if (iface == baseType) throw new Exception(classType.Name + " implemented as non-static object"); } methods = baseType.GetMethods(); foreach (MethodInfo meth in methods) { if (!HasStaticMethod(classType, meth.Name, false)) { if (throwExceptionOnMismatch) throw new Exception(meth.Name + " is missing. The method not implemented as static"); return false; } } methods = null; ifaces = null; return true; } }
Yardımcı sınıfındaki ImplementsOfStaticInterface metodu bahsi geçen probleme çözüm getirecek. Temel olarak yaptığı iş şu. Fonksiyona bir tür ve kontrol edilecek arayüz tür bilgisi ile önce kontrol edilecek türe ait arayüzleri alıp kontrol ettiğimiz arayüz tipi olup olmadığına bakmak. Eğer varsa tür standart şekilde implement edilmiş. Eğer bu kısım tamam ise türe ait tüm metodları alarak statik sınıfta bu metodların hem public hem de statik olarak gerçeklendiğini kontrol ediliyor. Bu aşamadan sınıflar arayüzün tam olarak statik gerçeklendiğini garantilemiş oluyor.
Ek olarak statik metod çağrısı yapan StaticMethodInvoker sınıfımıza biraz daha kısaltmak için iki ek fonksiyon ekleyelim.
public static bool ImplementsOf<T>(Type t) { return ReflectionHelper.ImplementsOfStaticInterface<T>(t, false); } public static bool ImplementsOf(Type classType, Type interfaceType) { return ReflectionHelper.ImplementsOfStaticInterface(classType, interfaceType, false); }
böylece kontrol etmek istediğimiz X statik class’ı için Ix arayüzü kontrolü için
ReflectionHelper.ImplementsOf<X>(typeof(Ix));
ReflectionHelper.ImplementsOf(typeof(X),typeof(Ix));
şeklinde kontrol edilebilir.
Şimdi buraya kadar yaptıklarımızı bir çalıştıralım ne durumda. Bunun için 3 adet statik test sınıfı hazırladım. Öncesinde implementasyonu yapılacak bir de arayüz oluşturalım.
Arayüz;
interface IStaticInterfaceTemplate { void MyMethod(); string MyMethodWithArgs(string sval); double MyProperty { get; } }
Test sınıfları;
static class StaticImplementationAlpha { public static void MyMethod() { _Console.PrintLine("MyMethod called from Alpha class"); } public static string MyMethodWithArgs(string sval) { _Console.PrintLine("MyMethodWithArgs called from Alpha class. Result: " + sval); return sval; } public static double MyProperty { get { return 18.5; } } } static class StaticImplementationBeta { public static void MyMethod() { _Console.PrintLine("MyMethod called from Beta class"); } public static string MyMethodWithArgs(string sval) { _Console.PrintLine("MyMethodWithArgs called from Beta class. Result: " + sval); return sval; } public static double MyProperty { get { return 10.8; } } } static class StaticImplementationThetaMissing { public static void MyMethod() { _Console.PrintLine("MyMethod called from Theta class"); } public static double MyProperty { get { return 12.5; } } }
3 adet Alpha, Beta ve Theta sınıfları mevcut. Bunlardan 2 tanesi tam olarak arayüzü gerçeklerken, 1 tanesi eksik olarak gerçeklemiş. MyMethodWithArgs fonksiyonu eksik. Bir adet test kodu yazalım ve sonucu görelim.
Test kodumuz aşağıda
static IEnumerable<Type> staticClasses; static List<Type> validClasses = new List<Type>(); static IEnumerable<Type> GetStaticClasses() { yield return typeof(StaticImplementationAlpha); yield return typeof(StaticImplementationBeta); yield return typeof(StaticImplementationThetaMissing); } static void ImplementationCheck() { bool valid; bool throwOnFail = true; staticClasses = GetStaticClasses(); Type checkType = typeof(IStaticInterfaceTemplate); Console.WriteLine("Checking implementation for " + checkType.Name); foreach (Type t in staticClasses) { try { valid = StaticClassInvoker.ImplementsOf(t, checkType, throwOnFail); if (valid) validClasses.Add(t); Console.WriteLine(string.Format("{0} -> Fully Implemented as Static: {1}", t.Name, valid.ToString())); } catch (Exception e) { Console.WriteLine("Exception: " + e.Message); } } }
test kodumuz için yardımcı sınıftan çağrılan kısa olması için kullanılan ImplementsOf metod grubuna bir tane de başarısızlık durumunda exception fırlatmasını belirten bir opsiyon ekledim ki başarısızlık durumu net görülebilsin. Kodu çalıştırdığımızda sonucumuz şöyle.
Test kodu çalıştı ve eksik olan sınıf için başarısız olup bir exception üretti. Exception içeriğinden de hangi metodun eksik olduğu bilgisini alabiliyoruz. Test kodu runtime’da bilmeyeceğimiz şekilde 3 adet statik sınıf türünü bir Enumerable listesi içine alarak çalışacak. Yani tam istediğimiz gibi sadece tip bilgisine dayalı çalışacağız. Böylece bilmediğimiz statik sınıfların arayüze uygun olup olmadığını biliyor olacağız. Test kodu çalıştıktan sonra geçerli olanları geçerli sınıfların tutulduğu listeye alıyor. Bu sonraki test kodu aşaması için gerekli. Sonraki test metodların istediğimiz gibi çağrılıp çağrılmadığı. Sonraki test için kodumuz şöyle;
static void InvokeMember(Type t, string member, bool isProp, params object[] args) { StaticClassInvoker.Invoke(t, member, isProp, args); } static void StaticImplementationInvoke() { foreach (Type t in validClasses) { Console.WriteLine(); Console.WriteLine(); Console.WriteLine("Invoking static members of " + t.Name); InvokeMember(t, "MyMethod", false); InvokeMember(t, "MyMethodWithArgs", false, "Test"); InvokeMember(t, "MyProperty", true); } Console.WriteLine(); }
InvokeMember kodu kısaca yardımcı olması için yazıldı. Esas olarak iş
StaticClassInvoker.Invoke
metodu kullanılarak yapılıyor. Metod adı property olup olmadığı ve varsa aldığı argümanlar.
Kodu çalıştırdıktan sonra türden bağımsız ortak bir şekilde arayüzden gerçeklenmiş metodların çalıştığını görebiliriz.
Peki sıklıkla yazıp çağırmamız gereken heryerde uzun uzun StaticClassInvoker.Invoke metodunu mu çağıracağız. Biraz daha kısaltıcı birşey yapılamaz mı?
Sanırım biraz daha kısaltabiliriz. Burada yapmamız gereken şey bir adet proxy class (vekil sınıf) hazırlamak. Adından anlaşılabileceği gibi bu çağrılarımıza vekalet edecek bir sınıf. Önceden hatırlayalım arayüzümüz interface IStaticInterfaceTemplate idi.
Bu arayüz için de IStaticInterfaceProxy adında bir proxy class yaratalım.
public class IStaticInterfaceProxy : IStaticInterfaceTemplate { private Type classType; public static explicit operator IStaticInterfaceProxy(Type type) { return new IStaticInterfaceProxy() { classType = type }; } public void MyMethod() { StaticClassInvoker.Invoke(this.classType, "MyMethod", false); } public string MyMethodWithArgs(string sval) { return StaticClassInvoker.Invoke<string>(this.classType, "MyMethodWithArgs", false, sval); } public double MyProperty { get { return StaticClassInvoker.Invoke<double>(this.classType, "MyProperty", true); } } }
burada yaptığımız arayüzü standart şekilde implement eden bir sınıf. Burada kilit nokta şu;
Eğer biz bu sınıf için bir explicit operatör override edip cast (tip dönüşüm) ederek bunu kontrol edersek bu proxy class’tan tip bilgisini verip doğrudan metodları çağırabiliriz. explicit keyword’ü bir tipten kendi veri tipimize dönüşümlerde kullanılan bir keyword olarak kullanılır. Bir de implicit olarak eşleniği vardır ki bu da cast etmekten ziyade doğrudan = (eşitle) operatörü ile atanacak veriler içindir. Yani
MyInt mn; int n = 12; mn = (MyInt)n; //explicit mn = n; //implicit
olarak işlem görür. Biz Type türünden bir bilgiyi kendi proxy sınıfımız için çevireceğimizden
public static explicit operator IStaticInterfaceProxy(Type type)
şeklinde yaptık. Bu sayede şu şekilde çağrı yapmamız olası.
((ProxyInterface)TypeVar).MemberFunction();
yani tür değişkenini proxy sınıfa cast edip çağırıyoruz. Bu durumda test kodumuz şu hale dönüşebilir
foreach (Type t in validClasses) { ((IStaticInterfaceProxy)t).MyMethod(); ((IStaticInterfaceProxy)t).MyMethodWithArgs("TEST"); ((IStaticInterfaceProxy)t).MyPropery; }
Performans?
Bütün bunlardan sonra ortaya bir de performans meselesi çıkıyor. En başta şunu çok rahat söyleyebiliriz. Bu işlem native şekle göre elbette oldukça yavaş çalışacaktır. Çünkü en başta da dediğim gibi reflection maliyetli bir işlemdir. Ancak yavaştan kasıt saniyeler ölçeğinde değil milisaniyeler ölçeğindedir. Ve normal şartlarda kullanıldığında aradaki performans farkı bir insanın anlayabileceği ölçekte değildir. Performans farkı ancak milisaniye hatta mikrosaniye hassasiyetinde yapıldığında görülebilir. Bu sayede performans farkının yüzde değerini çıkarıp az çok bir fikir sahibi olabiliriz.
Performans ölçümü için standart bir non-static arayüz implementasyonu çalıştıran bir de static arayüz implementasyonu çalıştıran iki farklı test yazıp sonuçlara bakalım.
Standart Interface Implementasyonu
class TestNativeNonStaticInterface : PerformanceTest { private IStaticInterfaceTemplate[] impls = new IStaticInterfaceTemplate[] { new NativeImplementationAlpha(), new NativeImplementationBeta() }; public override void Execute() { Begin(); for (int i = 0; i < 10000; i++) { foreach (IStaticInterfaceTemplate iface in this.impls) { iface.MyMethod(); iface.MyMethodWithArgs("TEST"); } } End(); } }
Statik Arayüz Implementasyonu
class TestStaticInterface : PerformanceTest { Type[] staticClasess = new Type[] { typeof(StaticImplementationAlpha), typeof(StaticImplementationBeta) }; public override void Execute() { Begin(); for (int i = 0; i < 10000; i++) { foreach (Type t in this.staticClasess) { StaticClassInvoker.Invoke(t, "MyMethod", false); StaticClassInvoker.Invoke(t, "MyMethodWithArgs", false, "TEST"); } } End(); } }
10,000 iterasyondan oluşan iki adet test mevcut. Testi çalıştırdığımızda aradaki farkı görebiliriz.
Non-Static hali beklendiği gibi çok hızlı. 10,000 iterasyon Sadece 2 milisaniye. Statik hali ise araya bir reflection operasyonları girdiği için haliyle diğerine göre oldukça yavaş. 136 milisaniye. Ancak tek başına ele alındığında ise yine de oldukça makul bir süre, yarım saniyeden daha kısa sürdü.
Peki bunu biraz daha kısaltabilir miyiz? Belki birazcık ek işlemlerle bir miktar daha kısalabilir. Örneğin her defasında metod bilgilerini baştan almak yerine tip bilgilerini önbellekleme (caching) yaparak invoke ettirebiliriz. Peki hem in memory caching (hafizada önbellekleme), hem de görece hızlı bir çözüm ne olabilir? Bu verileri bir hashtable üzerinde tutarak. Hashtable’lar adı üzerinde bir nesne veya değerin bir key (anahtar) değerinin belirli bir hash fonksiyonundan geçirilip hashtable dizisinin uzunluğuna modlanarak bir indeks elde etme işlemidir temel olarak. Böylece linear search (doğrusal arama) yaparak veya bir tree (ağaç) üzerinde arama yapmaktan kısa sürede istenilen veriye ulaşılabilir. Böyle bir işlemin maliyeti Big-O notation gösterimine göre O(1) kadardır. Yani tek seferde sonuca ulaşırız. Böyle bir işlem de bizim için ideal. Ancak ne kadar fayda sağlayacağı meçhul. Bunu göreceğiz.
private static MethodInfo TryGetFromCache(Type keyType, string method) { Hashtable methCacheHt; TypeCacheInfo cacheInfo; methCacheHt = (Hashtable)typeCacheHt[keyType]; if (methCacheHt == null) return null; cacheInfo = (TypeCacheInfo)methCacheHt[method]; if (cacheInfo == null) return null; return cacheInfo.meth; } private static void CacheTestAndSet(Type keyType, MethodInfo meth, BindingFlags bindFlag) { Hashtable methodCacheHt; if (!typeCacheHt.ContainsKey(keyType)) { methodCacheHt = new Hashtable(); methodCacheHt.Add(meth.Name, new TypeCacheInfo() { meth = meth, bindFlag = bindFlag }); typeCacheHt.Add(keyType, methodCacheHt); } else methodCacheHt = (Hashtable)typeCacheHt[keyType]; if (!methodCacheHt.ContainsKey(meth.Name)) methodCacheHt.Add(meth.Name, new TypeCacheInfo() { meth = meth, bindFlag = bindFlag }); }
StaticClassInvoker sınıfımıza ek iki adet metod ekliyoruz. Birisi istenen metodun cache’den varsa okunmasını, diğeri ise yoksa cache’lenmesini sağlayan fonksiyonlar. Esas Invoke kodu üzerinde caching rutinlerini ekledikten sonra bir de caching enabled halde çalışan hali için bir test yazıp çalıştıralım.
İşlem sonunda görülüyor ki sadece ~20 milisaniye kadar bir kazancımız oldu. Buradan .NET’in veya dolaylı olarak CLR’ın içsel olarak da bir caching mekanizması kullandığı söylenebilir. Ama yine de bu tip bir yaklaşımın ne kadar performansa etki edeceğini görmek açısından biraz faydalı oldu. Bu konuyla alakalı tüm kodlar, testlere https://github.com/0ffffffffh/staticinterface github adresimden ulaşabilirsiniz.
Kodları istediğiniz gibi kullanabilir, dağıtabilir, üzerinde değişiklik yapabilirsiniz. Umarım konu ile alakası olsun veya olmasın dolaylı olarak da olsa bazı konularda fayda sağlamıştır.