SOLID ile Değişime Dayanıklı Mimari

SOLID ile Değişime Dayanıklı Mimari

SOLID ilkelerini değişim maliyeti, bağımlılık yönü, test edilebilirlik ve üretim kodunun sadeliği üzerinden değerlendirir. İlkelerin gereksiz soyutlama üretmeden nasıl uygulanabileceğini gösterir.

Yazılım tasarımındaki asıl sorun, kodun bugün çalışması değildir. Asıl sorun, yarın değişirken ne kadar geniş bir alanı etkileyeceğidir. Küçük bir gereksinim çok sayıda sınıfı değiştirmeyi gerektiriyorsa sistem kırılgandır. Bir değişiklik ilgisiz davranışları bozuyorsa bağımlılıklar yanlış kurulmuştur.

SOLID prensipleri bu sorunu beş yönden ele alır. Sorumlulukları ayırır. Yeni davranışlar için güvenli genişleme noktaları kurar. Kalıtım ilişkilerini davranışsal olarak sınırlar. Arayüzleri istemci ihtiyaçlarına göre düzenler. İş kurallarını teknik ayrıntılardan korur.

Bu prensipler mekanik kurallar değildir. Her sınıf küçük olmak zorunda değildir. Her sınıf için interface yazmak gerekmez. Her switch ifadesi kötü tasarım değildir. Amaç daha fazla sınıf üretmek değil, değişikliğin doğru yerde kalmasını sağlamaktır.

Single Responsibility Principle (Tek Sorumluluk Prensibi)

Tek Sorumluluk Prensibi, bir sınıfın yalnızca bir değişiklik nedenine sahip olması gerektiğini söyler. Bu ilke çoğu zaman "bir sınıf tek iş yapmalıdır" biçiminde açıklanır. Buradaki iş, tek bir method anlamına gelmez. Aynı amaç için birlikte çalışan birkaç method, tek sorumluluk içinde kalabilir. Önemli olan, sınıfın farklı aktörlerden ve farklı gereksinimlerden etkilenmemesidir.

Kullanıcı kaydı yaygın bir örnektir. Aşağıdaki sınıf kullanıcıyı doğrular, veritabanına kaydeder ve e-posta gönderir. Bu işlemler farklı nedenlerle değişir. Doğrulama kuralları iş gereksinimlerine bağlıdır. Kayıt işlemi veri saklama teknolojisine bağlıdır. E-posta gönderimi ise mesaj şablonuna ve iletişim altyapısına bağlıdır.

public final class UserService {
    public void register(final User user) {
        if (user.getEmail() == null || !user.getEmail().contains("@")) {
            throw new IllegalArgumentException("Invalid email");
        }

```text
System.out.println("User saved: " + user.getEmail());
        System.out.println("Welcome email sent: " + user.getEmail());
    }
}

public final class User {
    private final String name;
    private final String email;

public User(final String name, final String email) {
        this.name = name;
        this.email = email;
    }

public String getName() {
        return name;
    }

public String getEmail() {
        return email;
    }
}

Daha doğru tasarımda her değişiklik nedeni ayrı bir sınıfa taşınır. `UserRegistrationService` yalnızca kayıt sürecini yönetir. Doğrulama, veri saklama ve bildirim ayrı sorumluluklardır. Orkestrasyonun kendisi de geçerli bir sorumluluktur. Ancak iş ayrıntıları orkestrasyon sınıfına gömülmemelidir.

```java
public final class UserValidator {
    public void validate(final User user) {
        if (user.getEmail() == null || !user.getEmail().contains("@")) {
            throw new IllegalArgumentException("Invalid email");
        }
    }
}

```text
public final class UserRepository {
    public void save(final User user) {
        System.out.println("User saved: " + user.getEmail());
    }
}

public final class EmailService {
    public void sendWelcomeEmail(final User user) {
        System.out.println("Welcome email sent: " + user.getEmail());
    }
}

public final class UserRegistrationService {
    private final UserValidator validator;
    private final UserRepository repository;
    private final EmailService emailService;

public UserRegistrationService(final UserValidator validator, final UserRepository repository, final EmailService emailService) {
        this.validator = validator;
        this.repository = repository;
        this.emailService = emailService;
    }

public void register(final User user) {
        validator.validate(user);
        repository.save(user);
        emailService.sendWelcomeEmail(user);
    }
}

SRP ihlalinin yaygın sonucu, zamanla büyüyen `Manager`, `Helper`, `Processor` ve `Service` sınıflarıdır. Bu sınıflar veritabanı, iş kuralı, dosya erişimi ve ağ işlemlerini birlikte taşır. Testleri zorlaşır. Constructor bağımlılıkları artar. Ancak aşırı parçalama da çözüm değildir. Her methodu ayrı sınıfa taşımak kodu dağıtır. Doğru sınır, birlikte değişen davranışlara göre belirlenir.

## Open Closed Principle (Açık Kapalı Prensibi)

Açık Kapalı Prensibi, yazılım bileşenlerinin genişlemeye açık, değiştirilmeye kapalı olması gerektiğini belirtir. Buradaki kapalılık mutlak değildir. Kod hata düzeltmek veya temel davranışı değiştirmek için düzenlenebilir. İlkenin hedefi, yeni bir seçenek eklendiğinde kararlı kodun sürekli değiştirilmesini önlemektir.

Alan hesaplama örneğinde merkezî sınıf, şeklin gerçek türünü kontrol eder. Yeni bir şekil eklendiğinde aynı method değiştirilir. Şekil sayısı arttıkça koşullu yapı büyür. Hesaplayıcı, desteklediği bütün türleri bilmek zorunda kalır.

```java
public final class Rectangle {
    private final double width;
    private final double height;

```text
public Rectangle(final double width, final double height) {
        this.width = width;
        this.height = height;
    }

public double getWidth() {
        return width;
    }

public double getHeight() {
        return height;
    }
}

public final class Circle {
    private final double radius;

public Circle(final double radius) {
        this.radius = radius;
    }

public double getRadius() {
        return radius;
    }
}

public final class AreaCalculator {
    public double calculate(final Object shape) {
        if (shape instanceof Rectangle) {
            final Rectangle rectangle = (Rectangle) shape;
            return rectangle.getWidth() * rectangle.getHeight();
        }

if (shape instanceof Circle) {
            final Circle circle = (Circle) shape;
            return Math.PI * circle.getRadius() * circle.getRadius();
        }

throw new IllegalArgumentException("Unsupported shape");
    }
}

Daha uygun tasarımda değişken davranış `Shape` sözleşmesine taşınır. Her şekil kendi alanını hesaplar. `AreaCalculator` yalnızca ortak sözleşmeyi kullanır. Yeni bir üçgen veya elips eklendiğinde hesaplayıcı değişmez.

```java
public interface Shape {
    double area();
}

```text
public final class Rectangle implements Shape {
    private final double width;
    private final double height;

public Rectangle(final double width, final double height) {
        this.width = width;
        this.height = height;
    }

@Override
    public double area() {
        return width * height;
    }
}

public final class Circle implements Shape {
    private final double radius;

public Circle(final double radius) {
        this.radius = radius;
    }

@Override
    public double area() {
        return Math.PI * radius * radius;
    }
}

public final class Triangle implements Shape {
    private final double base;
    private final double height;

public Triangle(final double base, final double height) {
        this.base = base;
        this.height = height;
    }

@Override
    public double area() {
        return base * height / 2.0;
    }
}

public final class AreaCalculator {
    public double calculate(final List<Shape> shapes) {
        double total = 0.0;

for (int i = 0; i < shapes.size(); i++) {
            total += shapes.get(i).area();
        }

return total;
    }
}

OCP yalnızca kalıtımla uygulanmaz. Strategy, Decorator, Factory ve Adapter gibi desenler de genişleme noktası oluşturabilir. Fakat her değişiklik olasılığı için önceden interface yazmak doğru değildir. Değişim kümesi küçük ve kapalıysa `switch` daha sade olabilir. OCP, geleceği tahmin etmek değil, sık değişen davranışı kararlı koddan ayırmaktır.

## Liskov Substitution Principle (Liskov Yerine Geçme Prensibi)

Liskov Yerine Geçme Prensibi, bir alt türün üst türün kullanıldığı yerde programın doğruluğunu bozmadan kullanılmasını ister. Derleyicinin kalıtıma izin vermesi yeterli değildir. Alt sınıf, üst sınıfın davranışını da korumalıdır. Girdi koşulları, sonuç garantileri, hata davranışı ve nesne kuralları değişmemelidir.

Kare ve dikdörtgen örneği bu sorunu açık biçimde gösterir. Dikdörtgende genişlik ve yükseklik bağımsızdır. Karede ise iki kenar her zaman eşittir. `Square`, `Rectangle` sınıfından türetilirse setter davranışlarını değiştirmek zorunda kalır.

```java
public class Rectangle {
    private int width;
    private int height;

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

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

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

public final class Square extends Rectangle {
    @Override
    public void setWidth(final int width) {
        super.setWidth(width);
        super.setHeight(width);
    }

@Override
    public void setHeight(final int height) {
        super.setHeight(height);
        super.setWidth(height);
    }
}

Bir method, genişlik ve yüksekliğin bağımsız değişebileceğini varsayabilir. Normal dikdörtgen için sonuç 50 olur. Kare geçirildiğinde ikinci setter ilk değeri de değiştirir ve sonuç 25 olur. Alt sınıf, üst sınıfın beklenen davranışını bozmuştur.

```java
public final class ShapeService {
    public int resizeAndCalculate(final Rectangle rectangle) {
        rectangle.setWidth(10);
        rectangle.setHeight(5);
        return rectangle.area();
    }
}

Daha doğru çözüm, iki sınıfı yalnızca gerçekten ortak olan davranış altında birleştirmektir. Kare ve dikdörtgen birer şekildir. Fakat aynı mutable kenar sözleşmesine sahip değildir.

public interface Shape {
    int area();
}

```text
public final class Rectangle implements Shape {
    private final int width;
    private final int height;

public Rectangle(final int width, final int height) {
        this.width = width;
        this.height = height;
    }

@Override
    public int area() {
        return width * height;
    }
}

public final class Square implements Shape {
    private final int side;

public Square(final int side) {
        this.side = side;
    }

@Override
    public int area() {
        return side * side;
    }
}

LSP ihlalinin açık işaretleri vardır. Alt sınıfın method içinde `UnsupportedOperationException` fırlatması güçlü bir uyarıdır. Boş bırakılan override da aynı sorunu gösterir. İstemci gerçek türü `instanceof` ile kontrol etmek zorunda kalıyorsa ortak sözleşme zayıftır. Gerçek sistemlerde thread-safety, timeout, idempotency ve blocking davranışı da bu sözleşmenin parçasıdır.

## Interface Segregation Principle (Arayüz Ayırma Prensibi)

Arayüz Ayırma Prensibi, istemcilerin kullanmadıkları methodlara bağımlı olmamasını ister. Amaç her interface'i tek methoda indirmek değildir. Amaç, farklı yetenekleri gereksiz biçimde aynı sözleşmede toplamamaktır. Bir sınıfın bazı methodları boş bırakması veya desteklenmeyen işlem hatası üretmesi, arayüzün fazla geniş olduğuna işaret eder.

Çok işlevli yazıcı örneğinde tek bir `Machine` interface'i yazdırma, tarama ve faks işlemlerini birlikte sunar. Basit bir yazıcı yalnızca yazdırabilir. Buna rağmen diğer methodları da implement etmek zorunda kalır.

```java
public interface Machine {
    void print(Document document);
    void scan(Document document);
    void fax(Document document);
}

```text
public final class BasicPrinter implements Machine {
    @Override
    public void print(final Document document) {
        System.out.println("Printing");
    }

@Override
    public void scan(final Document document) {
        throw new UnsupportedOperationException();
    }

@Override
    public void fax(final Document document) {
        throw new UnsupportedOperationException();
    }
}

public final class Document {
    private final String content;

public Document(final String content) {
        this.content = content;
    }

public String getContent() {
        return content;
    }
}

Arayüzler yeteneklere göre ayrıldığında her sınıf yalnızca desteklediği davranışları uygular. Basit yazıcı `Printer` sözleşmesini kullanır. Çok işlevli cihaz üç sözleşmeyi birlikte uygulayabilir.

```java
public interface Printer {
    void print(Document document);
}

```text
public interface Scanner {
    void scan(Document document);
}

public interface Fax {
    void fax(Document document);
}

public final class BasicPrinter implements Printer {
    @Override
    public void print(final Document document) {
        System.out.println("Printing");
    }
}

public final class MultiFunctionPrinter implements Printer, Scanner, Fax {
    @Override
    public void print(final Document document) {
        System.out.println("Printing");
    }

@Override
    public void scan(final Document document) {
        System.out.println("Scanning");
    }

@Override
    public void fax(final Document document) {
        System.out.println("Sending fax");
    }
}

ISP, yalnızca okunabilirliği artırmaz. Yetki sınırlarını da daraltır. Salt okuma yapan bir servis, silme ve güncelleme yeteneklerini içeren geniş bir repository almamalıdır. Ancak aşırı ayrım da zararlıdır. Her method için ayrı interface üretmek, interface explosion antipattern'ine dönüşebilir. Doğru ayrım, istemcilerin birlikte kullandığı davranışlara göre yapılır.

## Dependency Inversion Principle (Bağımlılıkların Tersine Çevrilmesi Prensibi)

Bağımlılıkların Tersine Çevrilmesi Prensibi, yüksek seviyeli iş kurallarının düşük seviyeli teknik ayrıntılara doğrudan bağlanmamasını ister. Yüksek seviyeli modül sistemin ne yapacağını söyler. Düşük seviyeli modül bunun e-posta, SMS, veritabanı veya dosya sistemiyle nasıl yapılacağını belirler.

Aşağıdaki bildirim servisi somut sınıfları kendi içinde oluşturur. Bu nedenle yeni bir kanal eklendiğinde servis değiştirilir. Ayrıca test sırasında gerçek göndericileri değiştirmek zordur.

```java
public final class EmailSender {
    public void send(final String message) {
        System.out.println("Email: " + message);
    }
}

```text
public final class SmsSender {
    public void send(final String message) {
        System.out.println("SMS: " + message);
    }
}

public final class NotificationService {
    private final EmailSender emailSender = new EmailSender();
    private final SmsSender smsSender = new SmsSender();

public void notifyUser(final String message) {
        emailSender.send(message);
        smsSender.send(message);
    }
}

Daha doğru tasarımda yüksek seviyeli servis yalnızca `MessageSender` sözleşmesini bilir. Somut göndericiler dışarıda oluşturulur ve constructor ile verilir. Yeni bir kanal, mevcut servisi değiştirmeden eklenebilir.

```java
public interface MessageSender {
    void send(String message);
}

```text
public final class EmailSender implements MessageSender {
    @Override
    public void send(final String message) {
        System.out.println("Email: " + message);
    }
}

public final class SmsSender implements MessageSender {
    @Override
    public void send(final String message) {
        System.out.println("SMS: " + message);
    }
}

public final class PushNotificationSender implements MessageSender {
    @Override
    public void send(final String message) {
        System.out.println("Push notification: " + message);
    }
}

public final class NotificationService {
    private final List<MessageSender> senders;

public NotificationService(final List<MessageSender> senders) {
        this.senders = new ArrayList<MessageSender>(senders);
    }

public void notifyUser(final String message) {
        for (int i = 0; i < senders.size(); i++) {
            senders.get(i).send(message);
        }
    }
}

public final class Application {
    public static void main(final String[] args) {
        final List<MessageSender> senders = new ArrayList<MessageSender>();
        senders.add(new EmailSender());
        senders.add(new SmsSender());
        senders.add(new PushNotificationSender());

final NotificationService service = new NotificationService(senders);
        service.notifyUser("Order completed");
    }
}

DIP ile Dependency Injection aynı şey değildir. DIP, bağımlılık yönünü belirleyen prensiptir. Dependency Injection, bağımlılıkları dışarıdan verme tekniğidir. IoC container kullanmak zorunlu değildir. Küçük sistemlerde manual wiring daha açık olabilir. İş sınıfları container içinden servis aramamalıdır. Bu yaklaşım Service Locator antipattern'ine dönüşür ve gerçek bağımlılıkları gizler.

SOLID prensipleri aynı tasarım hedefini farklı yönlerden korur. SRP değişiklik nedenlerini ayırır. OCP yeni davranışlara güvenli alan açar. LSP türlerin davranışsal uyumunu korur. ISP istemciyi gereksiz yeteneklerden uzak tutar. DIP iş kurallarını teknik ayrıntılardan ayırır. İyi tasarım, en fazla sınıfı değil, en doğru sınırları üretir.
Bu sayfanın QR kodu