Skip to content

SOLID Prensipleri: Kod Yazmanın SOLID Yolu

Yayınlandı:

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.

SOLID prensipleri

Ek Bilgiler

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.

  1. Fatura oluşturma.
  2. 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.

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.

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.

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.

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.

Kaynaklar

Wikipedia - SOLID!


Önceki Yazı
Katmanlı Yazılım Mimarisi: N-Layer Architecture
Sonraki Yazı
OOP Nedir? Temel Prensipler ve C# Uygulamaları