SOLID Yazılım Geliştirme Prensipleri

Başarılı bir yazılım geliştirme pratiği, sadece kodun çalışır durumda olmasından daha fazlasını gerektirir. Kodun okunabilir, genişletilebilir ve bakımı kolay olması, uzun vadede projenin başarısını sağlayacak unsurlardan biridir. İşte bu noktada SOLID prensipleri devreye girer.

SOLID, yazılım geliştirme dünyasında yaygın olarak kabul gören beş temel prensibi ifade eder. Her bir prensip, yazılım tasarımında belirli bir ilkeyi temsil eder ve uygulandığında daha esnek, daha anlaşılır ve daha sürdürülebilir kodlar elde edilmesini sağlar.

SOLID, Robert Cecil Martin (Bob Amca) tarafından ortaya atılan, nesne yönelimli programlamada yazılım tasarımının esnek , kolay anlaşılabilir ve sürdürülebilir hale getirmek için uyulması gereken kurallar bütünüdür.

Yazılım geliştirirken SOLID prensiplerine uymanız, Clean (Temiz) Kod yazma alışkanlığı sağlar.

SOLID Prensipleri

  1. (S)ingle Responsibility Principle (SRP: Tek Sorumluluk Prensibi )

  2. (O)pen/Closed Principle (OCP: Açık Kapalı Prensibi)

  3. (L)iskov ‘s Substitution Principle (LSP: Liskov’un Yerine Geçme Prensibi )

  4. (I)nterface Segregation Principle (ISP: Arayüz Ayrıştırma Prensibi )

  5. (D)ependency Inversion Principle (DIP: Bağımlılık Ters Çevirme Prensibi )

(S)ingle Responsibility Principle (SRP: Tek Sorumluluk Prensibi )

Single Responsibility Principle (Tek sorumluluk Prensibi) 'ne göre bir sınıfın , fonksiyonun yalnızca tek bir sorumluluğu olmalıdır. Bir method'a birden fazla iş vermek , o methodun sorumluluğunu arttırır. Sınıfı bir çok işten sorumlu tutmak yerine tüm methodları parçalara ayırıp bağımsız methodlar ile yapılmasını söyleyen bir prensiptir. Bazen uğraşmamak için bu prensibi göz ardı edebiliyoruz , bu küçük bir proje için belki sorun yaratmayabilir fakat on binlerce kodunuz olduğunu düşünürsek kod anlaşılamaz ve sürdürülemez hale gelecektir.

Tek bir sınıfta tek bir sorumluluk daha az bağımlılık sağlayacaktır.

Daha az sorumluluk daha yalın ve küçük yapılar oluşmasını sağlar. Bu sayede kod daha anlaşılabilir/okunabilir olacaktır.

public class User
{  
    // ...
    public void ChangeUserName()
    {
        //Kullanıcı adını değiştirir
    }

    public void ChangeEmailAddress()
    {
        //Email adresini değiştirir
    }

    public void SendEmail()
    {
        //Email gönderir
    }
    // ...
}

Yukarıdaki kullanım ne kadar normal gözükse bile yanlış bir kullanımdır. ChangeUserName ve ChangeEmailAddress, User sınıfının sorumlulukları olabilir ama SendEmail metodu sorumluluğun içinde değildir.

Bu şekilde bir kullanım ile SendEmail metodunun başka yerlerde kullanılabilirliğini engelledik.

public class User
{
	// ...
    public void ChangeUserName()
    {
        //Kullanıcı adını değiştirir
    }

    public void ChangeEmailAddress()
    {
        //Email adresini değiştirir
    }
    // ...
}

public class EmailHelper
{
	// ...
    public void SendAmail()
    {
        //Email gönderir
    }
    // ...
}

Yukarıdaki kod bloğu doğru bir kullanımdır. User sınıfına sadece kendi sorumluluğunu verdik ve email işlemlerini EmailHelper adındaki bir sınıfa verdik. Bu sayede EmailHelper başka yerlerde kolayla kullanılabilir ve daha anlaşılır ve kodumuzun kontrolü daha kolay hale geldi.

(O)pen/Closed Principle (OCP: Açık Kapalı Prensibi)

Geliştirmeye açık , Değiştirmeye kapalı

Sınıflarımız , fonksiyonlarımız değişikliğe kapalı ama yeni davranışların eklenmesine açık olmalıdır. Yani siz yeni bir davranış ekleyeceğiniz zaman daha önceki yazdığınız kodunuz üzerinde değişiklik yapmamanız gerekir.

public class MessageService 
{  
	public void sendMessage(string message) 
	{  
		// SMS ile mesaj gönderimi
	}
}

Örnek verecek olursak siz şuan müşterilerinize SMS ile bilgilendirme mesajı atıyorsunuz fakat belli bir süre zaman geçiyor ve siz artık SMS yerine Email yoluyla bilgilendirme yapmanız gerekiyor. Yukarıdaki koda göre bu değişikliği yapmak için SMS kodlarını değiştirip Email göndermek için gerekli kodları yazmanız gerekir ve bu yaklaşım Open Closed prensibine aykırı bir durumdur.

public interface INotificationService 
{
	void SendMessage(string message);
}

public class SMS : INotificationService
{
	public void sendMessage(string message)
	{
		// SMS gönderme ...
	}
}

public class Email : INotificationService
{
	public void sendMessage(string message)
	{
		// Email gönderme ...
	}
}

Kod bloğumuzu yukarıdaki gibi interface düzeyinde yazarak , önceki kod bloğunu değiştirmeden geliştirmeye açık hale getirebiliriz.

public class MessageService 
{
	//Şuanki Mesaj gönderim
	private INotificationService _notificationService = new SMS();
	_notificationService.sendMessage("Bu bir SMS mesajıdır.)
}

Yukarıdaki mantıkla yaklaşarak geliştirmesi gereken yerleri önceki kod bloğunu değiştirmeden yapabiliriz. Şuan SMS ile mesaj gönderiyoruz fakat bundan sonra Email ile mesajları göndermemiz gerekiyor , ne yapmalıyız ? Yapmamız gereken sadece new'lenen sınıfı değiştirmektir.

public class MessageService 
{
	//Şuanki Mesaj gönderim
	private INotificationService _notificationService = new Email();
	_notificationService.sendMessage("Bu bir Email mesajıdır.)
}

(L)iskov ‘s Substitution Principle (LSP: Liskov’un Yerine Geçme Prensibi )

Aynı objeden (class, interface) türeyen tüm sınıflar, birbirlerinin yerine kullanılabiliyor olmalıdır. Eğer böyle bir durum söz konusu değilse, nesneler birbirlerinin yerine geçtiğinde hatalar meydana gelebilir.

interface IBird 
{ 
	void Walk(); 
	void Fly(); 
}

Yukarıda bir interface tanımlanmış ve bu interface'i implemente eden sınıfların Walk ve Fly fonksiyonlarını implemente etmesi istenmiş. Her kuş yürüyebilir fakat her kuş uçabilir mi?

public class Serce : IBird
{
	public void Walk() 
	{ 
		Console.WriteLine("Serçe yürüyor."); 
	}

	public void Fly() 
	{ 
		Console.WriteLine("Serçe uçuyor."); 
	}
}

Yukarıdaki kodda da göründüğü gibi serçe yürüyebilir ve uçabilir. Serçe, IBird interface'ini implemente ettiğinde hiçbir sorun çıkmadı.

public class DeveKusu : IBird
{
	public void Walk() 
	{ 
		Console.WriteLine("Deve Kuşu yürüyor."); 
	}

	public void Fly() 
	{ 
		throw new InvalidOperationException("Deve Kuşu uçamaz.");
	}
}

Fakat DeveKusu sınıfı IBird interface'ini implemente ettiğinde hata fırlatır çünkü deve kuşu uçamaz.

static void Main()
{
	IBird bird = new Serce();

	bird.Walk();
	bird.Fly();
	// Sıkıntı yok her şey yolunda.

	bird = new DeveKusu();

	bird.Walk();
	bird.Fly(); // ? Patlama meydana gelecektir çünkü deve kuşu uçamaz.
}

Göründüğü gibi serçe için her şey yolundayken serçe yerine deve kuşunu kullandığımızda patlama meydana geliyor. Bu gibi durumlar Liskov Substitution (Yerine Geçme) Prensibi'ne aykırı bir durumdur.

Olması gereken durum, kuşlar için methodu ayırarak interface düzeyinde yazmaktır. Aslında bu prensibi ihlal edince Single Responsibility prensibini de ihlal etmiş oluyorsunuz. Çünkü tüm kuşlara birden fazla sorumluluk vermek yanlış bir kullanım olarak düşünülebilir. Bu methodları parçalamamız gerekir.

interface IWalkingBird 
{ 
	void Walk(); 
} 
interface IFlyingBird : IWalkingBird 
{ 
	void Fly(); 
} 
class Serce : IFlyingBird
{ 
	public void Walk() 
	{ 
		Console.WriteLine("Sparrow is walking"); 
	} 
	public void Fly() 
	{ 
		Console.WriteLine("Sparrow is flying"); 
	} 
} 
class DeveKusu : IWalkingBird 
{ 
	public void Walk() 
	{ 
		Console.WriteLine("Ostrich is walking"); 
	} 
}
static void Main() 
{ 
	IWalkingBird deveKusu = new DeveKusu(); 
	deveKusu.Walk(); // Bu, DeveKusu için çalışır 
	
	IWalkingBird serce1 = new Serce(); 
	serce1.Walk(); // Bu, Serce için çalışır 
	
	IFlyingBird serce2 = new Serce(); 
	serce2.Fly(); // Bu, sadece Serce için çalışır 
}

Düzeltilmiş kod yukarıdaki gibidir. Interface'in içini parçalayarak her interface'e kendi sorumluluğunu verdik. Böylece bu interfaceleri implemente eden sınıflar yer değiştirebilir duruma geliyor.

(I)nterface Segregation Principle (ISP: Arayüz Ayrıştırma Prensibi )

Interface Segregation Prensibi'ne göre interface öyle bir şekilde tasarlanmalı ki bu interface'i kullanan sınıfların sadece ihtiyaç olan methotları uygulamaları gereksin, ihtiyaçları olmayan metotları barındırmak zorunda olmasın.

Interfacelerin doğru parçalanmasıdır.

IAnimal interface'inde Fly() , Run(), Swim() methotları olması gibi. Bu yanlış bir parçalamadır çünkü her hayvan uçamaz , yüzemez. Dolayısıyla bu methodların içi boş kalacaktır. Bu Interface Segregation Prensibi'ne aykırı bir durumdur.

interface IAnimal
{
    void Run();
}

interface IFlyingAnimal : IAnimal
{
    void Fly();
}

interface ISwimmingAnimal : IAnimal
{
    void Swim();
}

Interface'ler yukarıdaki gibi parçalanmalı ve sadece ortak kullanabilecek metotları barındırmalıdır. Bu şekilde Hiçbir Animal yapamadığı gereksiz bir özelliği barındırmak zorunda kalmaz.

Aslında bakacak olursak çözüm yolları Liskov Prensibi ile oldukça benzerdir. Bu noktada kafanız karışmış olabilir.

Liskov Prensibi'nin amacı tutarlılıktır. Aynı nesneden türeyen sınıflar birbiriyle yer değiştirebilir olmalıdır.

Interface Segragation Prensibi'nin amacı ise bir interface'lerin tasarımında gereksiz bağımlılıkları ortadan kaldırır ve sınıfların sadece ihtiyaç duydukları metotları içeren interface'leri implement etmelerini sağlar.

(D)ependency Inversion Principle (DIP: Bağımlılık Ters Çevirme Prensibi )

Dependency Inversion Prensibi yüksek seviye modüllerin düşük seviye modüllere bağımlı olmaması gerektiğini, bunun yerine ikisinin de soyutlamalara (abstractions) bağlı olması gerektiğini söyler.

Bu prensip sayesinde esnek, bakımı kolay ve daha kolay test edilebilir bir koda sahip oluyorsunuz.

Örnek vermek gerekirse, mesaj göndermek için tasarladığımız bir sistem olduğunu düşünelim. MessageSender sınıfımız olsun ve bu sınıfın içerisinde SMS gönderilsin ve bu gönderim işlemi SmsService ile olsun. Burada üst seviye sınıf MessageSender, alt seviye sınıfımız ise SmsService'tir. Bu yapıda MessageSender, SmsService'e bağımlı durumdadır. Doğrudan MessageSender'ın içinde SmsService'i kullandığımız için MessageSender, SmsService'e bağımlı hale geliyor ve bu durum Dependency Inversion Prensibi'ne aykırı bir durumdur.

public class SmsService
{
	public void SendSms()
	{
		//SMS gönderme
	}
}

public class MessageSender
{
	private SmsService smsService;

	public MessageSender()
	{
		smsService = new SMSService;
	}

	public void SendMessage()
	{
		smsService.SendSms();
	}
}

Yukarıda görüldüğü gibi MessageSender içerisinde SmsService doğrudan new'lenmesi (new'lenmesinden ziyade class içerisinde başka class new'lenmesi), MessageService sınıfının SmsService sınıfına doğrudan bağımlı hale gelmesine sebep olur. Bu, sınıfın sadece SmsService'i kullanmak için tasarlanmış olmasına ve bu nedenle SmsService dışında başka bir şekilde kullanılamaz hale gelmesine yol açar. Bu durum, Dependency Inversion Prensibi'ne aykırıdır çünkü yüksek seviye modül olan MessageSender'ın düşük seviye modül olan SmsService'e doğrudan bağımlı olması gerekmeyip, soyutlamalara bağımlı olması gerekmektedir.


public interface IMessageSender()
{
	void SendMessage();
}

public class SmsService : IMessageSender
{
	public void SendMessage()
	{
		//SMS gönderme
	}
}

public class MessageSender
{
	private IMessageSender _messageSender;

	public MessageSender(IMessageSender messageSender)
	{
		_messageSender = messageSender;
	}

	public void SendMessage()
	{
		_messageSender.SendMessage();
	}
}

Yukarıdaki kod bloğunda Dependency Inversion Prensibi'nin doğru bir şekilde kullanımı yer almıştır. MessageSender'da kullanılacak sınıf soyutlaştırarak kullanılmış ve SmsService'e hiçbir bağımlılığı kalmamıştır.

Ek Kaynaklar

Last updated