Kalıtım yerine neden kompozisyonu tercih etmeliyim?


109

Her zaman kompozisyonun kalıtım yerine tercih edileceğini okudum. Türlerin aksine bir blog yazısı, örneğin, kalıtım üzerine kompozisyon kullanmayı savunuyor, ancak polimorfizmin nasıl elde edildiğini göremiyorum.

Ancak insanlar kompozisyonu tercih ettiklerini söylediğinde, kompozisyon ve arayüz uygulamasının bir kombinasyonunu tercih ettikleri anlamına geliyor. Kalıtım olmadan polimorfizmi nasıl elde edersiniz?

Miras kullandığım somut bir örnek. Bu kompozisyonu kullanmak için nasıl değiştirilir ve ne kazanırdım?

Class Shape
{
    string name;
  public:
    void getName();
    virtual void draw()=0;
}

Class Circle: public Shape
{
    void draw(/*draw circle*/);
}

57
Hayır, insanlar derken gerçekten demek kompozisyon tercih tercih , kompozisyon değil hiç hiç devralma kullanın . Tüm sorunuz hatalı bir öncül dayanmaktadır. Uygun olduğunda kalıtım kullanın.
Kertenkele Bill


2
Bill ile aynı fikirdeyim, kalıtımın GUI geliştirmede ortak bir uygulama olduğunu gördüm.
Prashant Cholachagudda

2
Kompozisyon kullanmak istiyorsunuz çünkü bir kare sadece iki üçgenin bileşimidir, aslında elips dışındaki tüm şekiller sadece üçgenlerin kompozisyonlarıdır. Polimorfizm, sözleşmeden doğan yükümlülüklerle ilgilidir ve% 100 mirastan kaldırılmıştır. Üçgen elde edildiğinde bir tuhaflık eklenir, çünkü birileri piramit üretme kabiliyetine sahip olmak isterdi, eğer Üçgen'den miras alırsanız, altıgenizden asla bir 3d piramit üretmeyecekseniz bile hepsini elde edeceksiniz .
Jimmy Hoffa

2
@BilltheLizard Gerçekten mirasını asla kullanmayacak demek olduğunu söyleyenlerin çoğunun kaldığını düşünüyorum ama yanılıyorlar.
immibis 6:15

Yanıtlar:


51

Polimorfizm mutlaka kalıtım anlamına gelmez. Genellikle kalıtım, Polimorfik davranışı uygulamak için kolay bir araç olarak kullanılır, çünkü benzer davranışçı nesneleri tamamen ortak kök yapısına ve davranışına sahip olarak sınıflandırmak uygundur. Yıllar boyunca gördüğünüz tüm araba ve köpek kodu örneklerini düşünün.

Peki ya aynı olmayan nesneler. Bir otomobilin ve bir gezegenin modellenmesi çok farklı olurdu ve yine de her ikisi de Move () davranışını uygulamak isteyebilir.

Aslında, söylediğinde temelde kendi soruna cevap verdin "But I have a feeling that when people say prefer composition, they really mean prefer a combination of composition and interface implementation.". Ortak davranış, arayüzler ve davranışsal bir bileşik aracılığıyla sağlanabilir.

Hangisi daha iyi olduğuna göre, cevap biraz özneldir ve sisteminizin nasıl çalışmasını istediğinize, hem bağlamsal hem de mimari açıdan neyin mantıklı olduğu ve test etmenin ve sürdürmenin ne kadar kolay olacağına bağlıdır.


Uygulamada, "Arayüzle Polimorfizm" ne sıklıkta ortaya çıkmaktadır ve normal olarak kabul edilir (dillerin ifade edilişinden yararlanılmasının aksine). Polimorfizmin kalıtım yolu ile, spesifikasyondan sonra keşfedilen dilin (C ++) bir sonucu değil, dikkatli bir tasarımdan geçtiğine bahse girerim.
Samis

1
Bir gezegende ve bir arabada hamle () 'yi kim çağırır ve aynı düşünür?! Buradaki soru hangi bağlamda hareket edebilmeli? Her ikisi de basit bir 2D oyunda 2D nesnelerse, hareketi devralabilirler, eğer büyük bir veri simülasyonundaki nesneler olsaydı, aynı temelden devralmalarını sağlamak bir anlam ifade etmeyebilirdi ve bunun sonucunda bir arayüz
NikkyD

2
@ SamusArin Her yerde ortaya çıkıyor ve arayüzleri destekleyen dillerde tamamen normal kabul ediliyor. Ne demek, "bir dilin ifadeciliğini sömürmek"? Arayüzler bunun için var .
Andres F.

@AndresF. Observer desenini gösteren “Nesneye Yönelik Analiz ve Uygulamalarla Tasarım” okurken bir örnekte “Arayüzle Polimorfizm” ile karşılaştım. Sonra cevabımı farkettim.
Samis

@AndresF. Son projemde polimorfizmi nasıl kullandığımdan dolayı vizyonum biraz kör oldu sanırım. Hepsi aynı tabandan türetilmiş 5 kayıt türüm vardı. Zaten aydınlanma için teşekkürler.
Samis

79

Kompozisyonu tercih etmek sadece polimorfizm ile ilgili değildir. Her ne kadar bunun bir parçası olsa da ve haklı olarak (en azından nominal olarak yazılmış dillerde) insanların gerçekten kastettiği şeylerin "bir kompozisyon ve arayüz uygulamasının bir kombinasyonunu tercih etmesini" haklısın. Ancak, kompozisyonu tercih etme nedenleri (çoğu durumda) oldukça derindir.

Polimorfizm , birden fazla şekilde davranan bir şey hakkındadır. Dolayısıyla, jenerikler / şablonlar, tek bir kod parçasının davranışını türlere göre değiştirmesine izin verdikleri ölçüde "polimorfik" bir özelliktir. Aslında, bu tür polimorfizm gerçekten en iyi şekilde davranılır ve varyasyon bir parametre ile tanımlandığı için genellikle parametrik polimorfizm olarak adlandırılır .

Birçok dil, "aşırı yükleme" olarak adlandırılan bir polimorfizm veya aynı addaki çoklu işlemlerin geçici olarak tanımlandığı ve dilin dil tarafından seçildiği (belki de en özel olan) bir polimorfizm biçimi sağlar. Bu, en az iyi davranış gösteren polimorfizmdir, çünkü gelişmiş kongre hariç iki prosedürün davranışını hiçbir şey bağlamadı.

Üçüncü tür polimorfizm, alt tip polimorfizmdir . Burada, belirli bir tipte tanımlanmış bir prosedür, aynı zamanda, bu tipteki bir "alt tip" ailesinin tümü üzerinde de çalışabilir. Bir arayüz uyguladığınızda veya bir sınıfı genişlettiğinizde, genellikle bir alt tür oluşturma niyetinizi beyan etmiş olursunuz. Gerçek alt türler Liskov'un Değiştirme İlkesi tarafından yönetiliyorBir üst tipteki tüm nesneler hakkında bir şeyler ispat ederseniz, onu bir alt tipteki tüm örnekler hakkında ispatlayabileceğinizi söyler. C ++ ve Java gibi dillerde, insanlar genel olarak güçsüz ve çoğu zaman alt sınıfları için geçerli olabilecek sınıflarla ilgili belgelenmemiş varsayımlara sahip olduklarından, hayat tehlikeli hale geliyor. Yani, kod gerçekten olduğundan daha fazla kanıtlanmış gibi yazılır, bu da dikkatsiz bir şekilde alt yazı yazdığınızda çok sayıda sorun ortaya çıkarır.

Kalıtım aslında polimorfizmden bağımsızdır. Kendisine referansı olan bir "T" olayı göz önüne alındığında, kalıtım, "T" den "S" ye bir referansla değiştirilen "S" den yeni bir şey yarattığınızda meydana gelir. Bu tanım kasıtlı olarak belirsizdir, çünkü kalıtım birçok durumda gerçekleşebilir, ancak en yaygın olanı thissanal işlevler tarafından işaretlenen thisişaretçiyi işaretçiyle alt tip yerine değiştirme etkisi olan bir nesneyi alt sınıftır.

Kalıtım tüm çok güçlü şeyler gibi tehlikelidir ; kalıtımın tahribata yol açma gücü vardır. Örneğin, bir sınıftan miras alırken bir yöntemi geçersiz kıldığınızı varsayalım: her şey iyi ve iyidir; o sınıfın başka bir yöntemi, özgün sınıfın yazarı tarafından tasarlandığı şekliyle belirli bir şekilde davranmak için miras aldığınız yöntemi kabul edene kadar . Başkaları tarafından çağrılan tüm yöntemleri geçersiz kılmak için tasarlanmadıkça , özel veya sanal olmayan (son) olarak bildirerek kısmen koruyabilirsiniz . Bu olsa bile her zaman yeterince iyi değil. Bazen böyle bir şey görebilirsiniz (sözde Java'da, umarım C ++ ve C # kullanıcılarına okunabilir)

interface UsefulThingsInterface {
    void doThings();
    void doMoreThings();
}

...

class WayOfDoingUsefulThings implements UsefulThingsInterface{
     private foo stuff;
     public final int getStuff();
     void doThings(){
       //modifies stuff, such that ...
       ...
     }
     ...
     void doMoreThings(){
       //ignores stuff
       ...
     }
 }

bunun çok güzel olduğunu düşünüyorsunuz ve “şeyler” yapmak için kendi yolunuza sahipsiniz, ancak “daha ​​fazla şeyler” yapma yeteneğini kazanmak için miras kullanıyorsunuz,

class MyUsefulThings extends WayOfDoingUsefulThings{
     void doThings {
        //my way
     }
}

Ve her şey iyi ve güzel. WayOfDoingUsefulThingsbir yöntemi değiştirmek, başka hiçbir şeyin anlamını değiştirmeyecek şekilde tasarlandı ... beklemek dışında, hayır değildi. Öyle gözüküyor, ama doThingsönemli olan değişken durumu değiştirdi. Dolayısıyla, geçersiz kılınabilir işlevler olmasa da,

 void dealWithStuff(WayOfDoingUsefulThings bar){
     bar.doThings()
     use(bar.getStuff());
 }

şimdi, a'yı geçirdiğinizde beklenenden farklı bir şey yapıyor MyUsefulThings. Daha da kötüsü, WayOfDoingUsefulThingsbu sözleri verdiğini bile bilmiyor olabilirsiniz . Belki dealWithStuffaynı kitaplığından gelen WayOfDoingUsefulThingsve getStuff()(düşünmek bile kütüphaneye tarafından ihraç edilmez arkadaş sınıfı C ++). Daha da kötüsü, sen fark etmeden dilin statik kontrollerini yendin: dealWithStuffBir sürdü WayOfDoingUsefulThingssadece bunun olurdu emin olmak için getStuff()belirli bir şekilde davrandığını işlevi.

Kompozisyon kullanarak

class MyUsefulThings implements UsefulThingsInterface{
     private way = new WayOfDoingUsefulThings()
     void doThings() {
        //my way
     }
     void doMoreThings() {
        this.way.doMoreThings();
     }
}

statik tip güvenliği geri getirir. Genel olarak, alt tipleme yapılırken, kalıtsallıktan daha kolay ve daha güvenli bir bileşim kullanmak kolaydır. Ayrıca, son yöntemleri geçersiz kılmanıza izin verir, bu da ara yüzlerde çoğu zaman sanal olmayan ve sanal olmayan her şeyi ilan etmekte özgür olmanız gerektiği anlamına gelir .

Daha iyi bir dünyada, diller otomatik olarak bir delegationanahtar kelimeyle kazan plakasını yerleştirir . Çoğu, öyle değil, bir dezavantajı daha büyük sınıflar. Bununla birlikte, IDE'nizi size temsilci seçme örneğini yazmasını sağlayabilirsiniz.

Şimdi, yaşam sadece polimorfizmle ilgili değil. Her zaman alt tür yazmanıza gerek yoktur. Polimorfizmin amacı genellikle yeniden kod kullanımıdır, ancak bu hedefe ulaşmak için tek yol bu değildir. Çoğu zaman, bileşimin, alt tip polimorfizmi olmadan, işlevselliği yönetmenin bir yolu olarak kullanılması mantıklıdır.

Ayrıca, davranışsal kalıtımın da kullanımları vardır. Bilgisayar bilimindeki en güçlü fikirlerden biridir. Sadece bu, çoğu zaman iyi OOP uygulamaları sadece arayüz kalıtım ve kompozisyonlar kullanılarak yazılabilir. İki ilke

  1. Kalıtım yasağı veya bunun için tasarım
  2. Kompozisyon tercih et

yukarıdaki nedenlerden dolayı iyi bir rehberdir ve önemli bir maliyete maruz kalmayın.


4
Güzel cevap Miras ile yeniden kod kullanımı sağlamaya çalışmanın yanlış bir yol olduğunu özetlerdim. Kalıtım çok güçlü bir kısıtlamadır ("güç" eklemek imho yanlıştır!) Ve kalıtsal sınıflar arasında güçlü bir bağımlılık yaratır. Çok fazla bağımlılık = hatalı kod :) Yani kalıtım (aka "gibi davranır") tipik olarak birleştirilmiş arayüzler için parlar (= kalıtsal sınıfların karmaşıklığını gizleme), başka bir şey için iki kere düşünün veya kompozisyon kullanın ...
Mar

2
Bu iyi bir cevap. +1. En azından gözlerimde, fazladan komplikasyonun önemli bir maliyet olabileceği düşünülüyor ve bu, beni kişisel olarak kompozisyonu tercih etmekten büyük bir sorun çıkarmak için biraz tereddüt etti. Özellikle, arayüzler, kompozisyonlar ve DI (birimine başka şeyler eklediğini biliyorum) yoluyla bir sürü test dostu kod hazırlamaya çalıştığınızda, birisinin çok farklı dosyalara bakmasını sağlamak çok kolay görünüyor. birkaç detay Miras tasarım ilkesi üzerine sadece kompozisyonla uğraşırken bile neden bu daha sık belirtilmiyor?
Panzercrisis

27

İnsanların bunu söylemelerinin sebebi, polimorfizmi miras derslerinden yeni çıkmış olan OOP programcılarının, çok sayıda polimorfik yöntemle ve daha sonra yolun herhangi bir yerinde büyük sınıflar yazma eğiliminde olmayan bir karmaşaya yol açma eğiliminde olmalarıdır.

Tipik bir örnek oyun geliştirme dünyasından gelir. Tüm oyun varlıklarınız için temel bir sınıfınız olduğunu varsayalım - oyuncunun uzay gemisi, canavarları, mermileri vb .; her varlık türünün kendi alt sınıfı vardır. Miras yaklaşım örneğin, bir kaç polimorfik yöntemleri kullanmak istiyorsunuz update_controls(), update_physics(), draw()vb ve her alt sınıf için bunları uygulamak. Bununla birlikte, bu, ilgisiz işlevselliği birleştirdiğiniz anlamına gelir: bir nesnenin onu taşımak için nasıl göründüğü önemli değildir ve onu çizmek için AI ile ilgili hiçbir şey bilmeniz gerekmez. Kompozisyon yaklaşımı bunun yerine birkaç temel sınıfı (veya arayüzleri), örneğin EntityBrain(alt sınıflar AI veya oynatıcı girdisini EntityPhysicsuygular ), (alt sınıflar hareket fiziğini uygular) ve EntityPainter(alt sınıflar çizimle ilgilenir) ve polimorfik olmayan bir sınıfı tanımlarEntityBu her birinin bir örneğini tutar. Bu şekilde, herhangi bir görünümü herhangi bir fizik modeliyle ve AI ile birleştirebilirsiniz ve onları ayrı tuttuğunuzdan, kodunuz da daha temiz olacaktır. Ayrıca, "1. seviyedeki balon canavarı gibi görünen bir canavarı istiyorum, ancak 15. seviyedeki çılgın palyaço gibi davranan bir canavarı istiyorum" gibi sorunlar gider: siz sadece uygun bileşenleri alıp bir araya yapıştırın.

Kompozisyon yaklaşımının hala her bir bileşen içerisinde kalıtım kullandığını unutmayın; Her ne kadar ideal olarak burada sadece arayüzleri ve uygulamalarını kullanırsınız.

Burada “kaygıların ayrılması” anahtar kelimedir: fiziği temsil etmek, bir AI uygulamak ve bir varlık çizmek, üç kaygıdır, onları bir varlık olarak birleştirmek dördüncüdir. Kompozisyon yaklaşımıyla, her kaygı bir sınıf olarak modellenmiştir.


1
Bunların hepsi iyi ve asıl soruya bağlanan makalede savundu. Polimorfizmi koruyarak (C ++ 'da) bütün bu varlıkları içeren basit bir örnek verebilirseniz (zamana ne zaman gelirseniz) çok sevinirim. Veya beni bir kaynağa yönlendir. Teşekkürler.
MustafaM

Cevabınızın çok eski olduğunu biliyorum ama oyunumda bahsettiğinizle aynı şeyi yaptım ve şimdi miras yaklaşımı üzerine bir kompozisyona gidiyorum.
Sneh

“Görünüşe göre bir canavar istiyorum…” bu nasıl bir anlam ifade ediyor? Bir arayüz herhangi bir uygulama yapmaz, görünüm ve davranış kodunu bir şekilde veya başka bir şekilde kopyalamanız gerekir
NikkyD

1
@NikkyD Sen almak MonsterVisuals balloonVisualsve MonsterBehaviour crazyClownBehaviourbir örneğini Monster(balloonVisuals, crazyClownBehaviour)yanında, Monster(balloonVisuals, balloonBehaviour)ve Monster(crazyClownVisuals, crazyClownBehaviour)seviye 1 ve seviye 15 üzerinde örneği alındı örnekleri
Caleth

13

Verdiğiniz örnek, kalıtımın doğal seçim olduğu bir örnek. Bir kimsenin kompozisyonun her zaman mirastan daha iyi bir seçim olduğunu iddia edeceğini sanmıyorum - bu, sadece oldukça fazla sayıda özel nesne oluşturmaktan ziyade nispeten basit birkaç nesneyi bir araya getirmenin daha iyi olduğu anlamına gelir.

Delegasyon, kalıtım yerine kompozisyon kullanmanın bir yoludur. Temsilci seçme, bir sınıfın davranışını alt sınıflandırma olmadan değiştirmenize olanak sağlar. Ağ bağlantısı sağlayan bir sınıf düşünün, NetStream. Ortak bir ağ protokolü uygulamak için NetStream'i sınıflamak doğal olabilir, bu nedenle FTPStream ve HTTPStream ile karşılaşabilirsiniz. Ancak, tek bir amaç için çok spesifik bir HTTPStream alt sınıfı oluşturmak yerine, örneğin, UpdateMyWebServiceHTTPStream, yerine, bu nesneden aldığı verilerle ne yapacağını bilen bir temsilci ile birlikte düz eski bir HTTPStream örneği kullanmak daha iyi olur. Bunun daha iyi bir nedeni, korunması gereken fakat asla tekrar kullanamayacağınız sınıfların çoğalmasını önlemesidir.


11

Yazılım geliştirme söyleminde bu döngüyü çok göreceksiniz:

  1. Bazı özellik veya modellerin (“Desen X” olarak adlandıralım) bazı özel amaçlar için yararlı olduğu keşfedilmiştir. Blog gönderileri, Desen X ile ilgili erdemleri vurgulayarak yazılmıştır.

  2. Yutturmaca bazı insanları mümkün olduğunca Pattern X kullanmanız gerektiğini düşünmeye yönlendirir .

  3. Diğer insanlar, uygun olmayan durumlarda bağlamlarda kullanılan Desen X'i görmekten rahatsız olurlar ve Desen X'i her zaman kullanmamanız gerektiğini ve bazı bağlamlarda zararlı olduğunu belirten blog yazıları yazarlar .

  4. Geri tepme, bazı insanların Desen X'in her zaman zararlı olduğuna ve asla kullanılmaması gerektiğine inanmalarına neden olur .

Bu yutturmaca / geri tepme döngüsünün GOTO, kalıptan SQL'e, NoSQL ve evet, kalıtımdan hemen hemen her özelliğe sahip olduğunu göreceksiniz . Panzehir her zaman bağlamı dikkate almaktır .

Olması Circlesoyundan Shapeolduğu tam olarak miras miras destekleyen OO dilde kullanılabilir gerekiyordu nasıl.

Temel kural “miras yerine kompozisyonu tercih et” bağlamı olmadan gerçekten yanıltıcıdır. Miras daha uygun olduğunda mirası tercih etmelisiniz, ancak kompozisyon daha uygun olduğunda kompozisyonu tercih etmelisiniz. Cümle, kalıtımın her yerde kullanılması gerektiğini düşünen hype döngüsündeki 2. aşamadaki insanlara yöneliktir. Ancak bu döngü devam etti ve bugün bazı kişilerin mirasın kendi içinde bir şekilde kötü olduğunu düşünmesine neden oluyor gibi görünüyor.

Tornavidaya karşı çekiç gibi düşün. Bir çekiç üzerinde bir tornavida mı tercih etmelisin? Soru mantıklı değil. İşe uygun aracı kullanmanız gerekir ve bunların tümü, hangi görevi yerine getirmeniz gerektiğine bağlıdır.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.