Roslyn ile Bırakın Uygulamanız Kendini Geliştirsin

   Roslyn makale serisinde bir önceki makalemde size çalışma-zamanında derleyici API’si yardımıyla nasıl kod üretebileceğinizi ve ürettiğiniz bu kodu uygulamanız içerisinde nasıl kullanabileceğinizi göstermiştim. Bu makalemde ise size teorik bir bilgi paylaşmak yerine bir önceki makalemde verdiğim bilgileri nasıl gerçek hayata uygulayabileceğinizi göstermek istiyorum. Umuyorum ki anlatacaklarım en az benim hoşuma gittiği kadar sizin de hoşunuza gidecek ve ufkunuzu açacaktır.

   Bu makalemde sizinle birlikte adım adım geliştireceğimiz örnek bir uygulama ile komut satırını kullanarak son kullanıcıdan aldığımız komutları işleteceğiz. Bunun için bir konsol uygulaması açarak aşağıdaki kodu yazalım;

namespace Com.Enterprisecoding.RoslynOrnegi {
    class Program  {
        static void Main(string[] args) {
            while (true) {
                Console.Write("Komut giriniz > ");
                var komutAdi = Console.ReadLine();

                try {
                    var komut = KomutFabrikasi.KomutGetir(komutAdi);
                    komut.Calis();
                }
                catch (Exception ex){
                    Console.WriteLine("Komut çalıştırılırken hata oluştu : " + ex.Message);
                }
            }
        }
    }
}

   Yukarıdaki kod parçacığı sayesinden kullanıcıdan gelen komutları alarak bu komutları işleyecek sınıfları bulabilir ve çalıştırabiliriz. Uygulama kullanıcıdan aldığı komutu KomutFabrikasi sınıfının KomutGetir fonksiyonuna göndererek verilen komut için uygun sınıfın bulunmasını sağlar.

public static class KomutFabrikasi {
    public static IKomut KomutGetir(string komutAdi) {
        switch (komutAdi) {
            case "Klasör Listele": return new KlasorListelemeKomutu();
            case "Dosya Sil": return new DosyaSilKomutu();
            case "Dosya Oluştur": return new DosyaOlusturKomutu();
            case "Çıkış": return new UygulamadanCikisKomutu();

            default: throw new Exception("Belirtilen komut bulunamadı");
        }
    }

}

   Basit bir yapısı olan KomutFabrikasi sınıfı gelen komuta göre ilgili sınıfı geri döndürmekte. Geriye dönen bu sınıflarda uygulamamızın main fonksiyonu içerisinde kullanılmakta. Örnek uygulamada kullanmak üzere tasarladığım komut sınıfları ise aşağıda bulanabilir;

using System;

namespace Com.Enterprisecoding.RoslynOrnegi.Komutlar{
    public interface IKomut {
        void Calis();
    }

    public class KlasorListelemeKomutu : IKomut {
        public void Calis() {
            Console.WriteLine("Klasör listeleme komutundan merhaba!");
        }
    }
    public class DosyaSilKomutu : IKomut  {
        public void Calis() {
            Console.WriteLine("Dosya silme komutundan merhaba!");
        }
    }

    public class DosyaOlusturKomutu : IKomut {
        public void Calis()
        {
            Console.WriteLine("Dosya oluşturma komutundan merhaba!");
        }
    }

    public class UygulamadanCikisKomutu : IKomut {
        public void Calis() {
            Environment.Exit(0);
        }
    }
}

   Buradaki komut sınıflarını daha kolay kullanılabilmesi adına ayrı bir dll projesi içerisine yerleştirelim. Aşağıda, uygulamamızı derleyip çalıştırmamız sonrasında nasıl kullanıcı etkileşimli olarak kullanılabildiği görülebilir;

Örnek uygulamamızı derleyip çalıştırdığımızda bu şekilde kullanılabilir

  Gördüğünüz gibi oldukça basit bir mantığı olan uygulamamız kullanıcı geri dönüşlerini komutlara dönüştürebilme yeteneğine sahip, hızlı ve oldukça kullanışlı; fakat tasarımımızın önemli bir zayıf noktası bulunuyor : genişletilebilir değil! Yeni bir komut eklemek için öncelikle IKomut arayüzünü implemente etmeli, ardından da KomutFabrikasi.KomutGetir fonksiyonu içerisine yeni bir girdi eklemeliyiz. Bu konu uygulamamızın tamamen kendimiz tarafında geliştirilmesi durumunda büyük bir problem oluşturmasa da kurumsal bir uygulamada 3.parti firmaların da eklenti geliştirebilmesi isteniyorsa maalesef ki ayağımıza köstek olacaktır.

   Alternatif olarak relfection yöntemi kullanılabilir. KomutGetir fonksiyonu çağırıldığında yüklü assembly’ler taranarak komutlar ve komut sınıfları bulunabilir ve bulunan sınıflar ilklendirilebilir. Tabi böylesi bir tasarımda komut sınıflarımıza eşleşecekleri komutun adını belirtmeliyiz; ki bunu da ancak öz nitelikle yardımıyla komut sınıfları üzerinde meta veri saklayarak yapabiliriz. Aşağıda bu amaçla kullanabileceğimiz öz nitelik sınıfını ve bu öz nitelik yardımıyla yeniden tanımlanmış olan komut sınıflarını bulabilirsiniz;

[AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
public sealed class KomutAttribute : Attribute {
    private readonly string komut;

    public KomutAttribute(string komut) {
        this.komut = komut;
    }

    public string Komut {
        get { return komut; }
    }
}

public interface IKomut {
    void Calis();
}

[Komut("Klasör Listele")]
public class KlasorListelemeKomutu : IKomut {
    public void Calis() {
        Console.WriteLine("Klasör listeleme komutundan merhaba!");
    }
}

[Komut("Dosya Sil")]
public class DosyaSilKomutu : IKomut  {
    public void Calis() {
        Console.WriteLine("Dosya silme komutundan merhaba!");
    }
}

[Komut("Dosya Oluştur")]
public class DosyaOlusturKomutu : IKomut {
    public void Calis()
    {
        Console.WriteLine("Dosya oluşturma komutundan merhaba!");
    }
}

[Komut("Çıkış")]
public class UygulamadanCikisKomutu : IKomut {
    public void Calis() {
        Environment.Exit(0);
    }
}

  Bu değişiklikler ardından KomutGetir fonksiyonumuzu yeniden düzenleyerek yeni tasarımımıza uygun hale getirebiliriz;

public static IKomut KomutGetir(string komutAdi) {
    var komutArayuzTuru = typeof(IKomut);
    var komutTurleri = AppDomain.CurrentDomain
                        .GetAssemblies()
                        .ToList()
                        .SelectMany(assembly => assembly.GetTypes())
                        .Where(type => komutArayuzTuru.IsAssignableFrom(type) && !type.IsAbstract);

    foreach (var komutTuru in komutTurleri) {
        var metaVeri = komutTuru.GetCustomAttributes(typeof(KomutAttribute), true)
                        .Select(attribute => (KomutAttribute)attribute)
                        .Where(komutAttribute => komutAttribute.Komut == komutAdi);

        if (metaVeri != null) {
            var varsayilanConstructer = komutTuru.GetConstructor(new Type[0]);

            if (varsayilanConstructer == null) {
                throw new Exception("Belirtilen tur varsayılan bir constructer'a sahip değil");
            }

            var sinifOrnegi = varsayilanConstructer.Invoke(null);

            return (IKomut)sinifOrnegi;
        }
    }

    throw new Exception("Belirtilen komut tanımlı değil!");
}

   Yeni tasarımımız bize eskisine göre çok daha fazla esneklik sunacaktır. Bu tasarım sayesinde yeni bir komut eklemek için sadece komut sınıfını oluşturup KomutAttribute öz niteliğini eklememiz yeterli olacak. Reflection yöntemi bize ihtiyacımız olan esnekliği sunmakla birlikte beraberinde belirli bir maliyeti de olacaktır ve hızdan bir miktar feragat etmemiz gerekecektir.

   İşte tam da bu noktada devreye makalemin sadece başında söz ettiğim Roslyn CTP girmekte. Pek Roslyn CTP kullanarak bu kodu nasıl daha hızlandırabiliriz?

   Bu sorunun cevabı aslında basit; kodu derleme öncesi elle yazıyor olsaydık nasıl yazıyorsak aynı kodu çalışma-zamanı uygulamamızı analiz ederek Roslyn CTP ile üretmeli, ardında oluşan assembly’yi yükleyerek kullanmalıyız. Tabi ki bunu yaparken bazı püf noktaları da bulunuyor; ki bunları aşağıda yeni haliyle paylaştığım KomutFabrikasi sınıfında bulabilirsiniz.

public static class KomutFabrikasi {
private delegate IKomut KomutGetirDelegate(string komutAdi);
    private static KomutGetirDelegate gercekKomutGetir;

    static KomutFabrikasi()  {
        var kod = @"using System;
using Com.Enterprisecoding.RoslynOrnegi.Komutlar;

namespace Com.Enterprisecoding.RoslynOrnegi {{
        class GercekKomutFabrikasi {{
                public static IKomut KomutGetir(string komutAdi) {{
                {0}

                default: throw new Exception(""Belirtilen komut bulunamadı"");
                }}
        }}
}}";

        var caseIfadeleri = new List<string>();
        var komutArayuzTuru = typeof(IKomut);
        var komutTurleri = AppDomain.CurrentDomain
                            .GetAssemblies()
                            .ToList()
                            .SelectMany(assembly => assembly.GetTypes())
                            .Where(type => komutArayuzTuru.IsAssignableFrom(type) && !type.IsAbstract);

        foreach (var komutTuru in komutTurleri) {
            var komutOzniteligi = komutTuru.GetCustomAttributes(typeof(KomutAttribute), true)
                                            .FirstOrDefault() as KomutAttribute;

            if (komutOzniteligi != null) {
                caseIfadeleri.Add(string.Format(@"case ""{0}"" : return new {1}();", komutOzniteligi.Komut, komutTuru.Name));
            }
        }
        kod = string.Format(kod, string.Join(Environment.NewLine, caseIfadeleri));

        var assemblyAdi = "Enterprisecoding-" + Guid.NewGuid() + ".dll";

        var sozDizinAgaci = SyntaxTree.ParseCompilationUnit(kod);
        var derleme = Compilation.Create(
            assemblyAdi,
            options: new CompilationOptions(assemblyKind: AssemblyKind.DynamicallyLinkedLibrary),
            syntaxTrees: new[] { sozDizinAgaci },
            references: new[] { 
                new AssemblyFileReference(typeof(object).Assembly.Location), //mscorlib.dll
                new AssemblyFileReference(typeof(IKomut).Assembly.Location),
            });

        Assembly derlenmisAssembly;
        using (var akis = new MemoryStream())  {
            var derlemeSonucu = derleme.Emit(akis);
            derlenmisAssembly = Assembly.Load(akis.GetBuffer());
        }

        var gercekKomutFabrikasiTuru = derlenmisAssembly.GetType("Com.Enterprisecoding.RoslynOrnegi.GercekKomutFabrikasi");
        var komutGetirMetodu = gercekKomutFabrikasiTuru.GetMethod("KomutGetir");
        gercekKomutGetir = (KomutGetirDelegate)Delegate.CreateDelegate(typeof(KomutGetirDelegate), komutGetirMetodu);
    }

    public static IKomut KomutGetir(string komutAdi) {
        return gercekKomutGetir(komutAdi);
    }
}

   Yeni kodumuzda uygulama static KomutFabrikasi sınıfımızı ilk kez çağırdığında static constructer’ının içerisindeki kodları bir defalık çalıştıracaktır. Buradaki kodlar ise öncelikle yüklü assembly’ler içerisindeki komutları bulacak ve daha sonra  bu komutlar için string olarak örneğimizin ilk halindeki kodu oluşturacaktır. Takip eden satırlarda ise önceki makalemde paylaştığım şekilde bu string ifade derlenerk bir assembly’ye dönüştürülecek ve hafızaya yüklenecektir. Bu noktada dikkat ederseniz reflection ile elde ettiğim oluşan yeni GercekKomutFabrikasi.KomutGetir fonksiyonunu çağırmak için reflection değil delegate kullanmaktayım. Bu şekilde hem daha okunaklı hem de daha hızlı bir kullanıma sahip olmaktayım. Son haliyle uygulamamıza bir breakpoint koyut oluşturulan kod string’ine göz atacak olursak aşağıdaki gibi bir çıktı göreceksiniz,; ki bu da ilk örneğimizle aynı;

using System;
using Com.Enterprisecoding.RoslynOrnegi.Komutlar;

namespace Com.Enterprisecoding.RoslynOrnegi {
        class GercekKomutFabrikasi {
                public static IKomut KomutGetir(string komutAdi) {
                case "Klasör Listele" : return new KlasorListelemeKomutu();
case "Dosya Sil" : return new DosyaSilKomutu();
case "Dosya Oluştur" : return new DosyaOlusturKomutu();
case "Çıkış" : return new UygulamadanCikisKomutu();

                default: throw new Exception("Belirtilen komut bulunamadı");
                }
        }
}

   Bu noktada düşmem gereken önemli bir nokta; henüz CTP seviyesinde olan Roslyn’in case ifadelerini desteklemediği (desteklenmeyen tüm özellikleri listesi bu makalemde bulabilirsiniz). Bu durumda da maalesef yukarıdaki kod case ifadeleri nedeniyle derlenmeyecektir. Konunun anlaşılması adına bu haliyle paylaştığım ve ilerleyen sürümlerde derlenebiliyor olacak olan kodu mevcut CTP sürümünde de derleyip çalıştırmak için if ifadeleri kullanabilirsiniz;

private delegate IKomut KomutGetirDelegate(string komutAdi);
private static KomutGetirDelegate gercekKomutGetir;

static KomutFabrikasi()  {
    var kod = @"using System;
using Com.Enterprisecoding.RoslynOrnegi.Komutlar;

namespace Com.Enterprisecoding.RoslynOrnegi {{
    class GercekKomutFabrikasi {{
            public static IKomut KomutGetir(string komutAdi) {{
            {0}
            else {{ throw new Exception(""Belirtilen komut bulunamadı""); }}
            }}
    }}
}}";

    var ifIfadeleri = new List<string>();
    var komutArayuzTuru = typeof(IKomut);
    var komutTurleri = AppDomain.CurrentDomain
                        .GetAssemblies()
                        .ToList()
                        .SelectMany(assembly => assembly.GetTypes())
                        .Where(type => komutArayuzTuru.IsAssignableFrom(type) && !type.IsAbstract);

    foreach (var komutTuru in komutTurleri) {
        var komutOzniteligi = komutTuru.GetCustomAttributes(typeof(KomutAttribute), true)
                                        .FirstOrDefault() as KomutAttribute;

        if (komutOzniteligi != null) {
            ifIfadeleri.Add(string.Format(@"if (komutAdi == ""{0}"") {{ return new {1}(); }}",komutOzniteligi.Komut, komutTuru.Name));
        }
    }
    kod = string.Format(kod, string.Join(" else ", ifIfadeleri));
            
    var assemblyAdi = "Enterprisecoding-" + Guid.NewGuid() + ".dll";

    var sozDizinAgaci = SyntaxTree.ParseCompilationUnit(kod);
    var derleme = Compilation.Create(
        assemblyAdi,
        options: new CompilationOptions(assemblyKind: AssemblyKind.DynamicallyLinkedLibrary),
        syntaxTrees: new[] { sozDizinAgaci },
        references: new[] { 
            new AssemblyFileReference(typeof(object).Assembly.Location), //mscorlib.dll
            new AssemblyFileReference(typeof(IKomut).Assembly.Location),
        });

    Assembly derlenmisAssembly;
    using (var akis = new MemoryStream())  {
        var derlemeSonucu = derleme.Emit(akis);
        derlenmisAssembly = Assembly.Load(akis.GetBuffer());
    }

    var gercekKomutFabrikasiTuru = derlenmisAssembly.GetType("Com.Enterprisecoding.RoslynOrnegi.GercekKomutFabrikasi");
    var komutGetirMetodu = gercekKomutFabrikasiTuru.GetMethod("KomutGetir");
    gercekKomutGetir = (KomutGetirDelegate)Delegate.CreateDelegate(typeof(KomutGetirDelegate), komutGetirMetodu);
}

public static IKomut KomutGetir(string komutAdi) {
    return gercekKomutGetir(komutAdi);
}        
}

   Kodumuzun performansını arttırmak adına yapılabilecek son bir düzenleme ise Roslyn CTP ile oluşan assembly’yi çalışma-zamanında hafızaya yükleme yerine dosya sistemine saklamak ve buradan yüklemek olabilir. Bu şekilde uygulamanın takip eden çalıştırılmalarında tekrar tekrar derlemek yerine varsa doğrudan bu assembly yüklenebilir. Bu sayede derleme maliyeti sadece ilk kullanım için söz konusu olacağından daha da performanslı bir uygulamaya sahip olabiliriz.

   Umarım paylaştığım bu örnek Roslyn CTP’nin ne kadar güçlü olduğu konusunda size fikir verebilmiştir. Yaratıcı fikirler ve Roslyn bir araya geldiğinde yukarıdaki örneğimize benzer şekilde kendi kendini değiştiren, kullanımına göre şekillenen performanslı uygulamalar kolaylıkla geliştirilebilir.

Fatih Boy

Ankara'da yaşayan Fatih, bir kamu kurumunda danışman olarak çalışmaktadır. ALM süreçleri, kurumsal veri yolu sistemleri, kurumsal altyapı ve yazılım geliştirme konularında destek vermektedir. Boş zamanlarında açık kaynak kodlu projeler geliştirmeyi ve bilgisini yazdığı makalelerle paylaşmayı seven Fatih, aynı zamanda Visual C# ve Visual Studio teknolojileri konusundan Microsoft tarafından altı yıl üst üste MVP (En Değerli Profesyonel) ödülüne layık görülmüştür. İş hayatı boyunca masaüstü uygulamaları, web teknolojileri, akıllı istemciler gibi konularda Asp.Net, Php, C#, Java programlama dilleri ve MySql, MsSql ve Oracle gibi veritabanı yönetim yazılımları ile çalışmıştır. İngilizce ve Türkçe olarak yayınlanan makalelerini gerek İngilizce bloğunda, gerekse de Türkçe bloğunda bulabileceğiniz gibi web sitesinden de açık kaynak kodlu geliştirdiği yazılımlarına ulaşabilirsiniz. vCard - Twitter - Facebook - Google+

Bir Cevap Yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir