Aikido

Sürdürülebilir ve esnek kod için kalıtım (inheritance) yerine kompozisyon nasıl tercih edilir

Sürdürülebilirlik

Kural
Kalıtım yerine bileşimi tercih
edin. Derin kalıtım hiyerarşileri sıkı bağımlılık
oluşturur ve kodu anlamayı ve bakım yapmayı zorlaştırır.

Desteklenen diller: 45+

Giriş

Kalıtım, üst ve alt sınıflar arasında sıkı bir bağımlılık oluşturarak kodu kırılgan ve değiştirmesi zor hale getirir. Bir sınıf davranışı miras aldığında, üst sınıfın uygulama detaylarına bağımlı hale gelir. Metotları geçersiz kılan (override) ancak yine de super çağıran alt sınıflar özellikle sorunludur; kendi mantıklarını miras alınan davranışla, üst sınıf değiştiğinde bozulacak şekillerde karıştırırlar. Bileşim (composition), nesnelerin diğer nesnelere delegasyon yapmasına izin vererek bu sorunu çözer; böylece gevşek bağ (loose coupling) ve net bir sorumluluk ayrımı sağlar.

Neden önemli?

Karışık sorumluluklar ve sıkı bağımlılık: Kalıtım, ilgili olmayan sorumlulukları aynı sınıf hiyerarşisinin içine zorla sokar. Bir ödeme işlemcisinden miras alan yinelenen ödeme sınıfı, zamanlama mantığını ödeme işlemleriyle karıştırır. Şunu çağırıp super.process() ardından kendi davranışınızı eklemeniz gerektiğinde, üst sınıfın uygulamasına sıkı sıkıya bağımlı olursunuz. Üst sınıfın process() metodu değiştiğinde, alt sınıf beklenmedik şekillerde bozulur.

İstenmeyen davranışların kalıtılması: Alt sınıflar, üst sınıflarından ihtiyaç duymadıkları veya farklı bir implementasyon gerektiren metodlar dahil her şeyi miras alır. Yinelenen (recurring) bir ödeme, refund() tek seferlik ödemeler için tasarlanmış mantığı miras alır, ancak abonelik iadelerinin işleyişi farklıdır. Ya metodları ezersiniz (override) ve kargaşaya yol açarsınız ya da uygunsuz kalıtım davranışıyla yaşamak zorunda kalırsınız.

Kırılgan üst sınıf (fragile base class) problemi: Üst sınıflarda yapılan değişiklikler tüm alt sınıfları etkiler. Ödemelerin nasıl işlendiğini CreditCardPayment değiştirmek, RecurringCreditCardPayment yapılan değişiklik zamanlama ile ilgisiz olsa bile sınıfını etkiler. Bu durum, hangi alt sınıfların bozulacağını öngöremeyeceğiniz için yeniden yapılandırmayı (refactoring) tehlikeli hale getirir.

Test etme karmaşıklığı: Kalıtım hiyerarşisinin derinliklerindeki sınıfları test etmek, üst sınıfın davranışını anlamayı gerektirir. Yinelenen ödeme zamanlamasını test etmek için kredi kartı işleme mantığı, Stripe API çağrıları ve doğrulama ile de uğraşmanız gerekir. Kompozisyon ise basit bir sahte (mock) ödeme nesnesiyle zamanlamayı test etmenize olanak tanır.

Kod örnekleri

❌ Kural dışı:

class Payment {
    constructor(amount, currency) {
        this.amount = amount;
        this.currency = currency;
    }

    async process() {
        throw new Error('Must implement in subclass');
    }

    async refund() {
        throw new Error('Must implement in subclass');
    }

    async sendReceipt(email) {
        // All paymet types need receipts
        await emailService.send(email, this.buildReceipt());
    }
}

class CreditCardPayment extends Payment {
    constructor(amount, currency, cardToken, billingAddress) {
        super(amount, currency);
        this.cardToken = cardToken;
        this.billingAddress = billingAddress;
    }

    async process() {
        await this.validateCard();
        return await stripe.charges.create({
            amount: this.amount * 100,
            source: this.cardToken,
            currency: this.currency
        });
    }

    async refund() {
        await this.validateRefund();
        return await stripe.refunds.create({ charge: this.chargeId });
    }

    async validateCard() {
        // Card validation logic
    }
}

// Problem: RecurringCreditCardPayment's main concern is dealing with scheduling
// and not the actual payment
class RecurringCreditCardPayment extends CreditCardPayment {
    constructor(amount, currency, cardToken, billingAddress, schedule) {
        super(amount, currency, cardToken, billingAddress);
        this.schedule = schedule;
    }

    async process() {
        // Problem: Need to override parent's process() but also use it
        await super.process();
        await this.scheduleNextPayment();
    }

    async scheduleNextPayment() {
        // Subscription scheduling
    }

    // Problem: Inherits refund() from parent but refunding
    // subscriptions needs different logic
}

Neden yanlış: RecurringCreditCardPayment ödeme işleme mantığını miras alır ancak asıl odak noktası ödemeler değil zamanlamadır. Metodu çağırıp super.process() zamanlama davranışı ile sarmalaması gerekir, bu da sıkı bir bağımlılık (tight coupling) yaratır. Sınıf, refund() üst sınıftan özellikler miras alır ancak abonelik iadeleri, tek seferlik ödemelerden farklı bir mantık gerektirir. Üzerindeki değişiklikler, CreditCardPayment bu değişiklikler zamanlama ile alakasız olsa bile RecurringCreditCardPayment sınıfını etkiler.

✅ Kurallara uygun:

class CreditCardPayment extends Payment {
    constructor(amount, currency, cardToken, billingAddress) {
        super(amount, currency);
        this.cardToken = cardToken;
        this.billingAddress = billingAddress;
    }

    async process() {
        await this.validateCard();
        return await stripe.charges.create({
            amount: this.amount * 100,
            source: this.cardToken,
            currency: this.currency
        });
    }

    async refund() {
        await this.validateRefund();
        return await stripe.refunds.create({ charge: this.chargeId });
    }

    async validateCard() {
        // Card validation logic
    }
}

class RecurringCreditCardPayment {
    constructor(creditCardPayment, schedule) {
				this.creditCardPayment = creditCardPayment;
        this.schedule = schedule;
    }

    async scheduleNextPayment() {
        this.schedule.onNextCyle(() => {
	        await this.creditCardPayment.process();
        })
    }
}

const recurringCreditCardPayment = new RecurringCreditCardPayment(
	new CreditCardPayment(),
	new Schedule(),
);

Neden önemli: RecurringCreditCardPayment yalnızca zamanlamaya odaklanır ve ödeme işlemeyi oluşturulan (composed) CreditCardPayment örneğine devreder. Kalıtım olmaması, üst sınıf implementasyonuna sıkı bağımlılık olmadığı anlamına gelir. Kredi kartı işleme süreçlerindeki değişiklikler zamanlama mantığını etkilemez. Ödeme örneği, zamanlama kodunu değiştirmeden herhangi bir ödeme yöntemiyle değiştirilebilir.

Sonuç

Kalıtım yoluyla karıştırmak yerine sorumlulukları ayırmak için kompozisyon kullanın. Bir sınıf başka bir sınıfın işlevselliğine ihtiyaç duyduğunda, bunu miras almak yerine bir bağımlılık olarak kabul edin ve işi ona devredin (delegate). Bu, gevşek bağımlılık (loose coupling) yaratır, test etmeyi kolaylaştırır ve bir sınıftaki değişikliklerin diğerini bozmasını engeller.

Sık Sorulan Sorular

Sorularınız mı var?

Kalıtım mı yoksa kompozisyon mu kullanmalıyım?

Kalıtımı yalnızca, alt sınıfın üst sınıfın gerçekten özelleştirilmiş bir versiyonu olduğu gerçek 'is-a' (türüdür) ilişkileri için kullanın. Alanınızda (domain) kareler birer dikdörtgense, Rectangle sınıfını genişleten (extends) Square mantıklıdır. 'Has-a' (sahiptir) veya 'uses-a' (kullanır) ilişkileri için kompozisyon kullanın. Yinelenen bir ödeme, bir ödeme işlemcisi kullanır; bir tür ödeme işlemcisi değildir. Şüpheye düştüğünüzde kompozisyonu tercih edin.

Birden fazla kaynaktan kodu yeniden kullanmam gerekirse ne yapmalıyım?

Kompozisyon bunu birden fazla bağımlılık aracılığıyla doğal bir şekilde halleder. Bir sınıf; çoklu kalıtım (multiple inheritance) kısıtlamalarıyla uğraşmadan bir ödeme işlemcisi, bir zamanlayıcı ve bir bildirimciyi kompozisyonla birleştirebilir. Kalıtım ise sizi tekli kalıtım dillerine veya karmaşık çoklu kalıtım hiyerarşilerine mecbur bırakır. Kompozisyon daha nettir: Her bağımlılık kuricide (constructor) açıkça belirtilir.

Kalıtımı kompozisyona nasıl dönüştürebilirim (refactor)?

Alt sınıfın gerçekte ne yaptığı ile neyi miras aldığını tespit edin. Örnekte, RecurringCreditCardPayment ödemeleri zamanlar ancak işleme mantığını miras alır. Üst sınıf işlevselliğini ayrı bir sınıfa çıkarın ve ardından bunu bir bağımlılık olarak geçirin. extends Parent ifadesini bir kurucu (constructor) parametresiyle değiştirin. super.method() çağrılarını this.dependency.method() ile değiştirin. Her adımı test edin.

Kompozisyon daha fazla şablon kod (boilerplate) oluşturmaz mı?

İlk kurulum açık bağımlılıklar gerektirir, ancak bu netlik oldukça değerlidir. Üst sınıf hiyerarşilerinde arama yapmak zorunda kalmadan her sınıfın tam olarak neye ihtiyaç duyduğunu görürsünüz. Modern bağımlılık enjeksiyonu framework'leri şablon kodları azaltır. Bu açıklık, örtülü miras alınan davranışlardan kaynaklanan hataları önler. Birkaç ekstra satırlık kurulum kodu, sağladığı esneklik ve sürdürülebilirlik için buna değer.

Soyut taban sınıflar (abstract base classes) ve arayüzler (interfaces) hakkında ne düşünüyorsunuz?

Arayüzler, uygulamaları birbirine sıkıca bağlamadan (coupling) sözleşmeler tanımlamak için mükemmeldir. Bir sınıfın hangi davranışa ihtiyaç duyduğunu belirtmek için arayüzleri kullanın, ardından somut (concrete) uygulamaları enjekte edin. Soyut sınıflar ise yalnızca uygulanmamış bazı yöntemlere sahip kalıtımlardır, aynı sıkı bağlanma sorunlarına sahiptirler. Kalıtım içeren soyut sınıflar yerine kompozisyon içeren arayüzleri tercih edin.

Paylaşılan yardımcı (utility) yöntemleri nasıl ele almalıyım?

Bunları ayrı yardımcı sınıflara veya servislere çıkarın. Paylaşılan doğrulama mantığını miras almak yerine, bir Validator servisi enjekte edin. Örnekte, hem tek seferlik hem de tekrarlayan ödemelerin aynı doğrulamaya ihtiyacı varsa, her ikisinin de kompozisyon yoluyla kullanabileceği ortak bir PaymentValidator oluşturun. Bu, paylaşılan mantığın üst sınıflarda gizlenen yöntemlere kıyasla daha kolay keşfedilmesini ve test edilmesini sağlar.

Şimdi güvenliğinizi sağlayın

Kodunuzu, bulutunuzu ve çalışma zamanınızı tek bir merkezi sistemde güvenceye alın.
Güvenlik açıklarını otomatik olarak hızla bulun ve düzeltin.

Kredi kartı gerekmez | Tarama sonuçları 32 saniyede.