Caner Tosuner

Leave your code better than you found it

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.

Dependency Inversion Principle

SOLID prensipleri yazı serisinde son prensip olan SOLID'in "D" si Dependency Inversion prensibine gelmiş bulunuyoruz. Türkçe anlamını her ne kadar beğenmiyor olsam da atalarımız "Bağlılığı Tersine Çevirme Prensibi" olarak çevirmişler.

Bu prensip bizlere OOP yaparken şu 2 kurala uymamız gerektiğini söylüyor;

  • Üst seviye (High-Level) sınıflar alt seviye (Low-Level) sınıflara bağlı olmamalıdır, aralarındaki ilişki abstraction veya interface kullanarak sağlanmalıdır,
  • Abstraction detaylara bağlı olmamalıdır, aksine detaylar abstraction'lara bağlı olmalıdır.

Birçok projede malesef üst seviye sınıflar alt seviye sınıflara bağlıdır ve bu sınıflarda bir değişiklik yapmak istediğimizde başımıza onlarca iş açabilmektedir çünkü alt sınıfta yapılan bu değişiklik üstü sınıfıda etkileyebilir ve üst sınıfların etkilenmesi de projenizdeki bütün yapının etkilenmesi neden olabilir. Bu durum aynı zamanda reusability yani tekrar kullanılabilirlik durumuna engeller. İşte bu karmaşayı ortadan kaldırmak için Dependency Inversion prensibi ortaya çıkmıştır ve üsttede belirttiğim gibi modulleri birbirinden soyutlamamız gerekir. SOLID Nedir makalesinde verdiğim örnekte olduğu gibi gözlüğünüz var ve camlarını değiştirmek istediniz gittiniz gözlükçüye adam dediki "Bu camların değişmesi için gözlüğünde değişmesi gerek..." saçma dimi :) işte bazen bu prensibe uymazsak gözlük örneğinde olduğu gibi enteresan sorunlar başımıza gelebilir. 

Örnek bir case üzerinden ilerleyelim. LogManager adında bir class'ımız olsun ve bu class'ın Log() isminde bir metodu ve bu metod çağrıldığında FileLogger() objesine ait olan Log() metodu çağrılsın ve FileLog işlemini yapsın.

	public class FileLogger  
	{
		public string Message { get; set; }
		public void Log()
		{
			//File Log
		}
	}

	public class DBLogger  
	{
		public string Message { get; set; }		
		public void Log()
		{
			//Database Log
		}
	}

	public class LogManager  
	{
		private FileLogger _file;
		private DBLogger _db;
		
		public LogManager()
		{
			_file = new FileLogger();
			_db = new DBLogger();
		}
	
		public void Log()
		{
			_file.Log();
			_db.Log();
		}
	}

Yukarıda görüldüğü gibi LogManager üst seviye class'ımız ve tam da Dependency Inversion prensibine ters olarak FileLogger ve DBLogger class'larına yani alt seviye class'lara bağlı. DIP bize bu gibi alt-üst seviye sınıf ilişkilerini abstraction veya interface'ler kullanarak kurmamızı söylüyor ancak durum şuan bunun tam tersi ve yarın bir gün yöneticiniz geldi dedi ki "bundan sonra uygulama Log'ları EventLog'a da yazdırılacak". Bunun için gidip aynen File-DB Logger class'larında olduğu gibi EventLogger adında bir class tanımlayıp LogManager() içerisinde aynı işlemleri yapmak heralde istediğimiz bir çözüm değildir ki bu LogManger class'ına extra bir nesneye daha bağlı hale getirir. Hedefimiz LogManager class'ını olabildiğince nesne bağımsız hale getirmek.

 

Bunun için ilk olarak ILogger adında bir interface tanımlayalım ve FileLogger & DBLogger class'larını bu interface'den implement edelim.

	public interface ILogger  
	{
		void Log();
	}

	public class FileLogger : ILogger
	{
		public string Message { get; set; }
		public void Log()
		{
			//File Log
		}
	}

	public class DBLogger : ILogger
	{
		public string Message { get; set; }
		
		public void Log()
		{
			//Database Log
		}
	}

Ve son olarak da LogManager class'ını sadece ILogger interface'ine bağlı hale getirmek kalıyor. Böylelikle ILogger'den implement olmuş bütün class'lar LogManager tarafından kullanılabilecektir.

	public class LogManager 
	{
		private ILogger _logger;
		public LogManager(ILogger logger)
		{
			_logger = logger;
		}
	
		public void Log()
		{
			_logger.Log();
		}
	}

Bu refactoring işlemlerini yaptıktan sonra artık LogManager class'ı Dependency Inversion prensibine uygun hale gelmiştir yani alt seviye sınıflara bağlı değildir aradaki ilişki interface kullanarak sağlanmıştır. 

LogManager class'ının kullanım şekli ise aşağıdaki gibidir.

	public static void Main()
	{
		var dbLogger = new DBLogger();
		dbLogger.Message = "Test 123";
		
		var manager = new LogManager(dbLogger);
		manager.Log();
	}

Özetle; yaptığımız refactoring işlemiyle birlikte DIP'nin söylediği gibi high-level ve low-level sınıfları abstraction'lara bağlı hale getirdik.

Bu yazımızla beraber SOLID prensiplerinin sonuna gelmiş bulunuyoruz. Umarım faydalı bir yazı serisi olmuştur, soru, yorum veya eleştirilere açığımdır :) hope to keep in touch

Interface Segregation Principle

SOLID prensipleri yazı dizisinde sırada SOLID'in "I" olan Interface Segregation (ISP) var. Bu prensip bize kısaca şunu söylüyor; "Nesneler asla ihtiyacı olmayan property/metot vs içeren interface'leri implement etmeye zorlanmamalıdır  !".

Terminolojide bu interface'ler "fat" yada "polluted" interfaces diye adlandırılır.  ISP uygulanmadığında bir den fazla sorumluluğu olan nesneler ortaya çıkar ki bu da aslında SOLID'in "S" si olan "Single Responsibility" prensibine aykırı bir şeydir. Bu sınıflar yüklenen çoklu sorumluluklardan dolayı zaman içerisinde yönetilemez hale gelirler ve projemiz patates olur çıkar. Bu tarz case'ler le karşılaşıldığında interface içerisinde bulunan kullanılmaması gereken özellikleri içeren yeni interface'ler tanımlanır ve ihtiyaca göre nesneler tarafından implement edilir. Eğer projeniz bu prensibi ihlal ediyorsa Adapter Design Pattern avantajlarını kullanarak da ilerleyebilirsiniz. İlgili nesne interface'i implement edip hiç kullanmayacağı metotlara sahip olduğunda class'ınız içerisinde "NotImplementedException" yazan metotlar olacaktır ve buda OOP açısından hiç istenen bir şey değildir.

ISP'yi örnek bir case üzerinde anlatmaya çalışalım. Bir tane FileLog ve DbLog yapan işlemleri yapan bir proje geliştireceğiz ve içerisinde ILog adında bir interface tanımlayalım ve bu interface'inde Log(), OpenConn(), CloseConn() gibi metodları olsun.

public interface ILog
{ 
    void Log(string message);

    void OpenConnection();

    void CloseConnection();
}

 

İlk olarak DBLogger sınıfını yazalım

public class DBLogger : ILog
{		
        public void Log(string message)
        {
            //Code to log data to a database
        }

        public void OpenConnection()
        {
            //Opens database connection
        }

        public void CloseConnection()
        {
           //Closes the database connection
        }
}

 

Şimdi de FileLogger class'ını yazalım. 

public class FileLogger : ILog
    {
        public void Log(string message)
        {
            //Code to log to a file           
        }
    }

FileLog işlemi yaparken Db de olduğu gibi bir Connection açıp kapama işlemi yok ve bu metotları FileLogger'da kullanmak istemiyoruz sadece ILog interface'inde tanımlı olan Log(string message) metodunu kullanmak istiyoruz. Projeyi derlediğinizde şöyle bir hata alırsınız "FileLogger class doesn't implement the interface members ILog.OpenConnection() and ILog.OpenConnection()" .  Peki hatayı görüp eksik interface'in eksik üyelerini implement edelim. Bu sefer class'ımız aşağıdaki gibi olacaktır.

public class FileLogger : ILog
    {
        public void Log(string message)
        {
             //Code to log to a file           
        }

        public void CloseConnection()
        {
            throw new NotImplementedException();
        }

        public void OpenConnection()
        {
            throw new NotImplementedException();
        }
    }

E şimdi ne oldu ? Patates ! Hiç ihtiyacımız olmasa da FileLogger class'ına 2 tane gereksiz metot kazandırdık. İşte ISP burada devreye giriyor. Yapmamız gereken şey yeni interface veya interface'ler tanımlayarak FileLogger ve DbLogger class'larının sadece ihtiyacı olan metotları içeren interface'i implemente etmesini sağlamak.

Yeni interface'ler aşağıdaki gibi olacaktır.

    public interface ILog
    {
        void Log(string message);
    }

    public interface IDBLog: ILog
    {
        void OpenConnection();

        void CloseConnection();
    }

    public interface IFileLog: ILog
    {
        void CheckFileSize();

        void GenerateFileName();
    }

 

Bu interface'leri implement eden DbLogger ve FileLogger class'larımızda aşağıdaki gibidir.

 public class FileLogger : IFileLog
    {
        public void CheckFileSize()
        {
            //Code to check log file size
        }

        public void GenerateFileName()
        {
            //Code to generate a new file name
        }
		
        public void Log(string message)
        {
            //Code to log data to the log file
        }
    }

    public class DBLogger : IDBLog
    {
        public void Log(string message)
        {
            //Code to log data to the database
        }

        public void OpenConnection()
        {
            //Code to open database connection
        }

         public void CloseConnection()
         {
            //Code to close database connection
         }
    }

Interface Segregation çok önemli bir prensiptir. Özellikle Adapter Design Pattern ile haşır neşir olan arkadaşlar için dahada fazla öneme sahiptir. İçerisinde 60-70 tane üyesi bulunan interface'ler yazmaktan da çekinin bu gibi durumlarda bir yolunu bulup interface üyelerini ayırıp gruplayıp yeni interface'ler türetmeye çalışmamız daa doğru olacaktır. 

Liskov Substitution Principle

Geldik Liskov prensibine. Bu yazıda SOLID' in "L" si olan "Liskov Substitution Principle (LSP)" prensibinden bahsedicez. 

LSP'nin tarihine bakacak olursak; bu prensip ilk olarak 1987 yılında Barbara Liskov tarafından tanıtıldı ve daha sonrasında Jeannette Wing ile birlikte 1993 yılında bu prensibi resmi olarak yayınladılar.

Bu prensibin orjinal tanımı "Let q(x) be a property provable about objects x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T." dır. Ancak bu tanımdan senin benim gibi hiç kimse bir şey anlamadığı için namı değer bob amca Robert C. Martin  "Agile Principles, Patterns, and Practices in C#" kitabında bu prensibi kısaca "Subtypes must be substitutable for their base types." diye tanımladı ve günümüze yazılım dünyasında da bu tanımıyla kabul görmektedir.

Bu kadar tarih ve ingilizce bilgisinden sonra gelelim Liskov'un Türkçe sine. Bu prensip bize şunu söyler ; Alt sınıflardan oluşturulan nesneler üst sınıfların nesneleriyle yer değiştirdiklerinde aynı davranışı göstermek zorundadırlar.. Daha bizim dilden konuşacak olursak ; alt sınıf üst sınıftan kalıtım alırken property, method vs gibi bazı kullanmayacağı özellikler kazanmamalıdır. Eğer böyle bir duruma sebep olacak bir inheritance işlemi var ise alt sınıfın kullanmadığı fonksyon, metod vs. için farklı bir çözüm (sadece ihtiyacı olan özelliklerin bulunduğu başka bir base class vs. gibi) düşünülmelidir çünkü alt sınıf üst sınıfın bütün özelliklerini tam anlamıyla kullanamadığından yer değiştirdiklerinde aynı davranışı gösteremeyeceklerdir ve Liskov'a ters düşen bir durumdur.

 

Örnek uygulama üzerinden ilerleyelim, case şu şekilde olsun ; Bir tane Bank isminde bir class ve bu class içersinde Withdraw adında ara çekm işlemini yapan bir virtual metod olsun, 2 tanede hesap tanımı yapalım. Bank objesini kullanarak bu hesaplardan para çekme işlemini yapalım ve tabi ki bunu LSP kurallarına uyarak yapmaya çalışalım.  

 

  public class Bank
    {
        public void Withdraw(Account acc, int amount)
        {
            acc.Withdraw(acc.Id, amount);
        }
    }

    public class Account
    {
        public Account(int AccountId)
        {
            this.Id = AccountId;
        }

        public virtual int Id { get; set; }

        public virtual void Withdraw(int accountId, int amount)
        {
            Console.WriteLine("In base withdraw");
        }
    }

    public class SavingAccount : Account
    {
        public SavingAccount(int savingAccountId) : base(savingAccountId)
        {

        }

        public override void Withdraw(int accountId, int amount)
        {
            Console.WriteLine("In SavingAccount withdraw");
        }
    }

    public class CurrentAccount : Account
    {
        public CurrentAccount (int currentAccountId) : base(currentAccountId)
        {

        }

        public override void Withdraw(int accountId, int amount)
        {
            Console.WriteLine("In CurrentAccount withdraw");
        }
    }

 Şimdide Main fonksiyonu içerisinde bu yazmış olduğumuz class'ları kullanarak para çekme işini yapalım.

 using System;

    public class Program
    {
        static void Main(string[] args)
        {
            Bank bank = new Bank();
            Account acc = new Account(1);
            bank.Withdraw(acc, 100);
        }
    }

Yukarıda ki örnekte olduğu gibi Bank class'ı içerisinde bulunan Withdraw metodu bir Account objesini parametre olarak alıyordu. İlk kullanımda bir adet acc isminde Account oluşturup Withdraw metoduna verdik ve acc de bulunan Withdraw metodunu kullanarak para çekme işlemini yaptık.

Ancak LSP bize ne diyordu ; Alt sınıflardan oluşturulan nesneler üst sınıfların nesneleriyle yer değiştirdiklerinde aynı davranışı göstermek zorundadırlar. Aşağıdaki örnekte de LSP ye uygun biçimde kullanımı göreceğiz

 using System;

    public class Program
    {
        static void Main(string[] args)
        {
            Bank bank = new Bank();

            Account saving = new SavingAccount(2); //LSP nin dediği gibi sub class'lar base class'larla yer değiştirdiklerinde aynı davranışı göstermeleri beklenir
            bank.Withdraw(saving, 100);
        }
    }

Yukarıda ki gibi ise saving  isminde bir SavingAccount oluşturduk ve bu objede Account'ın bir alt class'ı olduğu için onun yerine kullanılabilir durumda Account saving = new SavingAccount(2); dedik ve Bank class'ında bulunan Withdraw metoduna parametre olarak geçtik ve aynı ilk kullanımda yaptığımız gibi saving objesininde Withdraw metodunu çağırıp para çekme işlemini tamamladık.

 

İnternette Liskov için çoğunlukla Rectangle örneğini görmek mümkün, bende herkes onu yazmışken biraz daha farklı bir örnek ile ele alim istedim ve bu örnek üzerinden gittim umarım anlaşılır olmuştur :)

Open Closed Principle

SOLID prensipleri yazı serisinde daha öncesnde SOLID nedir ve Single Responsibility konularından bahsetmiştik. Şimdi ise sırada SOLID'in  "O" su olan Open-Closed Principle. Bu prensip 1988 yılında fransız akademist Bertrand Meyer tarafından ortaya atılıp şu şekilde tanımlandı;

"Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification."

Peki ne demek bu "gelişime açık değişime kapalı" sözü ? Şu bir gerçek ki bir kod yazacaksınız ve ilelebet hep aynı kalacak.. tamamen palavra. Hiç bir kod yıllar içinde değişime uğramadan kalamaz, sürekli olarak yeni bir feature ekleme durumu söz konusudur ve müşteri sürekli olarak "şunu da ekleyelim, bunu da ekleyelim, şu da olsun.." vs. vs. istekler hiçbir zaman bitmeyecektir. Bu tarz durumlar için Open-Closed bize yeni gelen değişiklik isteklerini projenize eklerken yazmış olduğunuz sınıfların genel davranışlarını değiştirmeden sadece birkaç ufak dokunuşla bu geliştirmeleri ekleyebileceğiniz class lar yazmamızı söylüyor. Yani Core koda gerekirse hiç müdahale etmeden yeni feature'ları ekleyin diyor. Sebebi ise mevcut koda yapılacak olan her müdahale yeni bug'ların çıkmasına neden olabilir ve buda yeniden test-analiz süreci demektir ki bunu hiçbir firma veya çalışan istemez. 

Örnek bir proje üzerinden ilerleyelim. Bad Example ve Better Example diye Open-Closed'u iki şekilde ele alalım.

 

Bad Example 

Örneğimiz şu şekilde olacak; Araba ve Otobüs üretimi yapan bir fabrikamız var diyelim. Bunun için bir tane aşağıdaki gibi Vehicle adında bir class'ımız olsun. Bu class içerisinde VehicleType isminde bir enum ve bu enum'ında Car-Bus diye 2 değeri olsun. Daha sonra Vehicle class'ından inherit olmuş Car ve Bus isminde 2 class oluşturalım ve constructor'larında gerekli enum değerlerini verelim.

Vehicle.cs

namespace BadExample
{
    public class Vehicle
    {
        public VehicleType VType { get; set; }
    }

    public enum VehicleType
    {
        Car,
        Bus
    }

    public class Car : Vehicle
    {
        public Car()
        {
            this.VType = VehicleType.Car;
        }
    }

    public class Bus : Vehicle
    {
        public Bus()
        {
            this.VType = VehicleType.Bus;
        }
    }
}

 

Şimdi ise bu araçları üretme kısmına geldi. VehicleFactory adında bir class oluşturalım ve bu class içerisinde ProduceVehicle isminde Vehicle  objesini parametre olarak alan bir metod olsun ve bu metoda verilen objedeki vehicleType enum değerine göre ilgili aracı üreten fonksiyonları çağırsın.

VehicleFactory.cs

namespace BadExample
{
    public class VehicleFactory
    {
        public void ProduceVehicle(Vehicle vehicle)
        {
            switch (vehicle.VType)
            {
                case VehicleType.Car:
                    ProduceCar((Car)vehicle);
                    break;
                case VehicleType.Bus:
                    ProduceBus((Bus)vehicle);
                    break;
            }
        }

        private void ProduceCar(Car car)
        {
            Console.WriteLine("Car Produced\n");
        }

        private void ProduceBus(Bus car)
        {
            Console.WriteLine("Bus Produced\n");
        }
    }
}

 

Şimdi sırada yazmış olduğumuz class'ları kullanarak araçları üretme işlemi var. Bunun için Program.cs class'ı aşağıdaki gibi olacak

Program.cs

namespace BadExample
{
    class Program
    {
        static void Main(string[] args)
        {
            VehicleFactory vf1 = new VehicleFactory();
            vf1.ProduceVehicle(new Car());
 
            VehicleFactory vf2 = new VehicleFactory();
            vf2.ProduceVehicle(new Bus());

            Console.ReadLine();
        }
    }
}

 Uygulamamızın Ekran çıktısı

 

Ne güzel araçlarımız ürettik kodumuz tıkır tıkır çalışıyor ve fabrika haftada 300 car-bus üretiyor. Peki patron çıldırdı dedi ki "Arkadaşlar 2 ay sonra kamyon üretmeye başlıyoruz, bütün altyapıyı hazır edin..". Hiç sorun değil nasıl olsa kodumuzu yazdık arabayı otobüsü nasıl ürettiysek kamyonu da öyle üretiriz deyip kamyon için aşağıdaki gibi kodlarımızı modify edip değişiklikleri yapalım.

1- İlk olarak => VehicleType enum'ına Truck diye yeni bir alan eklemeliyiz

 public enum VehicleType
    {
        Car,
        Bus,
        Truck//Truck enum değerini ekledik
    }

 

2- İkinci olarak => Vehicle class'ına Truck objesini oluşturma. Nasıl Car ve Bus için Vehicle'dan inherit olan class lar yazdık aynı şekilde Truck içinde bunu yapıyoruz

namespace BadExample
{
    public class Vehicle
    {
        public VehicleType VType { get; set; }
    }

    public enum VehicleType
    {
        Car,
        Bus,
        Truck
    }

    public class Car : Vehicle
    {
        public Car()
        {
            this.VType = VehicleType.Car;
        }
    }
    public class Bus : Vehicle
    {
        public Bus()
        {
            this.VType = VehicleType.Bus;
        }
    }

    public class Truck : Vehicle//Truck objesini tanımladık 
    {
        public Truck()
        {
            this.VType = VehicleType.Truck;
        }
    }
}

 

3- Üçüncü olarak =>  VehicleFactory class'ına  ProduceTruck class'ını ekleyip ProduceVehicle metoduna Truck için gerekli kontrolleri ekleyeceğiz

using System;

namespace BadExample
{
    public class VehicleFactory
    {
        public void ProduceVehicle(Vehicle vehicle)
        {
            switch (vehicle.VType)
            {
                case VehicleType.Car:
                    ProduceCar((Car)vehicle);
                    break;
                case VehicleType.Bus:
                    ProduceBus((Bus)vehicle);
                    break;
                case VehicleType.Truck://truck üretimi için logic kısmını yazdık
                    ProduceTruck((Truck)vehicle);
                    break;
            }
        }

        private void ProduceCar(Car car)
        {
            Console.WriteLine("Car Produced\n");
        }

        private void ProduceBus(Bus car)
        {
            Console.WriteLine("Bus Produced\n");
        }

        private void ProduceTruck(Truck truck) //truck üretimi yapan metodu ekledik
        {
            Console.WriteLine("Truck Produced\n");
        }
    }
}

 

4- Dördüncü olarak =>  Yazmış olduğumuz kodları kullanma vakti, Main fonksiyonda da Truck üretimi için gerekli metod çağrılarını yapacağız.

namespace BadExample
{
    class Program
    {
        static void Main(string[] args)
        {
            VehicleFactory vf1 = new VehicleFactory();
            vf1.ProduceVehicle(new Car());
            
            VehicleFactory vf2 = new VehicleFactory();
            vf2.ProduceVehicle(new Bus());
            
            VehicleFactory vf3 = new VehicleFactory();
            vf3.ProduceVehicle(new Truck());
            
            Console.ReadLine();
        }
    }
}

Uygulamamızın Ekran çıktısı

 

Kamyonumuzu ürettikkk. Patron mutlu alan mutlu satan mutlu.. Peki ama yeni bir araç yani Kamyon türünde bir Vehicle üretmek için neler yaptık öyle... Elimizi atmadığımız class kalmadı. Bu işlemi tam 4 farklı adımda halledebildik. Bu peki istediğimiz bir şey mi ?.. Ne diyordu Open-Closed prensibi "gelişime açık değişime kapalı" Peki biz ne yaptık; projede ki logic kısmı dahil her yere müdahale edip bir takım değişiklikler yaptık yani prensibe uygun hareket etmedik.

Peki nasıl olması gerekirdi ? Sadece Main fonksiyonunda bir parça kod ekleyip sorunu çözecek halimiz yok tabiki de ancak patron yarın tekrardan gelip "Motorsiklet üretmeye başlıyoruz, Bisiklet üretmeye başlıyoruz.." diye devam etme ihtimaline karşı daha generic ve core koda yani logic'in bulunduğu kod kısımlarına çok fazla dokunmadan bir kaç extended class yazarak yeni üretim işlemine başlıyor olmamız gerekirdi.

 

Better Example

Şimdi gelin Truck üretim işlemimizi biraz daha "Better than the previous example" sloganıyla yazalım 

İlk olarak Vehicle.cs ile başlayalım. Bu sefer Vehicle class'ımız abstract içerisinde bir adet abstract olarak tanımlı Produce() metodu var ve Car, Bus ve Truck objeleri de yine Vehicle class'ını implemente edip ve Produce metodunu kullanacaklar.

using System;

namespace GoodExample.MyFolder
{
    public abstract class Vehicle
    {
        public abstract void Produce();
    }

    public class Car : Vehicle
    {
        public override void Produce()
        {
            Console.WriteLine("Car Produced\n");
        }
    }
    
    public class Bus : Vehicle
    {
        public override void Produce()
        {
            Console.WriteLine("Bus Produced\n");
        }
    }

    public class Truck : Vehicle
    {
        public override void Produce()
        {
            Console.WriteLine("Truck Produced\n");
        }
    }
}

 

VehicleFactory.cs class'ımızda artık hiçbir if/else yada switch/case condition'ı bulunmicak. Bu class ta yine Vehicle tipinde parametre alan ProduceVehicle() adında bir metod olacak ve tek görevi parametre olarak aldığı abstract Vehicle'dan türeyen objeyi alıp Produce() metodunu çağırmak. 

namespace GoodExample.MyFolder
{
    public class VehicleFactory
    {
        public void ProduceVehicle(Vehicle vehicle)
        {
            vehicle.Produce();
        }
    }
}

 

Program.cs'ın diğer örnekte olduğu gibi aynısını kullanıcaz yani üretmek istediğimiz objenin instance'ını alıp üretime başlicaz.

using System;

namespace GoodExample
{
    class Program
    {
        static void Main(string[] args)
        {
            VehicleFactory vf1 = new VehicleFactory();
            vf1.ProduceVehicle(new Car());

            VehicleFactory vf2 = new VehicleFactory();
            vf2.ProduceVehicle(new Bus());

            VehicleFactory vf3 = new VehicleFactory();
            vf3.ProduceVehicle(new Truck());
            
            Console.ReadLine();
        }
    }
}

Uygulamamızın Ekran çıktısı

 

Şimdi içimizden şu soruyu sorabiliriz.. "Eeee noldu şimdi aynı işi yaptık, hedefimiz Truck üretimi yapmaktı Bad Example'da da yaptık Better Example'da da..". Cevabı açık ve net arkadaşlar 

  • Projemizi daha generic hale getirdip hiçbir condition'a bağımlı tutmadık,
  • Abstraction kullanarak projedeki bazı yerleri daha kolay geliştirebilir hale getirdik,
  • Core taraftaki kodu gelişime açık çok büyük köklü değişimlere kapalı hale getirdik,
  • Yeni bir üretim işlemi  başlasın Vehicle objesinden inherit olan class'ımızı olluşturup Produce metodunun içerisini doldurduktan sonra kolayca üretime başlayabilecek hale geldik,
  • Yani kısaca projemizi Open-Closed prensibine uygun hale getirdik

BadExample'da yaptığımız gibi yeni bir araç üretimine başlamak için yapmış olduğumuz o 4-5 farklı değişiklikten kurtulup BetterExample'da olduğu gibi sadece üretilecek olan nesneyi Vehicle objesinden inherit edilmiş şekilde tanımlayıp Main fonksiyonunda Üretim işlemini başlatmak yeterli olacaktır.

Hadi son olarak Motorcycle üretelim.  İlk olarak nesnemizi tanımlıyoruz 

    public class Motorcycle : Vehicle
    {
        public override void Produce()
        {
            Console.WriteLine("Motorcycle Produced\n");
        }
    }

Sonrasında Main fonksiyonunda üretime başla diyoruz

using System;

namespace GoodExample
{
    class Program
    {
        static void Main(string[] args)
        {
            VehicleFactory vf1 = new VehicleFactory();
            vf1.ProduceVehicle(new Motorcycle());


            Console.ReadLine();
        }
    }
}

 

That's it :)

 

Single Responsibility Principle

Daha önceki SOLID nedir yazısında bahsettiğim üzre sırasıyla SOLID prensiplerini örneklerle anlatmaya başlıyoruz. Bu yazıda SOLID'in S'si olan ilk prensip Single Responsibility 'den bahsedeceğim ve daha önce çalıştığım bir şirkette karşılaştığım Single Responsibility ile ilgili bir olaydan da bahsetmek istiyorum.

Bir gün şirkette bir bankacılık projesinde çalışırken Mobil tarafta geliştirme yapıyordum ve projedeki modüllerden birinin bitirilmesine 4-5 gün kalmıştı ancak WebService tarafı yetişemiyordu. WebService tarafı .Net ile geliştiriliyordu ve full-stack .Net'liğin vermiş olduğu sorumlulukla project manager'ımız benden 2-3 gün service tarafına destekte bulunmamı rica etti bende ok deyip tfs'ten projeyi çektim ve projeyi ayağa kaldırdıktan sonra ufaktan incelemeye başladım ve solution'da bulunan katmanlardan biri olan  "....Common" isimli projeyi açtım. Her projede olmazsa olmaz "CommonFunctions.cs" isimli class'ı açmamla kapatmam bir oldu. Class'ın tek suçu isminin başında "Common" olması ama o class'ı yazanlar sağ olsunlar aşağıya doğru scroll ederken VS resmen dondu. Class'ın içerisinde yanılmıyorsam 5 bin satıra yakın kod vardı.

Aslında böyle olmasının bence temel sebebi proje yaklaşık 3 yıllık bir projeydi ve her gelen tam anlamıyla projeye hakim olmadan geliştirmeye başlamış olsa gerek her şeyi bu class'a yazmışlar ve ortaya OCOP - One Class Oriented Programming (benim uydurmam) çıkmış. Bazı hesaplama metodları var, Resource manager metodları var, string format metodları var, obejct mapping metodları var... var oğlu var. İsminin başında "Common" olmasından kaynaklanan yetkiyle her şey bu class'ta.

Single Responsibility prensibi bu ve benzeri durumlar için var diyebiliriz. 

Tek bir soru ve cevap ile ;

  • Single Responsibility (tek sorumluluk) nedir ?
  • Her class'ın tek bir sorumluluk alanı olup  tek bir amaca hizmet etmesidir. 

şeklinde tanımlayabiliriz. Peki ne demek bu "Her class'ın tek bir sorumluluk alanı olup tek bir amaca hizmet etmesidir." cümlesi. 

Yazılmış olan her bir class kendi işinden sorumlu olup başka class'ların yapması gereken işlere karışmaması veya kendi sorumluluk alanına çıkmaması istenir. Çok küçük bir örnekle, toplama işlemlerini yapması beklenen class gidip de çıkartma işlemlerine yapmasın, çıkartma işlemlerini yapan başka bir class yazılır ve orda yapılması beklenir.

Daha güzel ve oop odaklı olan bir örnek üzerinden gidelim. Bir tane banka için proje geliştiriyor olalım (üstte bahsettiğim gibi değil tabi :) ) ve BankAccount adında bir class ve bu class içerisinde belli hesaplamalar sonucu tutulan field'lar bulunsun.

public class BankAccount
{
    public int AccountNumber { get; set; }
    public int AccountBalance { get; set; }
 
    public void SaveData()
    {
        //kayıt işlemlerini yapan metod
    }
}

Yukarıda ki class'a ilk baktığımızda sorunsuz güzel bir class gibi duruyor ancak SRP(Single Responsibility Principle) açısından baktığımızda SRP'yi ihlal ediyor. BankAccount class'ı içerisinde bulundurduğu methodlardan dolayı data management-calculations işlerinede karışmış durumda. Yani bu class içerisinde field'ları olan birbir obje görevimi görecek yoksa barındırdığı metodlar itibariyle bir nevi helper class'ımı olacak. Bu gibi sebeplerden dolayı class'ın aldığı sorumluluklar değişkenlik göstermektedir ve bu sorumlulukların bir arada karışıklıklara sebebiyet verebilir.

Peki ne yapacağız bu durum için ? Eğer BankAccount objemiz diğer bankalardaki hesaplar için de kullanılacağını varsayalım ve böyle bir durumda bir interface veya abstrack class oluşturmak en güzel çözüm olacaktır. 

public interface IBankAccount
{
    int AccountNumber { get; set; }
    int AccountBalance { get; set; }
}

 

Şimdi IBankAccount interface'inden implement olan BankAccount class'ını oluşturabiliriz.

public class CTBank : IBankAccount
{
    int _accountNumber;
    int _accountBalance;
 
    // IBankAccount Members
     public int AccountNumber
    {
        get
        {
            return _accountNumber;
        }
        set
        {
            _accountNumber = value;
        }
    }
 
    public int AccountBalance
    {
        get
        {
            return _accountBalance;
        }
        set
        {
            _accountBalance = value;
        }
    }
}

 

Kolay ve daha oop olacak şekilde class'ımızı oluşturduk. Şimdi sırada en başta BankAccount class'ında yazdğımız SaveData metodu için bir DataAccessLayer yazmak var. Bunun için ilk olarak yine bir interface yazalım ismi IDataService olsun.

public interface IDataService
{
    bool Save(IBankAccount account);
}

 

Bu noktaya kadar ne yaptık ? 

  1. BankAccount objesinde bulunan field'ları barındıran bir IBankAccount interface'i yazdık
  2. Bu interface'den implement olan BankAccount objesini tanımladık,
  3. DataAccess class'ı için IDataService adında bir interface tanımaldık,

Bu noktadan sonra geriye sadece DataService class'ını yazmak kalıyor. Bu class tahmin ettiğiniz gibi IDataService interfce'ini implemente edecek.

public class DataService : IDataService
{
    //IDataService Members
    public bool Save(IBankAccount account)
    {
        bool isSaved = false;
        try
        {
            //save data here 
            isSaved = true;
        }
        catch (Exception)
        {
            // error
            // log exception data
            isSaved = false;
        }
        return isSaved;
    }
}

 

Görüldüğü üzre projemiz ilk başta yazdığımız BankAccount class'ına kıyasla artık daha object-oriented ve daha yönetilip genişletilebilir hale gelmiş oldu. Tabi ki istenilen feature'a göre değişiklik gösterebilir ancak proje release olduktan sonra ki değişiklik isteklerine daha kolay ve bu değişiklerin daha hızlı entegre edilebilmesini sağlar hale geldi diyebiliriz.

SOLID Prensipleri

SOLID Candır !

SOLID'in geçmişine baktığımızda çokta uzağa gitmemize gerek yok. İlk olarak 2000'li yılların başında Michael Feathers tarafından ortaya atıldı ve sonrasında Robert C. Martin tarfından "first five principles of object-oriented programming and design" olarak "SOLID" adını aldı.

 

Neden SOLID'e ihtiyaç duyuldu ?

"Bir kere yazılan kod asla ilelebet aynı kalamaz..!" aslında SOLID'in ortaya çıkış sebebi bu cümlede gizlidir. Aylar-yıllar geçtikten sonra yazmış olduğumuz kodlara bir değişiklik yapmak istediğimizde bu değişikliği ne kadar efor sarf ederek ve ne kadar sürede yapacağız..? SOLID projeyi yazdıktan sonraki süreçte bu soruya en iyi şekilde cevap verebilmemizi sağlamak için var.

Şu gerçektir ki bazen 7 ay gibi bir efor harcanıp production'a alınmış bir proje için çok değil 6 ay sonra ki süreçte müşteri bir değişiklik veya extra modül istediğinde o modülün projeye eklenmesi neredeyse projeyi production'a almak için harcanan süre kadar sürebiliyor.

7 ay gibi bir sürede projeyi bitir canlıya al, canlıya alındıktan 6 ay sonra müşteri gelsin yeni bir feature istesin sonra müşteriye "benim bunun geliştirmesini yapmak için 6 aya daha ihtiyacım var.." dediğinizde müşteri heralde ufaktan şöyle olur "WTF...man ?"

SOLID prensiplerinin amacı aslında bizlerin daha iyi programlar yazmamızdan çok ileride istenecek değişimlere açık olup bu değişimlere en az eforla ayak uydurabilecek kodlar yazmamızı sağlamaktır. Nesne yönelimli programla yapıyorsak dünyada standart kabul edilen bu 5 prensibe uygun kodlar yazıyor olmamız gerekir.

 

 Peki nedir bu ilk 5 prensip ?

  • – Single-Responsibility Principle
  • – Open-Closed Principle
  • L  – Liskov Substitution Principle
  • I  – Interface Segregation Principle
  • D  – Dependency Inversion Principle

Her bir prensip için sonrasında ayrı ayrı blog'lar yazacağım ancak kısaca bir kaç kelimeyle şu şekilde özetleyebiliriz;

Single-Responsibility Principle

Bir class'ın sadece tek bir işi olmalı ve sadece o işten sorumlu olmalıdır. Örneğin Log tutmak için yaptığınız bir Logger class'ı sadece Log işlemlerinden sorumlu olmalıdır.

 

Open-Closed Principle

Uygulamada yazdığımız objeler yada entity'ler gelişime açık değişime kapalı olmalıdır. Örnek olarak çok if-else yazmaktan kaçının demek desek yanlış olmaz. Bir şekilde daha öcnesinde yazmış olduğunuz metodunuzu genişletmek istediğinizde interface yada abstract class'lardan faydalanarak daha yönetilebilir ve ihtiyaç duyulduğunda genişletilebilir kodlar yazmak gerekmektedir.

 

Liskov Substitution Principle

Liskov prensibi için kısaca yerine geçme prensibi diyebiliriz. Bir base class'tan türetilen class'ların yeri geldiğinde ihtiyaç halinde üst class'ların yerine de kullanabileceğini söylemekte.

 

Interface Segregation Principle

Client'lar ihtiyaç duymadıkları bir interface'i kullanmaya zorlanmamalı veya ihtiyaç duyduğu interface'e ait tek bir metod için bütün interface'in metodları implemente etmemelidir. Interface Segregation prensibi bu gibi durumlar için interface'lerinizi ayırın ve bir interface'e gerektiğinden fazla görev yüklemeyin der. Böylelikle client geliştirmesini yaparken ihtiyaç duymadığı hiç bir metodu implemente etmek zorunda kalmicaktır.

 

Dependency Inversion Principle

Bağımlılığı tersine çevirme prensibine göre base class'lar veya metodlar vs. alt seviyeli sınıflara veya metodlara bağımlı olmamalıdır ve alt class'larda yapılan bir değişiklik base class'ları etkilememelidir. Örnek olarak kısa bir süre önce gözlüklerimin camını değiştirmiştim ve nedendir bilinmez tam o sırada aklıma bu prensip gelmişti :)

Alt modül ; gözlük camı,

Üst modül ; gözlük

olsun. Gözlükçüye gittiniz ve camları değiştireceksiniz adam dedi ki "Camlar gözlüğe bağlı olduğundan camları değiştirmek için komple gözlüğü değiştirmeniz gerekmekte.."  . En iyi ihtimal adama "manyak mısın sen arkadaş.. " dersiniz. İşte bu prensip bu gibi durumlar için var. Alt modül üst modüle bağımlı olsun ama bağımlılık başımızın belası da olmasın tabi ki :)

 

Yazımız burada bitiyor ancak her bir prensip için ayrı ayrı blog yazıları yazıyor olacağım..just follow :)