İçeriğe geç

Nesne Yönelimli Tasarım — Kapsülleme ve Kompozisyon

Orta 10 dk Çok sık karşılaşılır

Önce şunu oku: SOLID — Beş Harf, Tek Soru

30 saniyede özet

Nesne yönelimli tasarımın dört ilkesini saymak kolay. Günlük kodda ise iki karar verirsin: bu nesne neyi korumalı, ve bir şeyi yeniden kullanmak için ondan mı türeyeceğim, yoksa onu içime mi koyacağım?

Dört ilkeyi ezbere saymak kod yazarken pek yardım etmez. Günlük kodda iki karar verirsin: bu nesne neyi koruyor, ve yeniden kullanmak için kalıtım mı, kompozisyon mu.

Saklamak ile korumak arasındaki fark: bakiye eksiye düşebiliyor mu?
Adım adım oku
  1. Solda alan private ama bir setter'ı var: kapı kilitli görünüyor, anahtar ise herkeste.
  2. Dışarıdaki bir kod setBalance(-500) çağırır ve bakiye eksiye düşer. Kuralı koruyan kimse yok.
  3. Sağda nesnenin setter'ı yok; yalnızca withdraw davranışı var ve kuralı o bilir.
  4. withdraw(500) bakiye yetersiz diye reddedilir. Kural nesnenin içinde, dışarıdan bozulamaz.
  1. Bayt: Bütün alanları private yaptım, her birine getter ve setter ekledim. Kapsülledim!

  2. Sen: Harika, artık kimse alanlara dokunamaz.

  3. Bayt: Ama biri setBalance(-500) çağırdı ve hesap eksiye düştü!

  4. Bayt: Kapsülleme saklamak değil, korumaktır. Kuralı kim koruyor, ona bakalım.

Kapsülleme: saklamak değil, korumak

Yaygın okunuş şu: alanları private yap, getter/setter ekle. Bu kapsülleme değil, alanları daha uzun yoldan public yapmaktır.

Kapsüllemeyen kapsülleme
class Account {
private BigDecimal balance;
public BigDecimal getBalance() { return balance; }
public void setBalance(BigDecimal b) { this.balance = b; }
}
class Account {
private BigDecimal balance;
public BigDecimal balance() { return balance; }
public void withdraw(BigDecimal amount) {
if (amount.compareTo(balance) > 0) throw new InsufficientFunds();
balance = balance.subtract(amount);
}
}

Fark şurada: ilkinde bakiyeyi eksiye düşürmek çağıranın elinde. İkincisinde düşüremezsin — kural nesnenin içinde ve dışarıdan atlanamıyor.

Buna değişmez kuralNesne var olduğu sürece doğru kalması gereken şey. Kapsüllemenin ölçütü kaç alanın private olduğu değil, hangi kuralın dışarıdan bozulamadığıdır.Bir setter, koruduğu kural varsa onu atlanabilir kılar. Kuralı metodun içine koymak (withdraw gibi) nesneyi geçersiz duruma düşürmeyi imkânsız hâle getirir.Sözlükte gör → denir: nesne var olduğu sürece doğru kalması gereken şey. Kapsüllemenin ölçütü kaç alanın private olduğu değil, hangi kuralın dışarıdan bozulamadığıdır.

Kafam karıştı, daha basit anlat

Getter ve setter koymak kapıya kilit takıp anahtarı herkese dağıtmaktır. Asıl koruma, “para çek” gibi kuralı bilen bir metottur: bakiye eksiye düşemez.

Kural nerede yaşıyor?

Tohum 360355

Bakiye: 200 TL

    Oynat ya da adımla: her adımda bir çağıran para çeker.

    Hız
    Adım 0

    Şu an ne oldu?

    getBalance / setBalance

    Bakiye 200 TL. Üç çağıran sırayla para çekecek.

    Görevler0/3

    • Bakiyeyi eksiye düşüraçık

      İpucu

      Varsayılan ayarlar yeter.

    • Bakiyeyi koru ama kuralı üç ayrı yerde yazaçık

      İpucu

      Herkes hatırlasın.

    • Masraf işi unutsa bile bakiyeyi koruaçık

      İpucu

      Kuralı nesnenin içine taşı.

    Olay günlüğü (0)

    Henüz olay yok. Oynat veya adımla.

    1. Varsayılanla oynat. getBalance/setBalance: ATM ve uygulama kontrol etti, gece masraf işi unuttu ve bakiye -30 TL oldu.
    2. “Gece masraf işi de bakiyeye bakıyor”u aç. Bakiye korundu, ama kural artık üç ayrı yerde yazılı.
    3. Anahtarı kapat ve “withdraw” seç. Masraf işi unutsa bile withdraw reddetti; kural tek yerde.
    Hızlı kontrolBaşlangıç

    Kapsüllemenin ölçütü nedir?

    Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

    Soyutlama ile kapsülleme arasındaki fark nedir?

    Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

    Satır satır: kalıtım neyi devralır?

    extends yazmak kod devralmak değil, sözleşme devralmaktır. Üst sınıfın verdiği her sözü tutmak zorundasın — istemediklerini de.

    `Stack extends ArrayList` yazarsan LIFO garantisi korunur mu? Cevabı göster

    Korunmaz. ArrayList’in add(int index, E e) metodu da miras gelir ve çağıran onu kullanarak yığının ortasına eleman sokabilir.

    Devralınan istenmeyen sözler

    Yigin.java
    şu an çalışan satırclass Stack<E> extends ArrayList<E> {
    2 void push(E e) { add(e); }
    3 E pop() { return remove(size() - 1); }
    4}
    5
    6Stack<String> s = new Stack<>();
    7s.push("a");
    8s.push("b");
    9s.add(0, "araya girdim");
    10System.out.println(s.pop());

    Debug

    Adım 1/6

    tasarım Amaç kod tekrarından kaçınmak: ArrayList zaten dinamik dizi tutuyor, onu kullanalım.

    devralınan
    = ArrayList'in tüm public API'si
    Java 21UTF-8LF1:1

    Sol/sağ ok tuşlarıyla da gezebilirsin.

    Kural şu: alt tip gerçekten üst tipin bir çeşidiyse (“is-a”) ve onun her sözünü tutabiliyorsa kalıtım kullan. Sadece koda ulaşmak için kalıtım kullanmak, istemediğin bir sözleşmeBir metodun çağırana verdiği söz: neyi kabul eder, ne döndürür, hangi hataları atar. İmzada görünmeyen kısmı da sözleşmenin parçasıdır.Liskov ihlalleri neredeyse her zaman imzayı değil sözleşmeyi bozar — bu yüzden derleyici onları yakalayamaz.Sözlükte gör →yi de imzalamak demektir.

    Kafam karıştı, daha basit anlat

    extends bir evi eşyalarıyla birlikte almaktır. İstemediğin eşyalar da gelir, ve başkaları onları kullanabilir.

    Hızlı kontrolOrta

    `Stack<E> extends ArrayList<E>` yazmanın asıl sorunu nedir?

    Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

    Kompozisyon nerede kazanır?

    KalıtımKompozisyon
    Bağ ne zaman kurulurDerleme zamanı, sabitÇalışma zamanı, değiştirilebilir
    DevralınanÜst sınıfın tüm API’siYalnızca kullandığın metotlar
    Değişimin etkisiÜst sınıf değişirse alt sınıflar kırılabilirArayüz aynı kaldıkça etkilenmez
    TestÜst sınıfı da kurmak gerekirBağımlılığı sahteyle değiştirirsin
    Çoklu davranışJava’da tek kalıtımİstediğin kadar parça birleştirilir

    Bu yüzden varsayılan tercih kompozisyonBir nesnenin ihtiyacı olan davranışı devralmak yerine alan olarak tutması. Yalnızca kullandığın metotları alırsın, üst sınıfın tüm API'sini değil.Varsayılan tercih olmasının sebebi kalıtımın yanlış olması değil, geri alınmasının pahalı olması: bir hiyerarşiyi bozmak bir alanı silmekten çok daha zordur.Sözlükte gör →dur. Kalıtım yanlış olduğu için değil, geri alması pahalı olduğu için: bir hiyerarşiyi bozmak, bir alanı silmekten çok daha zordur.

    Bir kredi kartında görelim. Müşteri Classic karttan Gold’a geçtiğinde kart aynı kalır, yalnızca nakit avans ücretinin kuralı değişir.

    Derinleş · Kredi kartı: ücret kuralı içeride, değiştirilebilir 4 dosya · ~54 satır · ilk okumada atlayabilirsin
    Proje dosyaları

    src/main/java/com/bank/card/ FeePolicy.java Ücret kuralının sözleşmesi: tutarı alır, ücreti döner.

    src/main/java/com/bank/card/FeePolicy.java
    public interface FeePolicy {
    Money cashAdvanceFee(Money amount);
    }

    src/main/java/com/bank/card/ PercentageFeePolicy.java Yüzde ve alt sınırla çalışan bir ücret kuralı; her kart ürünü kendi değerleriyle kurar.

    src/main/java/com/bank/card/PercentageFeePolicy.java
    // Rate and minimum (in the card's currency) come from the product catalogue.
    public record PercentageFeePolicy(BigDecimal rate, BigDecimal minimum) implements FeePolicy {
    @Override
    public Money cashAdvanceFee(Money amount) {
    BigDecimal fee = amount.value().multiply(rate).setScale(2, RoundingMode.HALF_UP);
    return new Money(fee.max(minimum), amount.currency());
    }
    }

    src/main/java/com/bank/card/ CreditCard.java Kart, limit kuralını kendisi korur ve ücret kuralını bir alan olarak taşır; ürün değişince yalnızca o alan değişir.

    src/main/java/com/bank/card/CreditCard.java
    public final class CreditCard {
    private final String cardId;
    private final Money limit;
    private Money used;
    private FeePolicy fees;
    public CreditCard(String cardId, Money limit, FeePolicy fees) {
    this.cardId = cardId;
    this.limit = limit;
    this.used = Money.zero(limit.currency());
    this.fees = Objects.requireNonNull(fees);
    }
    // The invariant lives here: amount plus fee never takes the card past its limit.
    public Money cashAdvance(Money amount) {
    Money fee = fees.cashAdvanceFee(amount);
    Money afterThis = used.plus(amount).plus(fee);
    if (afterThis.isGreaterThan(limit)) {
    throw new LimitExceededException(cardId);
    }
    used = afterThis;
    return fee;
    }
    // Upgrade to Gold: same card, same debt, a different fee rule.
    public void changeProduct(FeePolicy newFees) {
    this.fees = Objects.requireNonNull(newFees);
    }
    }

    src/main/java/com/bank/card/legacy/ ClassicCard.java Şöyle de yazılabilirdi: her ürün için bir alt sınıf. Bak, Gold'a geçmek için kartı yeniden kurmak gerekiyor.

    src/main/java/com/bank/card/legacy/ClassicCard.java
    // Another way to write it: one subclass per product.
    public class ClassicCard extends Card {
    @Override
    protected Money cashAdvanceFee(Money amount) {
    return percentageWithMinimum(amount, CLASSIC_RATE, CLASSIC_MINIMUM);
    }
    }
    // GoldCard extends Card in the same way. The type is fixed when the object is created,
    // so an upgrade means building a new GoldCard and copying the card id and debt across.
    Hızlı kontrolOrta

    Her durumu, kalıtımın mı kompozisyonun mu doğru araç olduğuna göre ayır.

    Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

    Sınıflandırılmamış

    Kalıtım uygun

    Gerçek bir alt tip ve tutulabilen bir sözleşme var

      Kompozisyon uygun

      Yalnızca davranış veya kod kullanmak isteniyor

        Polimorfizm: çağıran bilmesin

        Polimorfizmin kazancı “aynı metot farklı davranır” değil. Kazanç, çağıranın hangi tip olduğunu bilmek zorunda olmaması.

        Ölçüt: çağıran değişiyor mu?
        // yeni bir kanal eklemek bu metodu açtırıyor
        if (type == EMAIL) sendEmail(m);
        else if (type == SMS) sendSms(m);
        // yeni bir kanal, yeni bir sınıf — bu satır hiç değişmez
        channel.send(m);

        Test basit: yeni bir tip eklediğinde çağıran kodu değiştirmen gerekiyor mu? Gerekmiyorsa doğru yerden bölmüşsündür.

        Kafam karıştı, daha basit anlat

        Çağıran kod karşısındakinin türünü sormamalı, sadece “gönder” demeli. Yeni bir kanal eklemek, çağıranı değiştirmeden mümkün olur.

        Hızlı kontrolOrta

        Polimorfizmi doğru kullandığını nasıl anlarsın?

        Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

        Tuzaklar

        Kansız (anemik) model. Her alanı getter/setter’lı bir sınıf ve bütün kuralları bilen ayrı bir Service. Bu nesne yönelimli değil; veri bir yerde, kural başka yerde.

        Çalışır, ama kural nerede diye aradığında her yerde arayacaksın. Kuralı, koruduğu verinin yanına koy.

        Derin hiyerarşi. Üç seviyeden sonra bir metodun nereden geldiğini bulmak için tüm zinciri okumak gerekir. Üstelik ortadaki bir sınıfı değiştirmek, altındaki her şeyi etkiler.

        Kalıtımı kod paylaşmak için kullanmak. Ortak kodun varsa onu bir alana veya yardımcı sınıfa koy. extends bir yeniden kullanım aracı değil, bir tip bildirimidir.

        Kurucuda override edilebilir metot çağırmak· istersen atla

        Üst sınıfın kurucusu, alt sınıfın override ettiği bir metodu çağırırsa o metot alt sınıfın alanları henüz atanmadan çalışır. Sonuç: beklenmedik null veya sıfır. Kurucudan yalnızca private, static veya final metot çağır.

        Kendini sına

        Şimşek turu1/5

        Her alana getter ve setter eklemek kapsüllemeyi tamamlar.

        Soru 1/3Orta

        Bu program ne yazdırır?

        Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.
        KurucuTuzagi.java
        1class Base {
        2 Base() { init(); }
        3 void init() { System.out.println("Base.init"); }
        4}
        5
        6class Child extends Base {
        7 private String name = "child";
        8 @Override void init() { System.out.println("Child.init: " + name); }
        9}
        10
        11new Child();
        Java 21UTF-8LF

        Aklında kalacak üç şey

        1. 1 Kapsülleme getter ve setter yazmak değil, nesnenin geçerli kalmasını sağlayan kuralı dışarı sızdırmamaktır. Her alana setter koyan sınıf hiçbir şeyi korumaz.
        2. 2 extends yazdığın an üst sınıfın verdiği her sözü tutmak zorundasın. Bu yüzden ilk tercih kompozisyondur.
        3. 3 Polimorfizmin kazancı, çağıranın ayrıntıyı bilmemesidir: yeni bir tip eklemek çağıran kodu değiştirmiyorsa doğru yerden bölmüşsündür.
        Sonraki kapı Farklı ekipler aynı soruna aynı çözümü bulunca ne olur? Çözüme bir ad verilir. Tasarım Kalıpları — Ne Zaman Kazandırır · 10 dk

        5 kart sonraki derste seni bekliyor

        0/5 kart bu dersten toplandı

        Bu dersin üstüne kurulanlar

        Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.