SOLID Yazılım Süreçlerini Yavaşlatıyor Mu ?

Geçtiğimiz haftalarda  SOLID yazı serisini sonlandırdık ve sırasıyla

konularından bahsettik.

İlk defa SOLID'i araştırırken bir çok blog yazlım forum sitesi vs. dolaştım ve yapılan yorumları okuma fırsatım olmuştu. Bu yorumlardan en çok yaygın olanı kabaca şöyle bir şey "ya tamamda ne gerek var bunca şeye ?.. SOLID hem beni hemde yazdığım kodu yavaşlatıyor..." vs. şeklinde yorumlar yapılmıştı. Doğrusu en başlarda bende buna benzer cümleler kurmadım değil :)

SOLID prensiplerinin asıl amacı Separation of Concerns veya türkçe deyimiyle farklı kavramların, işlerin birbirinden ayrılması ilkesini High Cohesion & Low Coupling yoluyla geliştirmek. Prensipleri anlattığımız ilk yazıda şuna benzer bir cümle kullanmıştım "asıl amaç sonradan kolay müdahale edilebilir, yeni modüllerin kolay entegre edilebildiği core code'a çok fazla dokunmadan yazılımlar geliştirmek..." ve aslında bu cümle 5 temel prensiple bizlere anlatılıyor. İlk bakışta bu prensiplerin uygulaması zahmetli gibi geliyor olabilir ancak bunu ileriye dönük iyi bir yatırım olarak görmeliyiz ve yazılım geliştirme sürecini while(true) {develop}şeklinde sonsuz bir döngü olarak bakmalıyız.

SOLID prensiplerine uyalım derken bazı istenmeyen sonuçlarortaya çıkarmamızda mümkün;

  • Çok fazla abstraction ve interface,
  • İstenmeyen Concrete class'lar,
  • Gerektiğidnen fazla layer oluşabilme itimali,
  • Kalıtım ağacıdnaki derinliği gereknden fazla artırma,
  • Yo-yo problemi,
  • vs...

SOLID prensiplerinin gelişi güzel uygulanması bu ve benzeri durumların olmasına neden oalbilir ancak geliştirme yaparken arada bir geriye gelip geniş açıdan bakmakta fayda var çünkü bazen oop nin dibine vurmaya çalışırken farkında olmadan saçmalamışta olabiliriz :) . Tabi burda da devreye iyi deneyime sahip, çeşitli projelerde görev almış vizyon sahibi bir takım liderine gerek var ki code-review sırasında nokta atışı yaparak yanlış veya gereksiz olan yerleri saptayıp düzeltebilme fırsatınız olsun.

SOLID yazılım süreçlerini yavaşlatıyor mu ? sorusuna geri dönecek olursak, yukarıda da bahsettiğimiz üzre çok separation of concern'ü düşünerek yapacağımız işleri çoklu katmanalra ayırarak böyle bir duruma sebebiyet verme ihtimalimiz azda olsa var. Diğer bir deyişle tek bir metot çağrısı yaparak çözebileceğimiz bir sorunu birden fazla instance alarak metot çağrısı yapmaya zorlandığımız durumlar ortaya çıkabilir ve bu da geliştirme süresini uzatabilir. Ancak SOLID prensiplerini hangi projeye uygulanması gerektiğine karar vermekte bir o kadar önemli çünkü small diyebileceğimiz projeler için dibine kadar SOLID kastırmak çokta şart değildir hani. Bunun yerine enterprise veya işlevselliği çok olan büüyk projerlerde SOLID prensiplerini uygulamaya çalışmalıyız ve unutmamalıyız ki SOLID'e uygun geliştirmeler yaparak günü kurtarmıyoruz bunu ileriye dönük bir yatırım olarak görmeliyiz ve asıl faydalarını projenin ilerleyen fazlarında görüyor olacağız.

Ben kendi projelerimde nasıl ilerliyorum diye soracak olursak, mümkün olduğunda separation of concern'e odaklanmaya çalışıyorum ve ona uygun ilerleyecek sade-temiz bir design pattern seçmeye çalışırım. Çok fazla abstraction'dan dolayı performans sorunları olacağını düşündüğüm yerler varsa da yaygın kullanılan bir runtime performans tool'u kullanarak testler yapıp sorunu saptamaya odaklanırım.

 

Sonuç olarak SOLID yazılım süreçlerini yavaşlatıyor mu sorusuna bence en uygun cevap, SOLID'i ne kadar doğru zamanda doğru yerde kullandığınızla ilgili. Eğer prensiplere tamamiyle hakimseniz ve doğru projede uyguladığınızı düşünüyorsanız kesinlikle yazılım süreçlerini yavaşlatmıyor aksine hızlandırıyor. Şuan için farkında olmasanız bile ileriye dönük size birçok katkı sağlayacağı noktalar olacaktır. Eğer çok gereksiz bir projede hiç ihtiyaç yokken SOLID'in dibine vurmakta tavsiye edilen bir durum olmayacaktır, çok büyük bir ihtimalle geliştirme sırasında gereksiz zaman kaybına yol açacaktır.

Web Api MemoryCache Kullanımı

Daha önce Web Api ile ilgili yazdığımız yazılarda çeşitli konulara değinmiştik ve bunlardan bir tanesi de yazısıydı. Bu yazımızda yine web api için cache'den bahsediyor olacağız anack bu sefer output cache değilde uygumala içerisinde kendimiz manuel olarak memory'e atıp daha sonra kullanmak istediğimiz de alıp modify edebilmemizi sağlayan veya direkt olarak output cache de olduğu gibi alıp client'a dönmemizi sağlayan yapı MemoryCache den bahsediyor olacağız. MemoryCache .Net 4.0 ile birlikte System.Runtime.Caching.dll içerisinde sunulmuş bir yapı olarak karşımıza çıkıyor. Projemize bu dll'i referans olarak eklemek için solution'da bulunan references'a sağ tıklayıp add reference deyip Assemblies => Framework kategorisine tıkalyıp gelen listeden System.Runtime.Caching.dll'ini seçip ekliyoruz.

Referansımızı ekeldikten sonra projemize MemoryCacheManager adında adında bir class ekleyelim. Case şöyle olsun, ProductController.cs adında bir controller ve içerisinde GetAllProducts ve GetProductById adında iki metot tanımlayalım. İlk metotda geriye döndüğümüz product listesini ICache den implement olan MemoryCacheManager class'ını kullanarak MemoryCache'e atalım ve GetProductById metoduna Id parametresi ile request'te bulunulduğunda cache'den okuyup geriye product objesini dönelim.

ICache.cs

    public interface ICache
    {
        bool Contains(string key);//key varmı yokmu diye control ettiğimiz metot
        void Add<T>(string key, T source);//cache key'i ile birlikte cache model'i alıp cache'e ekleyen metot
        T Get<T>(string key);//key parametresi alarak cache'de ki data yı return eden metot
        void Remove(string key);//key parametresine göre mevcut cache'i silen metot
    }

 

MemoryCacheManager.cs

    public class MemoryCacheManager : ICache
    {
        ObjectCache cache;

        public MemoryCacheManager()
        {
            cache = MemoryCache.Default;
        }

        public void Add<T>(string key, T source)
        {
            //60 dakika boyunca cache'de tutacak
            var policy = new CacheItemPolicy { AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(60) };
            cache.Add(key, source, policy);
        }

        public bool Contains(string key)
        {
            return cache.Contains(key);
        }

        public T Get<T>(string key)
        {
            return (T)cache.Get(key);
        }

        public void Remove(string key)
        {
            cache.Remove(key);
        }
    }

 

Product.cs

public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Category { get; set; }
        public decimal Price { get; set; }
    }

 

ProductController.cs

    public class ProductController : ApiController
    {
        [HttpGet]
        public Product[] GetAllProducts()
        {
            Product[] products = new Product[]
            {
                new Product { Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1 },
                new Product { Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M },
                new Product { Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M }
            };

            //ürünleri cache'e atıyoruz
            var cm = new MemoryCacheManager();

            string cacheKey = "products";
            if (cm.Contains(cacheKey))//varmı diye kontrol ediyoruz, eğer varsa mevcutu sil yeni listeyi ekle
                cm.Remove(cacheKey);
            cm.Add(cacheKey, products);

            return products;
        }

        [HttpGet]
        public Product GetProductById(int Id)
        {
            var cm = new MemoryCacheManager();
            string cacheKey = "products";
            if (cm.Contains(cacheKey))//varmı diye kontrol ediyoruz
                return cm.Get<Product[]>(cacheKey).FirstOrDefault(p => p.Id == Id);
            return null;
        }
    }

Projemizi run edip browser aracılığıyla önce GetAllProducts metoduna request'te bulunuyoruz ve product listesini return etmeden önce MemoryCacheManager'ın Add metodunu kullanarak geriye product listesini return etmeden önce listeyi alıp "products" key'i ile cache'e atıyor ve sonrasında return ediyor. Product listesini aldıktan sonra Id=2 olan ürün için GetProductById metoduna request'te bulunuyoruz ve ilk olarak MemoryCacheManager içerisine gidip "products" key'i ile mcache atılmış olan product listesini bulup alıyor ve sonrasında Id=2 olan product'ı bulup geriye döndürüyor.

Manuel olarak MemoryCache entegrasyonu bu şekilde yapabilirsiniz veya ihtiyaca göre farklı kullanımlarda uygulayabilirsiniz. Cache önemlidir arkadaşlar, doğru yerde kullanıldığında çok can kurtarır :)

Web Api Projelerinde Versiyonlama

Yazmış olduğunuz Api projelerinde versiyonlama yapmak oldukça önemlidir. Düzenli bir versiyonlama yaparak hem mevcut hemde yeni versiyonu kullanacak olan client'lar için tutarlı response'lar sağlayabilirsiniz. Örneğin v1.0 için çalışan listesi dönen bir api metodu yazdınız ve mevcutta kullanan kullanıcılar var. production'da. Sonra v2.0 de dediler ki çalışan isimlerinin yanında departman kodu da yazsın (Caner Tosuner, IT gibi). Her ne kadar kullanıcılar v2.0 geliştirmesini yapacak olsalarda belli bir süre daha eski versiyon için destek veriyor olmak gerekir çünkü bütün herkes aynı anda v2.0'a geçemeyebilir veya v1.0 da kalmak isteyebilir. İşte bu gibi durumları en iyi şekilde yönetilmemiz için versiyonlamayı son derece iyi yapmamız gerekir.

Web Api için versiyonlama yapmanın ortak kabul görmüş bir kaç yol bulunmakta;

  • URL de versiyon bilgisini query string olarak geçebiliriz,
  • Request Header'a "appVersion" gibi bir key/value ekleyebiliriz,
  • Genelde media type için kullanılsada AcceptHeader'a versiyon bilgisini ekleyebiliriz,
  • Her request'te BaseRequest'te içerisinde bir versiyon bilgisini alma.

Yukarıda sıralanan 4 yol da versiyonlama için çözüm ancak WebApi'ın son sürümüyle birlikte tercih edilmemeye başlandı hatta ve hatta yanlış çözüm olarak söyleyenler bile mevcut.

WebApi için versiyonlamayı Api metodlarının başına attribute olarak ekleyeceğimiz Route class'ı bulunmakta. Bu class'ı kullanarak kısaca hangi url bilgisi ile o metod çağrılabilir onu belirtmiş oluyoruz.

Burda dikkat etmemiz gereken konuların başında elimizden geldiğince mevcut URL'i bozmamaız gerekiyor.

Şimdi gelelim örneğimize.

Yukarıda bahsettiğimiz örnek üzerinden ilerleyelim ve EmployeeController adında bir ApiController'ımız var ve içerisinde GetEmployeeList() adında bir metod olsun. Bu metod öncelikle v1.0 için geriye çalışan adı ve soyadı bilgilerini içeren bir string array dönsün.

        [HttpGet]
        [Route("api/v1/Employee/{GetEmployeeList}")]
        public IHttpActionResult GetEmployeeList()
        {
            var list = new string[] { "İlhan Mansız", "Tümer Metin" };

            return Ok(list);
        }

Yukarıda ki gibi v1.0 için kullanıcılar bu metodu kullanmakta. GetEmployeeList() metoduna request'te bulunmak için url "http://localhost/api/v1/Employee/GetEmployeeList".

Ancak v2.0 da ad soyad bilgisinin yanında bir de departman adı istenmekte İlhan Mansız, Pazarlama gibi.

Şimdide v2.0 metodunu yazalım.

        [HttpGet]
        [Route("api/v2/Employee/{GetEmployeeList}")]
        public IHttpActionResult GetEmployeeListV2()
        {
            var list = new string[] { "İlhan Mansız, Pazarlama", "Tümer Metin, Satış" };

            return Ok(list);
        }

v2.0 metodunuda yazdık. Bu metoda request'te bulunmak için url "http://localhost/api/v2/Employee/GetEmployeeList".

Yukarıda da bahsettiğmiz gibi en dikkat etmemiz gerekn konuların başında client'ın yapacağı request'i ve api url'ini elimizden geldiğinde aynı tutmak ve bütün versiyonlarda url için değişen tek şeyin versiyon numarası olduğunu belirtmek. Böylelikle service tarafı için ayrımı url de bulunan versiyon numarasından alarak yapabiliriz.

Route attribute'ü kullanmadan versiyonlama işlemini farklı controller'lar kullanarak ta yapabilirsiniz. Üstteki örneğimizde olduğu gibi GetEmployeeList() meetodlarını içeren 2 farklı EmployeeV1Controller ve EmployeeV2Controller adında controller'lar tanımlayabiliriz. Url route işini de WebApiConfig.cs class'ı içerisine yine yukarıdaki url'leri kullanarak gerekli controller'lar da bulunan metodlara yönlendirmeleri yapabilirsiniz. Örneğin şu şekilde;

config.Routes.MapHttpRoute(
            name: "ApiV1_Route",
            routeTemplate: "api/v1/{controller}/{id}",
            defaults: new { controller = "EmployeeV1Controller", action = "GetEmployeeList", id = RouteParameter.Optional }
            );

config.Routes.MapHttpRoute(
            name: "Apiv2_Route",
            routeTemplate: "api/v2/{controller}/{id}",
            defaults: new { controller = "EmployeeV2Controller", action = "GetEmployeeList", id = RouteParameter.Optional }
            );

Yine "http://localhost/api/v1/Employee/GetEmployeeList" metoduna istekte bulunduğumuzda yukarıda yaptığımız config'den dolayı EmployeeV1Controller'ında bulunan GetEmployeeList metoduna yönlendirecektir. v2 için ise EmployeeV2Controller'ında bulunan GetEmployeeList metoduna yönlendirecektir.

 

Versiyonlama büyük çapta olan projeler için oldukça önemlidir. Production'a uygulamanızı çıktıktan sonraki zaman içerisinde tekrardan update'ler çıkmaya devam edeceksinizdir ve versiyonlama yaparak bu işlemleri yürütüyor olmak hem kolaylıklar sağlar hem de projenizi daha yönetilebilir hale getirir.