Esnek ve sürdürülebilir kod yazmak her geliştiricinin ulaşmak istediği bir hedeftir. Ancak bu hedefe ulaşmak, özellikle projeler büyüdükçe ve gereksinimler sürekli değiştikçe düşündüğümüzden daha zor hale gelebilir. Karmaşık kod yapıları ve yönetimi zor tasarımlar, yazılım projelerinin en büyük zorluklarından biridir. Tam bu noktada SOLID prensipleri devreye girerek, yazılım tasarımlarımızı daha sağlam ve güvenilir bir temele oturtmamıza yardımcı olur.
Peki, SOLID nedir? SOLID, yazılım dünyasında sürdürülebilirliği ve esnekliği artırmayı hedefleyen beş temel prensibin baş harflerinden oluşur. Bu yazıda, SOLID prensiplerinin temellerini, C# uygulamalarıyla birlikte nasıl hayata geçirilebileceğini ve bu prensiplerin kod kalitesine nasıl katkı sağladığını inceleyeceğiz.
Ek Bilgiler
- Robert C. Martin (bilinen adıyla: Uncle Bob) bu ilkeleri 2000 yılında yazdığı “Design Principles and Design Patterns” kitabında tanıtmıştır.
S - Single Responsibility Principle
Tek Sorumluluk İlkesi
SRP, bir sınıfın yalnızca tek bir sorumluluğu olması gerektiğini savunur. Bu, bir sınıfın yalnızca tek bir işi yapması ve bu işle ilgili tüm işlevleri içermesi anlamına gelir. Yani, bir sınıfın amacı net bir şekilde tanımlanmalı ve yalnızca bu amaca yönelik işlemleri gerçekleştirmelidir.
SRP’ye uygun olmayan örnek
Aşağıdaki örnekte bir sınıfın birden fazla sorumluluğu vardır.
- Fatura oluşturma.
- E-posta gönderme.
public class InvoiceManager
{
public void CreateInvoice()
{
Console.WriteLine("Invoice created.");
}
public void SendInvoiceByEmail()
{
Console.WriteLine("Invoice sent via email.");
}
}
SRP’ye uygun hale getirelim
Sorumlulukları farklı sınıflara ayırabiliriz.
public class InvoiceCreator
{
public void CreateInvoice()
{
Console.WriteLine("Invoice created.");
}
}
public class EmailSender
{
public void SendEmail(string recipient, string message)
{
Console.WriteLine($"Email sent to {recipient}: {message}");
}
}
O - Open/Closed Principle
Açık/Kapalı İlkesi
OCP, bir sınıfın gelişime açık, ancak değişime kapalı olması gerektiğini ifade eder. Yani, bir sınıfa yeni bir özellik eklemek gerektiğinde mevcut kodu değiştirmek yerine, var olan yapıyı koruyarak yeni özellikler eklenebilmelidir.
OCP’ye uygun olmayan örnek
Bir ödeme sistemi düşünelim.
- Aşağıdaki sisteme yeni bir ödeme yöntemi eklemek istediğimizde
ProcessPayment
metodunu değiştirmemiz gerekir.
public class PaymentProcessor
{
public void ProcessPayment(string paymentType)
{
if (paymentType == "CreditCard")
{
Console.WriteLine("Processing credit card payment...");
}
else if (paymentType == "PayPal")
{
Console.WriteLine("Processing PayPal payment...");
}
else
{
throw new Exception("Unsupported payment type.");
}
}
}
OCP’ye uygun hale getirelim
Sisteme yeni ödeme yöntemleri eklenebilme ihtimalini de düşünerek mevcut sınıfı değiştirmek yerine, interface
ve polimorfizm
kullanarak sınıf tasarımını şu şekilde yapabiliriz.
// Ortak bir arayüz tanımlıyoruz
public interface IPaymentMethod
{
void ProcessPayment();
}
// Kredi kartı ödemesi için sınıf
public class CreditCardPayment : IPaymentMethod
{
public void ProcessPayment()
{
Console.WriteLine("Processing credit card payment...");
}
}
// PayPal ödemesi için sınıf
public class PayPalPayment : IPaymentMethod
{
public void ProcessPayment()
{
Console.WriteLine("Processing PayPal payment...");
}
}
// Yeni ödeme yöntemleri için sınıf ekleyebiliriz (örn. Bitcoin)
public class BitcoinPayment : IPaymentMethod
{
public void ProcessPayment()
{
Console.WriteLine("Processing Bitcoin payment...");
}
}
// PaymentProcessor, yeni ödeme yöntemleri için değiştirilmesine gerek kalmadan çalışabilir
public class PaymentProcessor
{
public void ProcessPayment(IPaymentMethod paymentMethod)
{
paymentMethod.ProcessPayment();
}
}
L - Liskov Substitution Principle
Liskov'un Yerine Geçme İlkesi
LSP, türetilmiş sınıfların, temel sınıfların yerine kullanılabilmesini ifade eder. Başka bir deyişle, bir türetilmiş sınıf, temel sınıfın tüm özellik ve davranışlarını devralmalı ve temel sınıfın beklenilen işlevselliğini bozmadan çalışabilmelidir.
LSP’ye uygun olmayan örnek
Bir Employee
sınıfı düşünelim.
- Tam zamanlı çalışanların yıllık maaşı hesaplanabilir ancak genellikle stajyerlerin yıllık maaşı hesaplanmaz.
public class Employee
{
public string Name { get; set; }
public decimal MonthlySalary { get; set; }
public virtual decimal GetYearlySalary()
{
return MonthlySalary * 12;
}
}
public class FullTimeEmployee : Employee
{
// Tam zamanlı çalışan maaş alır ve yıllık hesaplanabilir, sorun yok.
}
public class Intern : Employee
{
public override decimal GetYearlySalary()
{
throw new NotImplementedException("Stajyerlerin genellikle yıllık maaşı hesaplanmaz.");
}
}
Employee emp = new Intern();
Console.WriteLine(emp.GetYearlySalary()); // HATA!
Burada problem şu: Intern
sınıfı, Employee
sınıfının yerine sorunsuzca kullanılamıyor çünkü yıllık maaş hesaplama işlemi stajyerler için mantıklı değil.
LSP’ye uygun hale getirelim
Bu problemi çözmek için Worker
adında Employee
sınıfından daha üst bir sınıf oluşturabiliriz.
public abstract class Worker
{
public string Name { get; set; }
}
public class Employee : Worker
{
public decimal Salary { get; set; }
public decimal GetYearlySalary()
{
return Salary * 12;
}
}
public class Intern : Worker
{
// Stajyerlerin yıllık maaşı olmadığı için buraya maaş hesaplama eklemiyoruz.
}
I - Interface Segregation Principle
Arayüz Ayrımı İlkesi
ISP, bir sınıfın ihtiyaç duymadığı arayüzlere bağımlı olmaması gerektiğini ifade eder. Yani, büyük ve kapsamlı arayüzler yerine, daha küçük, spesifik ve yalnızca ilgili işlevleri içeren arayüzler tanımlanmalıdır.
ISP’ye uygun olmayan örnek
Bir yazıcı sistemini ele alalım. Bazı yazıcılar sadece baskı yapabilirken, bazıları tarama veya faks gibi ek özelliklere sahiptir. Eğer aşağıdaki gibi büyük ve genel amaçlı bir arayüzü tanımlarsak bu ISP ilkesine uymaz.
- Sadece yazdırma özelliği olan bir yazıcı bu arayüzü uygulamak zorunda kalır. Ancak
Scan
veFax
metotlarını desteklemez.
public interface IMultiFunctionPrinter
{
void Print(string content);
void Scan(string content);
void Fax(string content);
}
ISP’ye uygun hale getirelim
Arayüzü daha küçük ve spesifik parçalara ayıralım.
public interface IPrinter
{
void Print(string content);
}
public interface IScanner
{
void Scan(string content);
}
public interface IFax
{
void Fax(string content);
}
public class SimplePrinter : IPrinter
{
public void Print(string content)
{
Console.WriteLine($"Printing: {content}");
}
}
public class AdvancedPrinter : IPrinter, IScanner, IFax
{
public void Print(string content)
{
Console.WriteLine($"Printing: {content}");
}
public void Scan(string content)
{
Console.WriteLine($"Scanning: {content}");
}
public void Fax(string content)
{
Console.WriteLine($"Faxing: {content}");
}
}
D - Dependency Inversion Principle
Bağımlılıkları Tersine Çevirme İlkesi
DIP, üst seviye modüllerin (yani sistemin ana işleyişini belirleyen bileşenlerin) alt seviye modüllere (detaylara) doğrudan bağımlı olmaması gerektiğini ifade eder. Bunun yerine, her ikisi de soyutlamalara (interface veya abstract class) bağımlı olmalıdır.
DIP’e uygun olmayan örnek
Aşağıda bir sipariş işleme sistemi örneği verilmiştir. Üst düzey sınıf olan OrderProcessor
, doğrudan alt düzey bir sınıf olan EmailNotifier
sınıfına bağımlıdır.
- Eğer başka bir bildirim yöntemi (örneğin, SMS) eklemek istersek,
OrderProcessor
sınıfını değiştirmemiz gerekir.
public class EmailNotifier
{
public void SendNotification(string message)
{
Console.WriteLine($"Email sent: {message}");
}
}
public class OrderProcessor
{
private readonly EmailNotifier _notifier;
public OrderProcessor()
{
_notifier = new EmailNotifier();
}
public void ProcessOrder(string order)
{
Console.WriteLine($"Processing order: {order}");
_notifier.SendNotification("Order processed successfully.");
}
}
DIP’e uygun hale getirelim
Soyut bir arayüz kullanarak bildirim işlevselliğini soyutlayalım. Artık OrderProcessor
sınıfı, belirli bir bildirim sınıfına değil, bir arayüze bağımlı olacaktır.
// Soyutlama
public interface INotifier
{
void SendNotification(string message);
}
// Alt seviye sınıflar
public class EmailNotifier : INotifier
{
public void SendNotification(string message)
{
Console.WriteLine($"Email sent: {message}");
}
}
public class SmsNotifier : INotifier
{
public void SendNotification(string message)
{
Console.WriteLine($"SMS sent: {message}");
}
}
// Üst düzey sınıf
public class OrderProcessor
{
private readonly INotifier _notifier;
public OrderProcessor(INotifier notifier)
{
_notifier = notifier;
}
public void ProcessOrder(string order)
{
Console.WriteLine($"Processing order: {order}");
_notifier.SendNotification("Order processed successfully.");
}
}
N-Layer Architecture hakkındaki yazımı okumak için tıklayınız.