SOLID prensipleri, C#i Java, Python gibi OOP (Nesne Tabanlı Programlama) dillerindeki en önemli tasarım prensipleridir. Beş temel tasarım prensibinin (design principle) birleşiminden oluşur.
- S = Single Responsibility Principle (SRP) (Tek Sorumluluk İlkesi)
- O = Open/Closed Principle (OCP) (Açık/Kapalı İlkesi)
- L = Liscov Substitution Principle (LSP) (Liscov Değiştirme İlkesi)
- I = Interface Segregation Principle (ISP) (Interface Ayrıştırma İlkesi)
- D = Dependency Inversion Principle (DIP) (Bağımlılık Tersine Çevirme İlkesi)
SOLID kelimesi de bu beş temel prensiplerin baş harflerinden meydana gelmektedir.
SOLID prensiplerine programcılık tarafından baktığımız zaman anlaşılmazı biraz zor bir konu gibi gözüküyor. Bu nedenle bende basit bir kılavuz hazırlamak istedim umarım faydalı olabilirim.
1. Single Responsibility Principle (SRP) (Tek Sorumluluk İlkesi)
Bir sınıfın her zaman bir sorumluluğu olmalı ve onu değiştirmek için tek bir sebep olmalıdır.
Bu ilke, bir sınıfın değişmesi için asla birden fazla neden olmaması gerektiğini belirtir. Bu, sınıflarınızı, her sınıfın tek bir amacı olacak şekilde tasarlamanız gerektiği anlamına gelir.
Kısaca: Bir sınıfın değişmesi için tek bir nedeni olmalıdır.
Yanlış Uygulama
Employee sınıfının altında isçi bilgilerini tutan birkaç alan (field), hesaplamalar yapmak için özellikler (property) ve kaydetmek/güncellemek için metot bulunur.
public class Employee
{
private string fullName;
private string dateOfJoing;
private string annualSalaryPackage;
//Standard getters and setters methods
//Business logic
public double calculateEmployeeSalary { get; set; }
public double calculateEmployeeLeaves { get; set; }
public double calculateTaxOnSalary { get; set; }
//Data persistence logic
public void saveEmployee() { }
public void updateEmployee() { }
}
Bu sınıfa baktığımız zaman birbirine bağlı yapıları ve birden fazla görev olduğunu görüyoruz. Yani sınıfımızı değiştirmek için elimizde yeterli gerekçeler var.
Doğru Uygulama
Tek bir Employee sınıfını, belirli sorumluluklarına göre birden çok sınıfa ayırabiliriz.
public class Employee
{
private string fullName;
private string dateOfJoing;
private string annualSalaryPackage;
}
public class EmployeeService
{
public double calculateEmployeeSalary { get; set; }
public double calculateEmployeeLeaves { get; set; }
public double calculateTaxOnSalary { get; set; }
}
public class EmployeeDAO
{
public void saveEmployee() { }
public void updateEmployee() { }
}
Şimdi sınıfımızı esnek bir şekilde bağlı, bakımı kolay ve değiştirmek için tek bir nedene dayalı hale dönüştürdük.
2. Open/Closed Principle (OCP) (Açık/Kapalı İlkesi)
Bir Sınıf Genişleme için Açık (Open), Değişiklik için Kapalı (Closed) olmalıdır.
Kuralın “Kapalı (Closed)” kısmı, bir modül geliştirilip test edildikten sonra kodun yalnızca hataları düzeltmek için değiştirilmesi gerektiğini belirtir. “Açık (Open)” kısım, yeni işlevler sunmak için mevcut kodu genişletebilmeniz gerektiğini söylüyor.
Kötü Uygulama
EmployeeSalary sınıfının altında bulunan calculateSalary metodu maaşı çalışanın türüne göre hesaplar: Kadrolu (permament) ve Sözleşmeli (contract).
public Double calculateSalary(Employee employee, int day, double bonus)
{
double salary = 0;
if (employee.EmployeeType == "permanent")
{
salary = (employee.dailyWages * day) + bonus;
}
else if(employee.EmployeeType == "contract")
{
salary = (employee.dailyWages * day);
}
return salary;
}
Sorun: Gelecekte, yeni bir tür örneğin part-time (Yarı Zamanlı Çalışan) gelirse, çalışan türüne göre maaşı hesaplamak için kodun değiştirilmesi gerekir.
İyi Uygulama
Yeni bir EmployeeSalary adlı interface sunabilir ve Permanent (Kadrolu) ve Contractual (Sözleşmeli) çalışanlar için iki alt sınıf oluşturabiliriz.
public interface IEmployeeSalary
{
public double calculateSalary(Employee employee, int day);
}
public class permanentEmployeeSalary : IEmployeeSalary
{
public double bonus { get; set; }
public double calculateSalary(Employee employee, int day)
{
return (employee.dailyWages * day) + bonus;
}
}
public class contractEmployeeSalary : IEmployeeSalary
{
public double calculateSalary(Employee employee, int day)
{
return employee.dailyWages * day;
}
}
public class parttimeEmployeeSalary : IEmployeeSalary
{
public double calculateSalary(Employee employee, int day)
{
return employee.dailyWages * (day / 2);
}
}
Bu şekilde kodumuzu planlarsak artık yeni bir tür geldiğinde diğer metotlarımızı bozmadan, yeni bir alt sınıf oluşturulma yöntemiyle yolumuza devam ederiz. Bu durumda prensibimizin temel mantığı değişmeyecektir.
3. Liscov Substitution Principle (LSP) (Liscov Değiştirme İlkesi)
Child Sınıfları, kodumuzun davranışını bozmadan Parent Sınıfları ile değiştirilebilmelidir.
Bu ilke, temel sınıflara referanslar kullanan metotların, türetilmiş sınıfların nesnelerini bilmeden kullanabilmesi gerektiğini belirtir.
Örnek. “Kisi” ve “Ogrenci” sınıfları arasnda bir inheritance gerçekleştiğini varsayalım. Kisi’yi nerede kullanabiliyorsanız, Oğrenci’yi de kullanabilmelisiniz, çünkü Oğrenci, Kisi’nin bir alt sınıfıdır.
Kötü Uygulama
Aşağıda ki senaryoda Car adında bir Parent Sınıfımız ve bundan türeyen ElectricCar ve StandardCar adlı iki adem Child sınıfımız bulunmakta.
public class Car
{
public virtual void fuel() { }
public virtual void wheels() { }
public virtual void run () { }
}
public class ElectricCar: Car
{
public override void fuel()
{
throw new Exception("Not Supported");
}
public override void wheels()
{
//...
base.wheels();
}
public override void run()
{
//...
base.run();
}
}
public class StandardCar: Car
{
public override void fuel()
{
//...
base.fuel();
}
public override void wheels()
{
//...
base.wheels();
}
public override void run()
{
//...
base.run();
}
}
ElectricCar sınıfı, Car sınıfını referans alarak genişletir ancak elektrikli bir araç olduğundan dolayı olarak fuel() metodunu desteklemez. Bu yüzden LS ilkesi ihlal edilmiş olur.
İyi Uygulama
Parent olan Car Sınıfı daha spesifik özellik taşıyacak bir şekle dönüştürülür ve yakıt desteği için Car sınıfından miras alan yeni bir FuelCar adlı sınıf eklenir.
public class Car
{
public virtual void wheels() { }
public virtual void run() { }
}
public class FuelCar: Car
{
public virtual void fuel() { }
}
public class StandardCar: FuelCar
{
public override void fuel()
{
//...
base.fuel();
}
public override void wheels()
{
//...
base.wheels();
}
public override void run()
{
//...
base.run();
}
}
public class ElectricCar: Car
{
public override void wheels()
{
//...
base.wheels();
}
public override void run()
{
//...
base.run();
}
}
Artık StandardCar sınıfı FuelCar sınıfını referans alarak genişletir. ElectricCar sınıfı ise Car sınıfını referans alarak genişletir. Böylece LS ilkesi ihlal edilmemiş olur.
4. Interface Segregation Principle (ISP) (Interface Ayrıştırma İlkesi)
Interface yalnızca tüm child sınıflar için geçerli olan metotlara sahip olmalıdır.
Bu ilke, İstemcilerin (Client) kullanmadıkları Interface’lere bağımlı olmaya zorlanmaması gerektiğini belirtir. Bu, Interface’deki bağımlı sınıf tarafından görülebilen üye sayısının en aza indirilmesi gerektiği anlamına gelir.
İstemciye sunulan Interface, tümü metotları değil, yalnızca istemciyle ilgili metotları içermelidir. Aksi durumda bu tip metotları yeni bir Interface’e taşımanız gerekmektedir.
Kötü Uygulama
Vehicle adlı Interface, Otobüs, Araba vb. gibi tüm araçlar tarafından desteklenmeyen fly() metodunu içerir. Bu nedenle, Vehicle interface’inden kalıtım alan Bus Class’ı zorla sahte bir uygulama sağlaması gerekir.
Bu durumda ISP ilkesi ihlal edilmiş olur.
public interface IVehicle
{
public void accelerate();
public void applyBrakes();
public void fly();
}
public class Bus : IVehicle
{
public void accelerate()
{
}
public void applyBrakes()
{
}
public void fly()
{
//dummy implementation
throw new NotImplementedException();
}
}
public class Aeroplane : IVehicle
{
public void accelerate()
{
}
public void applyBrakes()
{
}
public void fly()
{
}
}
İyi Uygulama
fly() metodunu yeni bir Flyable interface’ine çekmek sorunu çözer. Artık Vehicle interface, tüm araçlar tarafından desteklenen metotlar içermektedir. Ve Aeroplane, uçabildiği için hem Araç hem de Flyable interface’ini uygular.
public interface IFlyable
{
public void fly();
}
public interface IVehicle
{
public void accelerate();
public void applyBrakes();
}
public class Bus : IVehicle
{
public void accelerate()
{
}
public void applyBrakes()
{
}
}
public class Aeroplane : IVehicle, IFlyable
{
public void accelerate()
{
}
public void applyBrakes()
{
}
public void fly()
{
}
}
5. Dependency Inversion Principle (DIP) (Bağımlılık Tersine Çevirme İlkesi)
Sınıf (Class), somut uygulamalar yerine soyutlamalara (interface ve abstract class) bağlı olmalıdır. Bu durum sınıflarımızın birbirinden ayrılmasını sağlar ve uygulama değişirse, soyutlama yoluyla ona atıfta bulunan sınıf değişmez.
Bu ilke şunları içerir:
- Yüksek seviyeli modüller, düşük seviyeli modüllere bağlı olmamalıdır. Her ikisi de abstract’lara dayanmalıdır.
- Abstract’lar ayrıntılara bağlı olmamalıdır. Detaylar abstract’lara bağlı olmalıdır.
Üst düzey modüllerin, alt düzey modüllerin somut Implement’lerden ziyade abstract’lara bağlı olmasını sağlayarak, serbest bir çift kodu geliştirmemize yardımcı olur.
Kötü Uygulama
Doğrudan somut sınıfa (SQLRepository) başvurduğumuz bir Service sınıfımız var.
Sorun: Sınıfımız artık SQLRepository ile sıkı bir şekilde bağlantılı, gelecekte NoSQLRepository’yi desteklemeye başlamamız gerekirse Service sınıfını değiştirmemiz gerekiyor.
public class SQLRepository
{
public void save()
{
}
}
public class NoSQLRepository
{
public void save()
{
}
}
public class Service
{
private SQLRepository repository = new SQLRepository();
public void save()
{
repository.save();
}
}
İyi Uygulama
Bir parent interface Repository oluşturun ve SQL ve NoSQL Repository’lerine bunu uygulayın.
Service sınıfı, Repository interface’i ifade eder, gelecekte NoSQL’i desteklememiz gerekirse, örneğini Service sınıfını değiştirmeden constructor’da (yapıcıda) geçirmemiz yeterlidir.
public interface Repository {
void save();
}
public class SQLRepository : Repository
{
public void save()
{
//...
}
}
public class NoSQLRepository : Repository
{
public void save()
{
//...
}
}
public class Service
{
private Repository repository;
public Service(Repository repository)
{
this.repository = repository;
}
public void save()
{
repository.save();
}
}
Hepsi bu kadar! Örneklerin kaynak kodlarına aşağıda ki github adresi üzerinden erişebilirsiniz. İyi çalışmalar…
alkanfatih/SOLIDPrenciple
Döküman: http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod