SOLID Prensipleri: Yazılım Tasarımında Kaliteyi Artırmanın 5 Temel Yolu

image

08 Aug 2024

Yazılım geliştirme sürecinde kaliteli, sürdürülebilir ve esnek kod yazmak büyük önem taşır. Bu noktada, SOLID prensipleri devreye girer. SOLID, yazılım geliştirme sürecinde dikkat edilmesi gereken beş temel tasarım ilkesini ifade eden bir kısaltmadır: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, ve Dependency Inversion. Bu prensipler, yazılımın daha okunabilir, daha az hata içeren ve değişime daha dayanıklı olmasını sağlar.

1. Single Responsibility Principle (SRP) - Tek Sorumluluk Prensibi

Tanım: Bir sınıfın yalnızca bir sorumluluğu olmalıdır. Başka bir deyişle, bir sınıf sadece bir işlevi yerine getirmekle sorumlu olmalıdır. Her bir sınıfın tek bir nedeni olmalıdır, yani değişikliğe uğraması için tek bir sebep olmalıdır.

Gerçek Dünya Örneği:

Bir restoran yönetim sistemi geliştirdiğinizi düşünün. Bu sistemde Order (Sipariş) sınıfı, siparişlerin alınıp işlenmesinden sorumlu olabilir. Ancak eğer bu sınıf aynı zamanda siparişlerin mutfak ekranında gösterilmesi veya fatura kesilmesi gibi işlemlerden de sorumlu olursa, bu sınıfın sorumlulukları fazla genişlemiş olur. Bu durumda, Order sınıfının sadece sipariş işlemleriyle ilgilenmesi, diğer görevlerin ise farklı sınıflara dağıtılması gerekir (örneğin, OrderDisplay ve InvoiceGenerator sınıfları).

Fayda: SRP, kodun modülerliğini artırır ve değişikliklerin kod üzerinde yayılma riskini azaltır. Her bir sınıfın belirli bir görevi olduğu için, bir değişiklik yapıldığında diğer sınıfların etkilenme olasılığı azalır.

2. Open/Closed Principle (OCP) - Açık/Kapalı Prensibi

Tanım: Bir yazılım varlığı genişletmeye açık, ancak değişikliğe kapalı olmalıdır. Yani yeni bir özellik eklemek için mevcut kodu değiştirmek yerine, yeni kodlar eklenmelidir. Bu, mevcut işlevselliği koruyarak yeni işlevler eklemeyi kolaylaştırır.

Gerçek Dünya Örneği:

Bir e-ticaret sitesinde ödeme işlemlerini yöneten bir sistem düşünün. PaymentProcessor adlı bir sınıf, kredi kartı ödemelerini işlemekten sorumlu olabilir. Ancak bu sınıfa yeni bir ödeme yöntemi (örneğin, PayPal) eklemek isterseniz, mevcut sınıfı değiştirmek yerine, PaymentProcessor arayüzünü kullanarak yeni bir PayPalProcessor sınıfı ekleyebilirsiniz. Böylece, kredi kartı ödemelerini işleyen kodu değiştirmeden, sistemi genişletmiş olursunuz.

interface PaymentProcessor {
    void processPayment(double amount);
}

class CreditCardProcessor implements PaymentProcessor {
    public void processPayment(double amount) {
        // Kredi kartı ödemesini işleme kodu
    }
}

class PayPalProcessor implements PaymentProcessor {
    public void processPayment(double amount) {
        // PayPal ödemesini işleme kodu
    }
}

Fayda: OCP, yazılımın esnekliğini artırır ve yeni özelliklerin eklenmesini kolaylaştırır. Mevcut sistemi değiştirmeden yeni işlevler ekleyebildiğiniz için hata yapma riskini azaltır.

3. Liskov Substitution Principle (LSP) - Liskov Yerine Geçme Prensibi

Tanım: Türemiş sınıflar, taban sınıfın yerine kullanılabilir olmalıdır. Yani, bir türemiş sınıf, taban sınıfının tüm işlevselliğini korumalı ve onu bozmayacak şekilde genişletmelidir. Bu, nesne yönelimli tasarımda miras ilişkilerinin doğru bir şekilde kurulmasını sağlar.

Gerçek Dünya Örneği:

Bir şekil çizim uygulaması düşünün. Bu uygulamada, bir Rectangle (Dikdörtgen) sınıfı ve bu sınıftan türeyen bir Square (Kare) sınıfı olabilir. Ancak, Square sınıfı, Rectangle sınıfının yerini alıyorsa (yani, bir Square nesnesi bir Rectangle nesnesi gibi davranıyorsa), bu sınıfların her ikisi de aynı davranışı sergilemelidir. Örneğin, bir dikdörtgenin genişliği ve yüksekliği bağımsız olarak ayarlanabilirken, bir karenin genişliği ve yüksekliği her zaman eşit olmalıdır. Bu durumda, Square sınıfı Rectangle sınıfının yerine kullanılamaz ve bu prensibe aykırı bir tasarım oluşur.

class Rectangle {
    protected int width;
    protected int height;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getArea() {
        return width * height;
    }
}

class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        this.width = width;
        this.height = width;
    }

    @Override
    public void setHeight(int height) {
        this.width = height;
        this.height = height;
    }
}

Fayda: LSP, kodun yeniden kullanılabilirliğini ve genişletilebilirliğini artırır. Ayrıca, beklenmeyen davranışlardan kaynaklanan hataların önüne geçer.

4. Interface Segregation Principle (ISP) - Arayüz Ayrımı Prensibi

Tanım: Bir sınıf, kullanmadığı metodları içeren arayüzleri uygulamaya zorlanmamalıdır. Bu, büyük ve genel arayüzler yerine, daha küçük ve özelleşmiş arayüzlerin oluşturulması gerektiğini savunur.

Gerçek Dünya Örneği:

Bir yazıcı yönetim sistemi düşünelim. Bu sistemde Printer adlı bir arayüz, print(), scan() ve fax() metodlarını içerebilir. Ancak, bazı yazıcılar sadece yazdırma işlevini destekleyebilir ve tarama veya faks gönderme yeteneklerine sahip olmayabilir. Bu durumda, BasicPrinter adlı bir sınıfın, kullanmadığı scan() ve fax() metodlarını uygulaması gereksiz olacaktır. Bu problemi çözmek için, arayüzleri daha küçük parçalara ayırmak mantıklıdır.

interface Printable {
    void print(Document d);
}

interface Scannable {
    void scan(Document d);
}

interface Faxable {
    void fax(Document d);
}

class BasicPrinter implements Printable {
    public void print(Document d) {
        // Yazdırma işlemi
    }
}

Fayda: ISP, sınıfların gereksiz metodları uygulamak zorunda kalmasını engeller ve kodun modülerliğini artırır. Ayrıca, arayüzlerin yeniden kullanılabilirliğini ve genişletilebilirliğini sağlar.

5. Dependency Inversion Principle (DIP) - Bağımlılıkların Tersine Çevrilmesi Prensibi

Tanım: Yüksek seviye modüller, düşük seviye modüllere bağımlı olmamalıdır. Her ikisi de soyutlamalara (arayüzlere veya abstract sınıflara) bağımlı olmalıdır. Bu prensip, bağımlılıkların tersine çevrilmesi olarak adlandırılır ve kodun daha esnek ve test edilebilir olmasını sağlar.

Gerçek Dünya Örneği:

Bir e-ticaret uygulamasında, sipariş işlemlerini yöneten bir OrderProcessor sınıfı düşünelim. Bu sınıf, ödeme işlemlerini gerçekleştirmek için doğrudan CreditCardPayment sınıfına bağımlı olabilir. Ancak, eğer bu bağımlılığı bir arayüz (örneğin, IPaymentService) ile soyutlarsanız, OrderProcessor sınıfı ödeme yönteminin ne olduğuna dair bilgiye sahip olmaz ve bu sayede farklı ödeme yöntemlerini kolayca ekleyebilirsiniz.

interface IPaymentService {
    void processPayment(double amount);
}

class CreditCardPayment implements IPaymentService {
    public void processPayment(double amount) {
        // Kredi kartı ödemesi işlemi
    }
}

class OrderProcessor {
    private IPaymentService paymentService;

    public OrderProcessor(IPaymentService paymentService) {
        this.paymentService = paymentService;
    }

    public void processOrder(double amount) {
        paymentService.processPayment(amount);
    }
}

Fayda: DIP, kodun bağımlılıklarını azaltır ve esnekliği artırır. Değişikliklerin daha az yerden etkilenmesini sağlar ve yazılımın daha kolay test edilmesine olanak tanır.