Menü

Neominal

BT Hizmetleri ve Danışmanlığı

5 Mayıs 20264 dk okuma18 görüntüleme
Part 3/5: Liskov Substitution Principle

Şöyle bir senaryo düşünelim. Ekibinize yeni bir geliştirici katıldı. "Transfer işlemi herhangi bir Account üzerinde çalışıyor" diye anlattınız. Geliştirici kodu aldı, SavingsAccount ile test etti, güzel çalıştı. Sonra production'da FixedDepositAccount devreye girdi ve sistem patladı. Çünkü o hesap debit() çağrıldığında exception fırlatıyordu.

"Ama ben Account üzerinden çalıştığını söyledim" dediniz.

Evet, söylediniz. Ama söz tutulmadı.

Nedir bu prensip aslında?

Liskov Substitution Principle'ı Barbara Liskov 1987'de tanımladı ve şöyle dedi:

"Bir programda S tipi nesneler, T tipi nesnelerin yerine geçtiğinde programın doğruluğu bozulmamalı."

Daha sade bir dille: alt sınıf, üst sınıfın yerine geçtiğinde sistem aynı şekilde çalışmalı. Ekstra sürpriz olmamalı.

Account üzerinde çalışan her kod, SavingsAccount ya da CheckingAccount verildiğinde de aynı şekilde çalışmalı. Eğer çalışmıyorsa — kalıtımı yanlış kurmuşuz demektir.


❌ Önce yanlışı görelim:

Bir süre sonra sisteme vadeli hesap eklenmesi isteniyor. Geliştirici hızlıca şunu yazıyor:



abstract class Account {
    protected BigDecimal balance;

    Account(BigDecimal balance) { this.balance = balance; }

    abstract void debit(BigDecimal amount);

    BigDecimal getBalance() { return balance; }
}

// Vadeli hesap — para çekilemiyor, makul görünüyor
class FixedDepositAccount extends Account {

    FixedDepositAccount(BigDecimal balance) { super(balance); }

    @Override
    void debit(BigDecimal amount) {
        // Vadeli hesaptan çekim yapılamaz
        throw new UnsupportedOperationException("Vadeli hesaptan çekim yapılamaz!");
    }
}

Kulağa mantıklı geliyor. Vadeli hesaptan gerçekten para çekilemiyor. Ama şimdi şuna bakalım:

// Transfer servisi Account üzerinden çalışıyor
void transfer(Account source, Account target, BigDecimal amount) {
    source.debit(amount);   // FixedDepositAccount gelirse — patlıyor!
    target.credit(amount);
}

transfer() metodunu yazan kişi Account sözleşmesine güvendi. debit() çağrılabilir, diye. Ama FixedDepositAccount o sözleşmeyi ihlal etti. LSP kırıldı.

Bu tip hatalar production'da ortaya çıkıyor ve debug etmesi gereksiz yere zor oluyor.

✅ Şimdi doğrusunu yapalım

Sözleşmeyi bozmadan her alt sınıf kendi kuralını ekleyebilir — ama debit() her zaman çalışmalı.

// Soyut taban — ortak alan ve davranışlar burada
abstract class Account {
    protected BigDecimal balance;

    Account(BigDecimal balance) {
        this.balance = balance;
    }

    void credit(BigDecimal amount) {            // Para yatır — ortak davranış
        this.balance = this.balance.add(amount);
    }

    abstract void debit(BigDecimal amount);     // Alt sınıf uygular ama sözleşmeyi bozamaz

    BigDecimal getBalance() { return balance; }
}

Her alt sınıf ek kural koyuyor — ama debit() her zaman çalışıyor:

// Vadesiz hesap — overdraft destekli para çekimi
class CheckingAccount extends Account {
    private final BigDecimal overdraftLimit;    // Eksi bakiye limiti

    CheckingAccount(BigDecimal balance, BigDecimal overdraftLimit) {
        super(balance);
        this.overdraftLimit = overdraftLimit;
    }

    @Override
    void debit(BigDecimal amount) {             // Para çek — overdraft dahil
        BigDecimal available = balance.add(overdraftLimit);
        if (amount.compareTo(available) > 0)
            throw new IllegalStateException("Overdraft limiti aşıldı");
        balance = balance.subtract(amount);
    }
}

// Tasarruf hesabı — aylık çekim limitli para çekimi
class SavingsAccount extends Account {
    private int monthlyWithdrawals = 0;
    private static final int LIMIT = 6;        // Ayda en fazla 6 çekim

    SavingsAccount(BigDecimal balance) { super(balance); }

    @Override
    void debit(BigDecimal amount) {             // Para çek — limit kontrollü
        if (monthlyWithdrawals >= LIMIT)
            throw new IllegalStateException("Aylık çekim limitine ulaşıldı");
        if (amount.compareTo(balance) > 0)
            throw new IllegalStateException("Yetersiz bakiye");
        balance = balance.subtract(amount);
        monthlyWithdrawals++;
    }
}

Transfer servisi hâlâ Account üzerinden çalışıyor — ve artık hangi hesap gelirse gelsin güvenle çalışıyor:

void transfer(Account source, Account target, BigDecimal amount) {
    source.debit(amount);   // CheckingAccount da gelse, SavingsAccount da — çalışır
    target.credit(amount);
}

Kullanalım

List<Account> accounts = List.of(
    new CheckingAccount(new BigDecimal("500"), new BigDecimal("200")),
    new SavingsAccount(new BigDecimal("1000"))
);

for (Account acc : accounts) {
    acc.debit(new BigDecimal("100"));
    System.out.println("Bakiye: " + acc.getBalance());
}

// CheckingAccount → Bakiye: 400
// SavingsAccount  → Bakiye: 900

Hangi hesap gelirse gelsin debit() çalıştı, bakiye güncellendi. Sürpriz yok.


Peki vadeli hesap ne olacak?

"Ama vadeli hesaptan gerçekten para çekilemiyor" diyebilirsiniz. Haklısınız. O zaman cevap şu: vadeli hesap Account hiyerarşisine girmemeli.

Bir sonraki bölümde göreceğimiz Interface Segregation Principle tam burada devreye giriyor. Debitable arayüzünü yalnızca para çekilebilen hesaplara vereceğiz. Vadeli hesap Debitable'ı implement etmeyecek — boş metod yazmak ya da exception fırlatmak zorunda kalmayacak.

Her şeyin bir yeri var. Önemli olan doğru yere koymak.

LSP'yi test etmenin en kolay yolu

Şunu kendinize sorun: "Bu alt sınıfı, üst sınıfın beklediği her yere koyarsam sistem hâlâ çalışır mı?"

Eğer cevap "evet ama şu metodda exception fırlatıyor" ya da "evet ama şu durumda beklenmedik davranıyor" ise — LSP ihlal edilmiş demektir.

Kısaca

Alt sınıf üst sınıfın sözleşmesini miras alır. Ek kural koyabilir, ek kontrol ekleyebilir — ama sözleşmeyi ihlal edemez. debit() çağrılabilir olmalı, exception fırlatarak "ben bu işi yapmıyorum" diyemez.

Bunu sağladığınızda polimorfizm gerçek anlamda güvenilir hale gelir. Account gören her kod, hangi alt sınıf gelirse gelsin rahatça çalışır.