← Kursa Dön
📄 Text · 15 min

SOLID ve Tasarım Prensipleri

Neden Tasarım Prensipleri?

Kod yazmak kolay, iyi kod yazmak zor. Bir projenin başında her şey temiz görünür. Ama 6 ay sonra yeni özellik eklemek, bug düzeltmek, kodu anlamak kabus olabilir. Tasarım prensipleri bu kaosu önlemenin yolu.

Bunu bir şehir planlaması gibi düşün. Plansız büyüyen şehirler trafik çilesi yaşar. Ama iyi planlanmış şehirlerde yeni mahalle eklemek mevcut yapıyı bozmaz. Tasarım prensipleri, yazılımın şehir planı.

SOLID, beş temel tasarım prensibinin baş harflerinden oluşur. Robert C. Martin (Uncle Bob) tarafından popülerleştirildi. Bunlara ek olarak DRY, KISS ve dependency injection gibi kavramları da göreceğiz.

S — Single Responsibility Principle (Tek Sorumluluk)

"Bir sınıfın değişmek için yalnızca bir nedeni olmalıdır."

Her sınıf tek bir iş yapmalı. Eğer bir sınıfı değiştirmek için birden fazla neden varsa, o sınıf çok fazla sorumluluk taşıyor demektir.

// KÖTÜ — birden fazla sorumluluk
class Employee {
    String name;
    double salary;

    void calculatePay() { /* maaş hesapla */ }
    void saveToDatabase() { /* veritabanına kaydet */ }
    void generateReport() { /* rapor oluştur */ }
}

Bu sınıf üç farklı nedenden değişebilir: maaş hesaplama kuralları değişirse, veritabanı yapısı değişirse, rapor formatı değişirse. Üç sorumluluk = üç neden.

// İYİ — her sınıf tek sorumluluk
class Employee {
    String name;
    double salary;
}

class PayCalculator {
    double calculatePay(Employee emp) {
        return emp.salary;
    }
}

class EmployeeRepository {
    void save(Employee emp) {
        System.out.println("Saving " + emp.name + " to DB");
    }
}

class ReportGenerator {
    String generate(Employee emp) {
        return "Report for: " + emp.name;
    }
}

Şimdi her sınıfın tek bir değişme nedeni var. Rapor formatı değiştiğinde sadece ReportGenerator'a dokunursun, diğerleri etkilenmez.

O — Open/Closed Principle (Açık/Kapalı)

"Sınıflar genişletmeye açık, değiştirmeye kapalı olmalıdır."

Yeni davranış eklemek için mevcut kodu değiştirmemeli, yeni kod eklemelisin.

// KÖTÜ — her yeni şekil için kodu değiştirmek lazım
class AreaCalculator {
    double calculate(Object shape) {
        if (shape instanceof Circle c) {
            return Math.PI * c.radius * c.radius;
        } else if (shape instanceof Rectangle r) {
            return r.width * r.height;
        }
        // Yeni şekil? Buraya if-else ekle...
        return 0;
    }
}
// İYİ — yeni şekil eklemek mevcut kodu değiştirmez
interface Shape {
    double area();
}

class Circle implements Shape {
    double radius;
    Circle(double r) { this.radius = r; }

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

class Rectangle implements Shape {
    double width, height;
    Rectangle(double w, double h) { this.width = w; this.height = h; }

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

// Yeni şekil eklemek? Yeni sınıf yaz, AreaCalculator'a DOKUNMA
class Triangle implements Shape {
    double base, height;
    Triangle(double b, double h) { this.base = b; this.height = h; }

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

class AreaCalculator {
    double calculate(Shape shape) {
        return shape.area(); // Polimorfizm — if-else yok
    }
}

Polimorfizm, Open/Closed prensibin doğal uygulamasıdır.

L — Liskov Substitution Principle (Liskov İkame)

"Alt sınıf nesneleri, üst sınıf nesnelerinin yerine sorunsuz kullanılabilmelidir."

Eğer Dog extends Animal ise, Animal beklenen her yerde Dog kullanıldığında program bozulmamalı.

// KÖTÜ — Liskov ihlali
class Bird {
    void fly() {
        System.out.println("Flying...");
    }
}

class Penguin extends Bird {
    @Override
    void fly() {
        throw new UnsupportedOperationException("Penguins can't fly!");
    }
}
void makeBirdFly(Bird bird) {
    bird.fly(); // Penguin gelirse patlıyor!
}

Penguin bir Bird olmasına rağmen, Bird beklenen yerde kullanıldığında exception fırlatıyor. Liskov ihlali.

// İYİ — doğru hiyerarşi
class Bird {
    void eat() { System.out.println("Eating..."); }
}

class FlyingBird extends Bird {
    void fly() { System.out.println("Flying..."); }
}

class Penguin extends Bird {
    void swim() { System.out.println("Swimming..."); }
}

class Eagle extends FlyingBird { }

Şimdi Penguin uçamayan bir kuş, Eagle uçabilen bir kuş. Hiçbir yerde bozulma yok.

💡 İpucu: Liskov prensibini test etmek kolay: "Alt sınıfı, üst sınıfın kullanıldığı her yere koy. Program hala doğru çalışıyor mu?" Cevap hayırsa, hiyerarşi yanlış.

I — Interface Segregation Principle (Arayüz Ayrımı)

"Sınıflar kullanmadıkları metotlara bağımlı olmamalıdır."

Şişman (fat) interface'ler yerine küçük, odaklı interface'ler tercih et.

// KÖTÜ — şişman interface
interface Worker {
    void work();
    void eat();
    void sleep();
    void attendMeeting();
    void writeReport();
}

class Robot implements Worker {
    @Override public void work() { System.out.println("Working..."); }
    @Override public void eat() { /* Robot yemek yemez! */ }
    @Override public void sleep() { /* Robot uyumaz! */ }
    @Override public void attendMeeting() { /* Anlamsız */ }
    @Override public void writeReport() { /* Belki */ }
}

Robot eat() ve sleep() implement etmek zorunda kalıyor — anlamsız.

// İYİ — küçük, odaklı interface'ler
interface Workable {
    void work();
}

interface Feedable {
    void eat();
}

interface Reportable {
    void writeReport();
}

class Human implements Workable, Feedable, Reportable {
    @Override public void work() { System.out.println("Working"); }
    @Override public void eat() { System.out.println("Eating"); }
    @Override public void writeReport() { System.out.println("Writing"); }
}

class Robot implements Workable {
    @Override public void work() { System.out.println("Working 24/7"); }
    // Sadece ilgili interface'i implement ediyor
}

Her sınıf sadece kendisiyle alakalı interface'leri implement ediyor. Robot yemek yemek zorunda değil.

D — Dependency Inversion Principle (Bağımlılık Tersine Çevirme)

"Üst seviye modüller alt seviye modüllere bağımlı olmamalı. İkisi de soyutlamalara bağımlı olmalı."

Somut sınıflara değil, interface/abstract class'a bağlan.

// KÖTÜ — üst seviye, alt seviyeye bağımlı
class NotificationService {
    private EmailSender emailSender = new EmailSender(); // Somut bağımlılık!

    void notify(String message) {
        emailSender.sendEmail(message);
    }
}

NotificationService doğrudan EmailSender'a bağlı. SMS'e geçmek istersen tüm kodu değiştirmen lazım.

// İYİ — soyutlamaya bağımlı
interface MessageSender {
    void send(String message);
}

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

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

class NotificationService {
    private MessageSender sender; // Interface'e bağımlı!

    NotificationService(MessageSender sender) {
        this.sender = sender;
    }

    void notify(String message) {
        sender.send(message);
    }
}
// Email ile
NotificationService emailService = new NotificationService(new EmailSender());
emailService.notify("Hello!");

// SMS ile — NotificationService koduna dokunmadan!
NotificationService smsService = new NotificationService(new SmsSender());
smsService.notify("Hello!");

NotificationService artık MessageSender interface'ine bağlı. Hangi implementasyonun kullanılacağı dışarıdan (constructor'dan) belirleniyor.

SOLID Özet Tablosu

PrensipKısa AçıklamaAnahtar Kelime
SRPHer sınıf tek iş yapsınTek sorumluluk
OCPGenişletmeye açık, değiştirmeye kapalıPolimorfizm
LSPAlt sınıf, üst sınıfın yerine geçebilmeliDoğru hiyerarşi
ISPKüçük, odaklı interface'lerAyrıştırma
DIPSoyutlamalara bağlan, somuta değilInterface bağımlılığı

DRY — Don't Repeat Yourself

"Her bilgi parçası sistemde tek bir yerde ifade edilmeli."

Aynı kodu kopyala-yapıştır yapmak yerine, ortak bir yere çıkar.

// KÖTÜ — tekrar eden kod
class OrderService {
    void createOrder(Order order) {
        if (order.amount <= 0) {
            throw new IllegalArgumentException("Invalid amount");
        }
        // sipariş oluştur...
    }

    void updateOrder(Order order) {
        if (order.amount <= 0) {
            throw new IllegalArgumentException("Invalid amount");
        }
        // sipariş güncelle...
    }
}
// İYİ — ortak mantık tek yerde
class OrderService {
    private void validateOrder(Order order) {
        if (order.amount <= 0) {
            throw new IllegalArgumentException("Invalid amount");
        }
    }

    void createOrder(Order order) {
        validateOrder(order);
        // sipariş oluştur...
    }

    void updateOrder(Order order) {
        validateOrder(order);
        // sipariş güncelle...
    }
}

Validasyon kuralı değiştiğinde tek yeri güncellersen yeter.

⚠️ Dikkat: DRY'ı aşırıya kaçırma. İki kod parçası bugün aynı görünüyor diye birleştirmek her zaman doğru değil. "Şu an aynı" ile "kavramsal olarak aynı" farklı şeyler. Farklı nedenlerle değişecek kodları zorla birleştirmek, ileride daha büyük sorun yaratır.

KISS — Keep It Simple, Stupid

"En basit çözüm genellikle en iyisidir."

Gereksiz karmaşıklıktan kaçın. Gerekmiyorsa pattern ekleme, abstraction katma.

// KÖTÜ — aşırı mühendislik
interface IStringProcessorFactory {
    IStringProcessor createProcessor();
}

interface IStringProcessor {
    String process(String input);
}

class UpperCaseProcessorFactory implements IStringProcessorFactory {
    @Override
    public IStringProcessor createProcessor() {
        return new UpperCaseProcessor();
    }
}

class UpperCaseProcessor implements IStringProcessor {
    @Override
    public String process(String input) {
        return input.toUpperCase();
    }
}
// İYİ — basit ve yeterli
String result = input.toUpperCase();

İlk versiyon 4 sınıf/interface ile bir satırlık işi yapıyor. "Belki ileride lazım olur" diye aşırı soyutlama yapmak, KISS'e aykırı.

Dependency Injection (DI) Fikri

Dependency Injection, Dependency Inversion prensibinin pratiğe dökülmüş hali. Bir sınıfın bağımlılıklarını kendi oluşturmak yerine dışarıdan almasıdır.

Üç yöntemi var:

1. Constructor Injection (en yaygın):

class UserService {
    private final UserRepository repository;

    // Bağımlılık constructor'dan veriliyor
    UserService(UserRepository repository) {
        this.repository = repository;
    }

    void registerUser(String name) {
        repository.save(new User(name));
    }
}

2. Setter Injection:

class UserService {
    private UserRepository repository;

    void setRepository(UserRepository repository) {
        this.repository = repository;
    }
}

3. Interface Injection:

interface RepositoryAware {
    void setRepository(UserRepository repository);
}

Constructor injection en çok tercih edilen yöntem çünkü:

  • Bağımlılıklar açıkça görülür

  • Nesne oluşturulduğunda hazır olur

  • final yapılabilir → immutable

Spring Framework gibi DI container'ları bu işi otomatize eder — bağımlılıkları sen oluşturmazsın, framework sağlar.

// Spring ile (ileride göreceksin)
@Service
class UserService {
    private final UserRepository repository;

    @Autowired
    UserService(UserRepository repository) {
        this.repository = repository;
    }
}

DI'ın Faydaları

// DI olmadan — test etmek zor
class OrderService {
    private PaymentGateway gateway = new StripeGateway(); // Sıkı bağlı

    void processOrder(Order order) {
        gateway.charge(order.amount); // Gerçek ödeme alır!
    }
}

// DI ile — test etmek kolay
class OrderService {
    private final PaymentGateway gateway;

    OrderService(PaymentGateway gateway) {
        this.gateway = gateway;
    }

    void processOrder(Order order) {
        gateway.charge(order.amount);
    }
}
// Test'te sahte (mock) gateway kullan
class FakeGateway implements PaymentGateway {
    boolean charged = false;

    @Override
    public void charge(double amount) {
        charged = true; // Gerçek ödeme yok, sadece kayıt
    }
}

// Test
FakeGateway fake = new FakeGateway();
OrderService service = new OrderService(fake);
service.processOrder(new Order(100));
assert fake.charged; // Ödeme çağrıldı mı?

DI sayesinde gerçek ödeme sistemi olmadan test yapabiliyorsun. Bu, unit test'lerin temelidir.

Prensipleri Birlikte Kullanmak

Gerçek dünyada bu prensipler birlikte çalışır:

// Interface (ISP — küçük, odaklı)
interface OrderValidator {
    boolean isValid(Order order);
}

interface OrderPersistence {
    void save(Order order);
}

interface OrderNotifier {
    void notifyCustomer(Order order);
}

// Implementasyonlar (SRP — tek sorumluluk)
class AmountValidator implements OrderValidator {
    @Override
    public boolean isValid(Order order) {
        return order.amount > 0;
    }
}

class DatabasePersistence implements OrderPersistence {
    @Override
    public void save(Order order) {
        System.out.println("Saved to DB: " + order);
    }
}

class EmailNotifier implements OrderNotifier {
    @Override
    public void notifyCustomer(Order order) {
        System.out.println("Email sent for: " + order);
    }
}

// Servis (DIP — soyutlamalara bağımlı, DI ile alır)
class OrderService {
    private final OrderValidator validator;
    private final OrderPersistence persistence;
    private final OrderNotifier notifier;

    OrderService(OrderValidator v, OrderPersistence p, OrderNotifier n) {
        this.validator = v;
        this.persistence = p;
        this.notifier = n;
    }

    void placeOrder(Order order) {
        if (!validator.isValid(order)) {
            throw new IllegalArgumentException("Invalid order");
        }
        persistence.save(order);
        notifier.notifyCustomer(order);
    }
}

Bu kodda:

  • SRP: Her sınıf tek iş yapıyor

  • OCP: Yeni validator, notifier eklemek mevcut kodu değiştirmez

  • ISP: Küçük, odaklı interface'ler

  • DIP: OrderService somut sınıflara değil, interface'lere bağımlı

  • DI: Bağımlılıklar constructor'dan veriliyor

  • DRY: Tekrar yok

  • KISS: Gereksiz karmaşıklık yok

⚠️ Dikkat: Bu prensipleri dogma olarak uygulama. Küçük bir script için SOLID'in her harfini uygulamak overkill. Prensipleri anla, duruma göre uygula. "Kural, kuralı bilenlerin kırması içindir."


Özet

  • Single Responsibility: Her sınıfın değişmek için tek bir nedeni olmalı; sorumlulukları ayır.

  • Open/Closed: Mevcut kodu değiştirmeden yeni davranış ekleyebilmelisin; polimorfizm bunun anahtarı.

  • Liskov Substitution: Alt sınıf, üst sınıfın yerine sorunsuz geçebilmeli; aksi halde hiyerarşi yanlış.

  • Interface Segregation: Küçük, odaklı interface'ler kullan; sınıfları kullanmadıkları metotlara bağımlı kılma.

  • Dependency Inversion + DI: Somut sınıflara değil soyutlamalara bağlan; bağımlılıkları dışarıdan (constructor ile) al.

  • DRY (tekrar etme), KISS (basit tut) prensipleri SOLID'i tamamlar; hepsini duruma göre dengeli uygula.