Kompozisyon kalıtım yerine tercih edilmeli mi?


1604

Kompozisyonu kalıtım yerine neden tercih etmelisiniz? Her yaklaşım için hangi ödünleşimler vardır? Kompozisyona göre kalıtım ne zaman seçilmelidir?



40
Burada bu soru hakkında iyi bir makale var . Benim kişisel görüşüm, tasarım için "daha iyi" veya "daha kötü" bir ilkenin olmamasıdır. Somut görev için "uygun" ve "yetersiz" tasarım vardır. Başka bir deyişle - Duruma bağlı olarak hem kalıtım hem de kompozisyon kullanıyorum. Amaç, daha küçük kodlar üretmek, okunması daha kolay, yeniden kullanımı ve sonunda daha da genişletilmesidir.
m_pGladiator

1
bir cümle mirası, genel bir yönteminiz varsa ve bunu değiştirirseniz, yayınlanan API'yi değiştirir. kompozisyonunuz varsa ve oluşturulan nesne değiştiyse, yayınlanmış API'nizi değiştirmeniz gerekmez.
Tomer Ben David

Yanıtlar:


1188

Daha sonra değiştirilmesi daha kolay / kolay olduğu için kompozisyonu kalıtım yerine tercih edin, ancak her zaman oluştur yöntemini kullanmayın. Kompozisyon ile, Bağımlılık Enjeksiyonu / Ayarlayıcıları ile anında davranışı değiştirmek kolaydır. Çoğu dil birden fazla türden türetmenize izin vermediğinden, kalıtım daha katıdır. Yani TipA'dan türettikten sonra kaz az çok pişirilir.

Yukarıdaki asit testim:

  • TypeB, TypeA'nın beklendiği yerde TypeB kullanılabilmesi için TypeA'nın tüm arabirimini (tüm genel yöntemler daha az değil) ortaya çıkarmak istiyor mu? Kalıtım gösterir .

    • Örneğin, bir Cessna çift kanatlı uçağı daha fazla olmasa bile bir uçağın tüm arayüzünü ortaya çıkaracaktır. Böylece Uçaktan türetmek uygun olur.
  • TypeB, TypeA tarafından ortaya konan davranışın yalnızca bir kısmını mı istiyor? Kompozisyon ihtiyacını gösterir .

    • Örneğin, bir Kuş bir Uçağın sadece uçma davranışına ihtiyaç duyabilir. Bu durumda, onu bir arabirim / sınıf / her ikisi olarak çıkarmak ve her iki sınıfın bir üyesi yapmak mantıklıdır.

Güncelleme: Cevabım geri döndü ve şimdi Barbara Liskov'un Liskov İkame Prensibi'nden 'Bu türden miras almalıyım?'


81
İkinci örnek, doğrudan İlk Baş Tasarım Desenleri ( amazon.com/First-Design-Patterns-Elisabeth-Freeman/dp/… ) kitabından çıkmaktadır :) Bu kitabı bu soruyu araştıran herkese şiddetle tavsiye ediyorum.
Jeshurun

4
Çok açık, ama bir şeyleri kaçırabilir: "TypeB, TypeA'nın beklendiği yerde TypeB'nin kullanılabileceği şekilde TypeA'nın tüm arayüzünü (tüm genel yöntemler daha az değil) ortaya çıkarmak istiyor mu?" Peki bu doğruysa ve TypeB de TypeC'nin tüm arayüzünü ortaya çıkarırsa? Peki ya TypeC henüz modellenmemişse?
Tristan

4
En temel sınama olması gerektiğini düşündüğüm şeyi ifade edersiniz: "Bu nesne, taban türünün nesnelerini (ne olurdu) bekleyen kodla kullanılabilir mi?" Cevap evet ise, nesne miras almalıdır . Hayır ise, muhtemelen olmamalıdır. Benim druthers'ım olsaydı, diller "bu sınıfa" atıfta bulunmak için bir anahtar kelime sağlar ve tıpkı başka bir sınıf gibi davranması gereken, ancak onun yerine geçmeyen bir sınıfı tanımlamanın bir yolunu sunacaktı (böyle bir sınıfın hepsi "bu" sınıf "referansları kendisiyle değiştirilir).
supercat

22
@Alexey - nokta 'Şaşırtıcı olmadan bir uçak bekleyen tüm müşterilere bir Cessna çift kanatlı geçebilir miyim?'. Eğer evet ise, o zaman miras almak şansınız vardır.
Gishu

9
Aslında kalıtımın cevabım olacağı örnekleri düşünmek için uğraşıyorum, genellikle toplama, kompozisyon ve arayüzlerin daha zarif çözümlerle sonuçlandığını görüyorum. Yukarıdaki örneklerin birçoğu bu yaklaşımlar kullanılarak daha iyi açıklanabilir ...
Stuart Wakefield

414

Sınırlamayı bir ilişki olarak düşünün . Bir arabanın "motoru", bir kişinin "adı" vb. Vardır.

Kalıtımın bir ilişki olduğunu düşünün . Bir araba "bir" araç, bir kişi "bir" memeli "vb.

Bu yaklaşım için hiç kredi almıyorum. Doğrudan Steve McConnell , Bölüm 6.3 tarafından tamamlanan İkinci Kod Tamamlama Sürümü'nden aldım .


107
Bu her zaman mükemmel bir yaklaşım değildir, sadece iyi bir rehberdir. Liskov İkame İlkesi çok daha doğrudur (daha az başarısız olur).
Bill K

40
"Arabamın bir aracı var." Bunu programlama bağlamında değil, ayrı bir cümle olarak görürseniz, bu kesinlikle bir anlam ifade etmez. Ve bu tekniğin bütün mesele bu. Kulağa garip geliyorsa, muhtemelen yanlıştır.
Nick Zalutskiy

36
@ Tabii ki, ama "Arabamın VehicleBehavior var" daha mantıklı (Sanırım "Vehicle" sınıfınız "VehicleBehavior" olarak adlandırılabilir). Bu yüzden, kararınızı "vs" olan "karşılaştırma" üzerine kuramazsınız, LSP kullanmanız gerekir, ya da hatalar yaparsınız
Tristan

35
Onun yerine "düşünmek" gibi davranır. Kalıtım sadece semantiği değil, davranışı miras almakla ilgilidir.
ybakos

4
@ybakos "Davranır", kalıtım gerekmeksizin arabirimler üzerinden gerçekleştirilebilir. Wikipedia'dan : "Kompozisyonun kalıtım üzerine uygulanması, tipik olarak sistemin göstermesi gereken davranışları temsil eden çeşitli arayüzlerin oluşturulmasıyla başlar ... Böylece, sistem davranışları kalıtım olmadan gerçekleştirilir."
DavidRR

210

Farkı anlarsanız, açıklamak daha kolaydır.

Usul Kanunu

Bunun bir örneği, sınıfları kullanmadan PHP'dir (özellikle PHP5'ten önce). Tüm mantık bir dizi fonksiyonda kodlanmıştır. Yardımcı işlevler içeren diğer dosyaları dahil edebilir ve işlevlerde veri ileterek iş mantığınızı gerçekleştirebilirsiniz. Uygulama büyüdükçe bunu yönetmek çok zor olabilir. PHP5 daha fazla nesne yönelimli tasarım sunarak bunu düzeltmeye çalışır.

miras

Bu sınıfların kullanımını teşvik eder. Kalıtım OO tasarımının üç ilkesinden biridir (kalıtım, polimorfizm, kapsülleme).

class Person {
   String Title;
   String Name;
   Int Age
}

class Employee : Person {
   Int Salary;
   String Title;
}

Bu işte miras. Çalışan "bir" Kişi'dir ya da Kişiden miras alır. Bütün miras ilişkileri "is-a" ilişkisidir. Çalışan aynı zamanda Title özelliğini Personelin gölgelendirmesi anlamına gelir.

Kompozisyon

Kompozisyon kalıtım yerine tercih edilir. Basitçe söylemek gerekirse:

class Person {
   String Title;
   String Name;
   Int Age;

   public Person(String title, String name, String age) {
      this.Title = title;
      this.Name = name;
      this.Age = age;
   }

}

class Employee {
   Int Salary;
   private Person person;

   public Employee(Person p, Int salary) {
       this.person = p;
       this.Salary = salary;
   }
}

Person johnny = new Person ("Mr.", "John", 25);
Employee john = new Employee (johnny, 50000);

Kompozisyon tipik olarak "vardır" veya "kullanır" ilişkisidir. Burada Çalışan sınıfının bir Kişisi vardır. Kişi'den miras almaz, bunun yerine Kişi nesnesini ona aktarır, bu yüzden "Kişi" vardır.

Kalıtım Üzerine Kompozisyon

Şimdi bir Yönetici türü oluşturmak istediğinizi varsayalım:

class Manager : Person, Employee {
   ...
}

Bu örnek işe yarayacaktır, ancak Kişi ve Çalışan her ikisi de beyan ederse Title? Manager.Title "Operasyon Müdürü" veya "Bay" ı döndürmeli mi? Kompozisyon altında bu belirsizlik daha iyi ele alınır:

Class Manager {
   public string Title;
   public Manager(Person p, Employee e)
   {
      this.Title = e.Title;
   }
}

Yönetici nesnesi, Çalışan ve Kişi olarak oluşturulur. Başlık davranışı çalışandan alınır. Bu açık kompozisyon, diğer şeyler arasındaki belirsizliği ortadan kaldırır ve daha az hatayla karşılaşırsınız.


6
Miras için: Belirsizlik yoktur. Manager sınıfını gereksinimlere göre uyguluyorsunuz. Yani "Operasyonlar Müdürü" dönecekti eğer gereksinimleriniz belirtilmişse, aksi takdirde sadece temel sınıfın uygulamasını kullanırsınız. Ayrıca Kişiyi soyut bir sınıf haline getirebilir ve böylece aşağı akış sınıflarının bir Title özelliği uyguladığından emin olabilirsiniz.
Raj Rao

68
Birisinin "Miras üzerinde kompozisyon" diyebileceğini hatırlamak önemlidir, ama bu "Her zaman miras üzerinde kompozisyon" anlamına gelmez. "Bir" miras kalıtım anlamına gelir ve kodun yeniden kullanılmasına yol açar. Çalışan Kişidir (Çalışanın bir kişisi yoktur).
Raj Rao

36
Örnek kafa karıştırıcıdır. Çalışan bir kişidir, bu yüzden miras kullanmalıdır. Bu örnek için kompozisyon kullanmamalısınız, çünkü teknik olarak kodda bildirebilseniz bile etki alanı modelinde yanlış bir ilişki vardır.
Michael Freidgeim

15
Bu örneğe katılmıyorum. Çalışan , kalıtımın doğru kullanımı konusunda bir ders kitabı olan Kişidir. Ayrıca Başlık alanının yeniden tanımlanmasının "sorun" unun mantıklı olmadığını düşünüyorum. Employee.Title'ın Person.Title'i gölgelemesi, kötü programlamanın bir işaretidir. Sonuçta, "Bay" ve "Operasyon Müdürü" gerçekten bir insanın aynı yönüne atıfta bulunuyor (küçük harf)? Employee.Title adını değiştiririm ve böylece her ikisi de gerçek hayatta mantıklı olan bir Employee ait Title ve JobTitle niteliklerine başvurabilirim. Dahası, Yönetici için bir sebep yok (devam ediyor ...)
Radon Rosborough

9
(... devam etti) hem Kişi hem de Çalışandan miras almaya - sonuçta, Çalışan zaten Kişi'den miras alır. Bir kişinin Yönetici ve Temsilci olabileceği daha karmaşık modellerde, birden fazla mirasın kullanılabileceği doğrudur (dikkatlice!), Ancak birçok ortamda Yöneticiden (Çalışanları içeren soyut bir Role sınıfına sahip olmak tercih edilir. yönetir) ve Acenteyi (Sözleşmeler ve diğer bilgileri içerir) devralır. Sonra, bir Çalışan, birden fazla Rolü olan bir Kişidir. Böylece, hem bileşim hem de kalıtım düzgün bir şekilde kullanılır.
Radon Rosborough

142

Mirasın sağladığı inkar edilemez faydalarla, burada bazı dezavantajları var.

Kalıtımın Dezavantajları:

  1. Çalışma zamanında süper sınıflardan devralınan uygulamayı değiştiremezsiniz (açıkçası kalıtım derleme zamanında tanımlandığı için).
  2. Miras, bir alt sınıfı üst sınıf uygulamasının ayrıntılarına maruz bırakır, bu nedenle mirasın kapsüllemeyi bozduğu sıklıkla söylenir (bir anlamda gerçekten sadece uygulama değil arayüzlere odaklanmanız gerekir, bu nedenle alt sınıflandırma ile yeniden kullanım her zaman tercih edilmez).
  3. Miras tarafından sağlanan sıkı bağlantı, bir alt sınıfın uygulanmasını, ana uygulamadaki herhangi bir değişikliğin alt sınıfı değişmeye zorlayacağı bir süper sınıfın uygulanmasına çok bağlı hale getirir.
  4. Alt sınıflandırma ile aşırı yeniden kullanım, kalıtım yığınını çok derin ve çok kafa karıştırıcı yapabilir.

Öte yandan, nesne kompozisyonu çalışma zamanında diğer nesnelere referans alan nesneler aracılığıyla tanımlanır. Böyle bir durumda, bu nesneler hiçbir zaman birbirlerinin korunan verilerine erişemeyecektir (kapsülleme kesintisi yoktur) ve birbirlerinin arayüzüne saygı duymaya zorlanacaktır. Ve bu durumda da, uygulama bağımlılıkları kalıtımdan çok daha az olacaktır.


5
Bu daha iyi cevaplardan biri, bence - Bunu, deneyimlerime göre, kompozisyon açısından yeniden düşünmeye çalışmanın, daha küçük, daha basit, daha bağımsız, daha yeniden kullanılabilir sınıflara yol açma eğiliminde olduğunu ekleyeceğim. daha net, daha küçük, daha odaklanmış bir sorumluluk kapsamı ile. Genellikle bu, bağımlılık enjeksiyonu veya alay etme (testlerde) gibi şeylere daha az ihtiyaç duyulduğu anlamına gelir, çünkü daha küçük bileşenler genellikle kendi başlarına durabilir. Sadece benim deneyimim. YMMV :-)
mindplay.dk

3
Bu yayındaki son paragraf benim için gerçekten tıkladı. Teşekkür ederim.
Salx

87

Kompozisyonun miras yerine tercih edilmesinin bir başka çok pragmatik nedeni, alan modelinizle ilişkisel bir veritabanıyla eşleştirilmesidir. Kalıtımın SQL modeliyle eşleştirilmesi gerçekten zordur (her zaman kullanılmayan sütunlar oluşturmak, görünümler kullanmak vb. Gibi her türlü hileli geçici çözümle sonuçlanırsınız). Bazı ORML'ler bununla başa çıkmaya çalışır, ancak her zaman hızlı bir şekilde karmaşıklaşır. Kompozisyon, iki tablo arasındaki yabancı anahtar ilişkisiyle kolayca modellenebilir, ancak kalıtım çok daha zordur.


81

Kısaca "kompozisyonu kalıtım yerine tercih etmeyi" kabul ederken, çoğu zaman benim için "koka kola yerine patatesleri tercih et" gibi geliyor. Kalıtım yerleri ve kompozisyon yerleri var. Farkı anlamanız gerekir, o zaman bu soru kaybolacaktır. Benim için gerçekten anlamı, "miras kullanacaksanız - tekrar düşünün, kompozisyona ihtiyacınız var".

Yemek istediğinizde coca cola yerine patatesleri, içmek istediğinizde ise coca cola'yı tercih etmelisiniz.

Bir alt sınıf oluşturmak, üst sınıf yöntemlerini çağırmanın uygun bir yolundan daha fazlası anlamına gelmelidir. Alt sınıf "-a" süper sınıf olduğunda hem yapısal hem de işlevsel olarak, üst sınıf olarak kullanılabildiğinde ve bunu kullanacağınızda kalıtım kullanmalısınız. Durum böyle değilse - miras değil, başka bir şeydir. Kompozisyon, nesnelerinizin bir başkasından oluştuğu veya onlarla bir ilişkisi olduğu zamandır.

Yani benim için birisinin miras veya kompozisyona ihtiyacı olup olmadığını bilmemesi gibi görünüyor, asıl sorun, içmek ya da yemek yemek isteyip istemediğini bilmemesi. Sorun alanınızı daha fazla düşünün, daha iyi anlayın.


5
Doğru iş için doğru araç. Bir çekiç, bir şeyleri vurmaktan bir anahtardan daha iyi olabilir, ancak bu, bir anahtarı bir "aşağı çekiç" olarak görmesi gerektiği anlamına gelmez. Devralma, nesnenin bir üst sınıf nesne olarak davranması için alt sınıfa eklenen şeyler gerektiğinde yardımcı olabilir. Örneğin, InternalCombustionEnginetüretilmiş bir sınıfı olan bir temel sınıfı düşünün GasolineEngine. İkincisi, temel sınıfın eksik olduğu bujiler gibi şeyler ekler, ancak bir şey olarak kullanmak InternalCombustionEnginebujilerin kullanılmasına neden olur.
supercat

61

Miras özellikle usul topraklarından gelen oldukça caziptir ve genellikle aldatıcı derecede zarif görünür. Demek istediğim tek yapmam gereken bu işlevselliği başka bir sınıfa eklemek, değil mi? Sorunlardan biri,

kalıtım muhtemelen sahip olabileceğiniz en kötü bağlantıdır

Temel sınıfınız, uygulama ayrıntılarını korumalı üyeler biçiminde alt sınıflara göstererek kapsüllemeyi keser. Bu, sisteminizi sert ve kırılgan hale getirir. Bununla birlikte, daha trajik bir kusur, yeni alt sınıfın, miras zincirinin tüm bagajını ve görüşünü beraberinde getirmesidir.

Kalıtım Kötülük: DataAnnotationsModelBinder'ın Destansı Başarısızlığı makalesi, C # 'da bunun bir örneğini gösterir. Kompozisyonun ne zaman kullanılması gerektiğini ve nasıl yeniden düzenlenebileceğini gösteren kalıtım kullanımını gösterir.


Kalıtım iyi veya kötü değildir, sadece özel bir Kompozisyon örneğidir. Gerçekten de, alt sınıf, üst sınıfa benzer bir işlevsellik uyguluyor. Önerilen alt sınıfınız yeniden uygulanmıyorsa, yalnızca üst sınıfın işlevselliğini kullanıyorsa , Devralma'yı yanlış kullandınız. Bu programcının hatası, Kalıtım üzerine bir yansıması değil.
iPherian

42

Java veya C # 'da, bir nesne somutlaştırıldıktan sonra türünü değiştiremez.

Bu nedenle, nesnenizin farklı bir nesne olarak görünmesi veya bir nesne durumuna veya koşullarına bağlı olarak farklı davranması gerekiyorsa, Kompozisyon'u kullanın : Durum ve Strateji Tasarım Kalıplarına bakın.

Nesnenin aynı türden olması gerekiyorsa, Devralma veya uygulama arabirimleri kullanın .


10
+1 Kalıtımın çoğu durumda işe yaradığını daha az buldum. Paylaşılan / devralınan arayüzleri ve nesnelerin kompozisyonunu tercih ederim .... ya da toplama denir mi? Bana sorma, EE derecem var !!
kenny

Bunun "miras üzerine kompozisyon" un uygulandığı en yaygın senaryo olduğuna inanıyorum çünkü her ikisi de teoriye uyuyor olabilir. Örneğin, bir pazarlama sisteminde bir Client. Sonra, yeni bir PreferredClientpop-up konsepti daha sonra ortaya çıkıyor. Miras PreferredClientalmalı Clientmı? Tercih edilen bir müşteri bir 'müşteri daha sonra, değil mi? Çok hızlı değil ... dediğin gibi nesneler çalışma zamanında sınıflarını değiştiremez. client.makePreferred()Operasyonu nasıl modelleyersiniz ? Belki de cevap, eksik bir kavramla kompozisyon kullanmaktır, Accountbelki de?
plalx

Farklı türde Clientsınıflara sahip olmak yerine , belki Accountde bir StandardAccountya da bir olabilecek bir kavramını kapsayan bir tane var PreferredAccount...
plalx

40

Burada tatmin edici bir cevap bulamadım, bu yüzden yeni bir cevap yazdım.

" Kompozisyonun miras yerine tercih edilmesini " anlamak için , öncelikle bu kısaltılmış deyimde atlanan varsayımı geri almamız gerekir.

Kalıtımın iki faydası vardır: alt tipleme ve alt sınıflama

  1. Alt tipleme , bir tip (arayüz) imzasına, yani bir dizi API'ye uymak anlamına gelir ve alt tip polimorfizmi elde etmek için imzanın bir kısmını geçersiz kılabilir.

  2. Alt sınıflandırma yöntem uygulamalarının örtülü olarak yeniden kullanılması anlamına gelir.

İki fayda ile miras yapmak için iki farklı amaç gelir: alt tip odaklı ve kod yeniden kullanım odaklı.

Eğer kodun yeniden kullanımı tek amaçsa, alt sınıflar ihtiyaç duyduklarından bir tane daha verebilir, yani üst sınıfın bazı genel yöntemleri çocuk sınıfı için fazla bir anlam ifade etmez. Bu durumda, bileşimi miras yerine tercih etmek yerine, bileşim talep edilir . Burası aynı zamanda "is-a" vs. "has-a" kavramının geldiği yerdir.

Bu yüzden sadece alt tipleme yapıldığında, yani yeni sınıfı daha sonra polimorfik bir şekilde kullanmak için, kalıtım veya kompozisyon seçme problemiyle karşı karşıyayız. Tartışılan kısaltılmış deyimde atlanan varsayım budur.

Alt tip, bir tip imzasına uymaktır, bu da kompozisyonun her zaman daha az miktarda API içermemesi gerektiği anlamına gelir. Şimdi takaslar başlıyor:

  1. Devralma, geçersiz kılınmazsa doğrudan kodun yeniden kullanılmasını sağlarken, kompozisyonun basit bir temsilci görevi olsa bile her API'yi yeniden kodlaması gerekir.

  2. Kalıtım , iç polimorfik bölge yoluyla basit bir açık özyineleme sağlar this, yani başka bir üye işlevinde, kamusal veya özel ( cesaret kırılmış olsa da) geçersiz kılma yöntemini (hatta türünü ) çağırır . Açık özyineleme kompozisyon yoluyla simüle edilebilir , ancak ekstra çaba gerektirir ve her zaman geçerli olmayabilir (?). Tekrarlanan bir soruya verilen bu cevap benzer bir şeyden bahsediyor.

  3. Miras korunan üyeleri ortaya çıkarır . Bu, üst sınıfın kapsüllenmesini keser ve alt sınıf tarafından kullanılırsa, çocuk ve üst öğesi arasında başka bir bağımlılık ortaya çıkar.

  4. Kompozisyon, kontrolün tersine dönme davranışına sahiptir ve dekoratör , dekoratör deseninde ve proxy deseninde gösterildiği gibi dinamik olarak enjekte edilebilir .

  5. Kompozisyon, birleştirici odaklı programlama avantajına sahiptir , yani bileşik desen gibi çalışır .

  6. Kompozisyon hemen bir arayüze programlamayı takip eder .

  7. Kompozisyon, kolay çoklu kalıtım avantajına sahiptir .

Yukarıdaki değişimler göz önüne alındığında, bileşimi kalıtım yerine tercih ediyoruz . Bununla birlikte, sıkı bir şekilde ilişkili sınıflar için, yani örtük kodun yeniden kullanımı gerçekten fayda sağladığında veya açık özyinelemenin sihirli gücü istendiğinde, miras seçim olacaktır.


34

Şahsen kompozisyonu kalıtım yerine tercih etmeyi öğrendim. Kompozisyon ile çözemediğiniz miras ile çözebileceğiniz programatik bir sorun yoktur; ancak bazı durumlarda Arayüzler (Java) veya Protokoller (Obj-C) kullanmanız gerekebilir. C ++ böyle bir şey bilmediğinden, soyut temel sınıfları kullanmanız gerekir, bu da C ++ 'da kalıtımdan tamamen kurtulamayacağınız anlamına gelir.

Kompozisyon genellikle daha mantıklıdır, daha iyi soyutlama, daha iyi kapsülleme, daha iyi kod kullanımı (özellikle çok büyük projelerde) sağlar ve sadece kodunuzun herhangi bir yerinde izole bir değişiklik yaptığınız için uzaktan herhangi bir şeyi kırma olasılığı daha düşüktür. Ayrıca , genellikle " Bir sınıfın değişmesi için asla birden fazla sebep olmamalı " şeklinde özetlenen " Tek Sorumluluk İlkesi " ni desteklemeyi kolaylaştırır ve her sınıfın belirli bir amaç için var olduğu ve sadece amacı ile doğrudan ilgili yöntemler vardır. Ayrıca çok sığ bir miras ağacına sahip olmak, projeniz gerçekten büyük olmaya başladığında bile genel bakışı korumayı çok daha kolay hale getirir. Birçok insan kalıtımın gerçek dünyamızı temsil ettiğini düşünüyoroldukça iyi, ama gerçek bu değil. Gerçek dünya kalıtımdan çok daha fazla kompozisyon kullanır. Elinizde tutabileceğiniz hemen hemen her gerçek dünya nesnesi, diğer daha küçük gerçek dünya nesnelerinden oluşmuştur.

Yine de kompozisyonun dezavantajları var. Kalıtımı tamamen atlarsanız ve yalnızca kompozisyona odaklanırsanız, kalıtım kullandıysanız gerekli olmayan birkaç ekstra kod satırı yazmanız gerektiğini fark edeceksiniz. Ayrıca bazen kendinizi tekrar etmek zorunda kalırsınız ve bu KURU Prensibini ihlal eder(DRY = Kendinizi Tekrarlamayın). Ayrıca kompozisyon genellikle temsilci seçme gerektirir ve bir yöntem yalnızca bu çağrıyı çevreleyen başka bir kod içermeyen başka bir nesnenin başka bir yöntemini çağırır. Bu tür "çift yöntem çağrıları" (kolayca üçlü veya dörtlü yöntem çağrılarına kadar uzayabilir ve bundan daha da uzak olabilir) mirastan çok daha kötü bir performansa sahiptir, burada ebeveyninizin bir yöntemini miras alırsınız. Miras alınan bir yöntemi çağırmak, miras alınmamış bir yöntemi çağırmak kadar hızlı olabilir veya biraz daha yavaş olabilir, ancak yine de birbirini izleyen iki yöntem çağrısından daha hızlıdır.

Çoğu OO dilinin birden fazla kalıtıma izin vermediğini fark etmiş olabilirsiniz. Birden fazla mirasın size gerçekten bir şey satın alabileceği birkaç durum olsa da, bunlar kuraldan ziyade istisnalardır. "Birden fazla kalıtımın bu sorunu çözmek için gerçekten harika bir özellik olacağını" düşündüğünüz bir durumla karşılaştığınızda, genellikle mirasın tamamen yeniden düşünülmesi gereken bir noktadasınız, çünkü birkaç ek kod satırı gerektirebilir , kompozisyona dayanan bir çözüm genellikle çok daha zarif, esnek ve geleceğe dönük bir kanıt olacaktır.

Kalıtım gerçekten harika bir özellik, ama son birkaç yıldır aşırı kullanıldığından korkuyorum. İnsanlar kalıtımı, aslında bir çivi, vida ya da belki de tamamen farklı bir şey olup olmadığına bakılmaksızın, her şeyi çivi çakabilen bir çekiç olarak kabul ettiler.


"Birçok insan mirasın gerçek dünyamızı oldukça iyi temsil ettiğini düşünüyor, ama gerçek bu değil." Bu kadar! Dünyadaki tüm programlama derslerinin aksine, gerçek dünya nesnelerini miras zincirleri olarak modellemek uzun vadede muhtemelen kötü bir fikirdir. Kalıtımı sadece inanılmaz derecede açık, doğuştan gelen, basit bir ilişki olduğunda kullanmalısınız. Gibi TextFilebir File.
neonblitzer

25

Genel kuralım: Kalıtım kullanmadan önce, kompozisyonun daha anlamlı olup olmadığını düşünün.

Sebep: Alt sınıflandırma genellikle daha fazla karmaşıklık ve bağlılık anlamına gelir, yani hata yapmadan değiştirmek, sürdürmek ve ölçeklendirmek daha zordur.

Sun'dan Tim Boudreau'dan çok daha eksiksiz ve somut bir cevap :

Gördüğüm gibi miras kullanımıyla ilgili yaygın sorunlar:

  • Masum eylemler beklenmedik sonuçlara yol açabilir - Bunun klasik örneği, alt sınıf örneği alanları başlatılmadan önce üst sınıf yapıcısından geçersiz kılınabilen yöntemlere çağrıdır. Mükemmel bir dünyada kimse bunu yapmazdı. Bu mükemmel bir dünya değil.
  • Alt sınıflar için yöntem çağrılarının sırası hakkında varsayımlar yapmak ve bu tür varsayımlar, üst sınıf zaman içinde gelişebilirse sabit olma eğilimi göstermez. Ayrıca ekmek kızartma makinem ve cezve benzetimime de bakınız .
  • Sınıflar ağırlaşır - üst sınıfınızın yapıcısında ne işe yaradığını veya ne kadar bellek kullanacağını bilmeniz gerekmez. Bu yüzden, masum olacak hafif bir nesne inşa etmek düşündüğünüzden çok daha pahalı olabilir ve üst sınıf gelişirse bu zamanla değişebilir
  • Alt sınıfların patlamasını teşvik eder . Sınıf yüklemesi zaman, daha fazla sınıf hafıza gerektirir. NetBeans ölçeğinde bir uygulamayla uğraşana kadar bu bir sorun olmayabilir, ancak orada, örneğin menülerin yavaş olmasıyla ilgili gerçek sorunlar yaşadık çünkü bir menünün ilk ekranı büyük sınıf yüklemesini tetikledi. Bunu daha bildirimsel sözdizimine ve diğer tekniklere geçerek düzelttik, ancak düzeltilmesi de zaman aldı.
  • Daha sonra bir şeyleri değiştirmeyi zorlaştırır - eğer bir sınıfı kamuya açıkladıysanız, üst sınıfı değiştirmek alt sınıfları kıracaktır - kodu herkese açık hale getirdikten sonra evlendiğiniz bir seçimdir. Dolayısıyla, üst sınıfınızdaki gerçek işlevselliği değiştirmiyorsanız, ihtiyacınız olan şeyi genişletmek yerine, daha sonra kullanırsanız, şeyleri değiştirme özgürlüğüne sahip olursunuz. Örneğin, JPanel alt sınıfını ele alalım - bu genellikle yanlıştır; ve alt sınıf bir yerde herkese açıksa, asla bu kararı tekrar gözden geçirme şansınız olmaz. JComponent getThePanel () olarak erişilirse, yine de yapabilirsiniz (ipucu: API'nizdeki bileşenler için modelleri ortaya çıkarın).
  • Nesne hiyerarşileri ölçeklenmemektedir (ya da daha sonra ölçeklendirilmeleri önceden planlamaktan daha zordur) - bu klasik "çok fazla katman" problemidir. Aşağıda bunu ve AskTheOracle deseninin nasıl çözebileceğini ele alacağım (OOP saflarını rahatsız edebilir).

...

Ne yapacağımı almam, mirasa izin verirseniz, bir tahıl tanesi ile alabileceğiniz:

  • Sabitler dışında hiçbir alanı gösterme
  • Yöntemler soyut veya nihai olmalıdır
  • Üst sınıf yapıcıdan yöntem çağırma

...

tüm bunlar küçük projeler için büyük projelerden daha az, özel sınıflar için ise kamu projelerinden daha az geçerlidir


25

Kompozisyonu kalıtım yerine neden tercih etmelisiniz?

Diğer cevaplara bakın.

Ne zaman miras kullanabilirsiniz?

Aşağıdaki cümle doğru olduğunda genellikle bir sınıfın bir sınıfı Bardevralabileceği söylenir Foo:

  1. bir bar foo

Ne yazık ki, yukarıdaki test tek başına güvenilir değildir. Bunun yerine aşağıdakileri kullanın:

  1. bir bar bir foo ve
  2. barlar foos'un yapabileceği her şeyi yapabilir.

Tüm bu ilk test olmasını sağlar getters arasında Fooyer yapmak anlamında Bar(= paylaşılan özellikleri), ikinci testi emin olur ise tüm belirleyiciler arasında Fooyer yapmak anlamında Bar(= paylaşılan işlevselliği).

Örnek 1: Köpek -> Hayvan

Köpek bir hayvandır VE köpekler hayvanların yapabileceği her şeyi yapabilir (nefes alma, ölme vb.). Bu nedenle, sınıf Dog edebilirsiniz sınıf miras Animal.

Örnek 2: Daire - / -> Elips

Daire, bir elipstir, ancak daireler elipslerin yapabileceği her şeyi yapamaz. Örneğin, elipsler gerilebilirken daireler uzayamaz. Bu nedenle, sınıf Circle edemez sınıf miras Ellipse.

Buna Çember Elips problemi denir , ki bu gerçekten bir problem değildir, sadece ilk testin kalıtımın mümkün olduğu sonucuna varmak için yeterli olmadığının açık bir kanıtıdır. Özellikle, bu örnek türetilmiş sınıfların temel sınıfların işlevselliğini genişletmesi gerektiğini , hiçbir zaman kısıtlamadığını vurgulamaktadır . Aksi takdirde, temel sınıf polimorfik olarak kullanılamaz.

Ne zaman miras almalısınız?

Eğer bile olabilir kullanmak kalıtımı anlamına gelmez gerekir : bileşimini kullanarak her zaman bir seçenektir. Kalıtım, örtük kodun yeniden kullanılmasına ve dinamik gönderime izin veren güçlü bir araçtır, ancak birkaç dezavantajla birlikte gelir, bu yüzden kompozisyon genellikle tercih edilir. Miras ve kompozisyon arasındaki ödünleşimler açık değildir ve bence en iyi şekilde LCN'nin cevabında açıklanmaktadır .

Genel bir kural olarak, polimorfik kullanımın çok yaygın olması beklendiğinde bileşime göre kalıtım seçme eğilimindeyim, bu durumda dinamik gönderimin gücü çok daha okunabilir ve zarif bir API'ye yol açabilir. Örneğin, WidgetGUI çerçevelerinde bir polimorfik sınıfa veya NodeXML kütüphanelerinde bir polimorfik sınıfa sahip olmak, yalnızca kompozisyona dayalı bir çözümle sahip olduğunuzdan çok daha okunabilir ve sezgisel bir API'ye sahip olmasına izin verir.

Liskov İkame İlkesi

Bildiğiniz gibi, mirasın mümkün olup olmadığını belirlemek için kullanılan başka bir yönteme Liskov İkame İlkesi denir :

İşaretçiler veya temel sınıflara başvurular kullanan işlevler, türetilmiş sınıfların nesnelerini bilmeden kullanabilmelidir.

Temel olarak bu, temel sınıf polimorfik olarak kullanılabiliyorsa kalıtımın mümkün olduğu anlamına gelir, bu da testimize eşdeğer olduğuna inanıyorum "bir çubuk bir foo ve çubuklar foos'un yapabileceği her şeyi yapabilir".


Daire-elips ve kare-dikdörtgen senaryolar kötü örneklerdir. Alt sınıflar, üst sınıflarından her zaman daha karmaşıktır, bu nedenle sorun çözülür. Bu sorun, ilişki ters çevrilerek çözülür. Bir elips bir daireden, bir dikdörtgen de bir kareden türetilir. Bu senaryolarda kompozisyon kullanmak son derece aptalca.
Bulanık Mantık

@FuzzyLogic Kabul etti, ama aslında, görevim bu durumda kompozisyon kullanmayı asla savunmuyor. Sadece daire-elips probleminin, "el-a" nın neden Circle'ın Elips'ten türetmesi gerektiği sonucuna varmak için tek başına iyi bir test olmadığının harika bir örneği olduğunu söyledim. Aslında, Circle'ın LSP ihlali nedeniyle Elips'ten türetmemesi gerektiğine karar verdikten sonra, olası seçenekler ilişkiyi tersine çevirmek veya kompozisyon kullanmak veya şablon sınıflarını kullanmak veya ek sınıflar veya yardımcı işlevler içeren daha karmaşık bir tasarım kullanmaktır, vs ... ve karar açıkça duruma göre alınmalıdır.
Boris Dalstein

1
@FuzzyLogic Ve eğer Circle-Ellipse özel durumu için ne savunacağımı merak ediyorsanız: Circle sınıfını uygulamamayı savunurdum. İlişkinin ters çevrilmesiyle ilgili sorun, aynı zamanda LSP'yi de ihlal etmesidir: işlevi hayal edin computeArea(Circle* c) { return pi * square(c->radius()); }. Bir Elips'ten geçilirse açıkça kırılır (radius () ne anlama geliyor?). Elips bir Daire değildir ve bu nedenle Daire'den türememelidir.
Boris Dalstein

computeArea(Circle *c) { return pi * width * height / 4.0; }Şimdi genel.
Bulanık Mantık

2
@FuzzyLogic Kabul etmiyorum: Bunun Circle sınıfının türetilmiş sınıf Elips'in varlığını öngördüğü ve bu nedenle width()ve height()? Şimdi bir kütüphane kullanıcısı "EggShape" adlı başka bir sınıf oluşturmaya karar verirse ne olur? Ayrıca "Çember" den mi türetilmeli? Tabii ki değil. Bir yumurta şekli bir daire değildir ve bir elips de bir daire değildir, bu yüzden LSP'yi kırdığı için hiçbiri Circle'dan türememelidir. Bir Circle * sınıfında işlem yapan yöntemler, bir dairenin ne olduğu hakkında güçlü varsayımlar yapar ve bu varsayımları kırmak neredeyse kesinlikle hatalara yol açacaktır.
Boris Dalstein

19

Kalıtım çok güçlüdür, ancak zorlayamazsınız (bkz: daire elips problemi ). Eğer gerçek bir “is-a” alt tipi ilişkisinden tam olarak emin olamıyorsanız, kompozisyona gitmek en iyisidir.


15

Kalıtım, bir alt sınıf ile süper sınıf arasında güçlü bir ilişki yaratır; alt sınıf süper sınıfın uygulama detaylarının farkında olmalıdır. Süper sınıfı yaratmak, nasıl genişletilebileceğini düşünmeniz gerektiğinde çok daha zordur. Sınıf değişmezlerini dikkatle belgelemeniz ve başka hangi yöntemlerin geçersiz kılınabilecek yöntemlerin dahili olarak kullanıldığını belirtmeniz gerekir.

Hiyerarşi gerçekten bir ilişki ilişkisini temsil ediyorsa, kalıtım bazen yararlıdır. Sınıfların modifikasyon için kapalı ancak genişletmeye açık olması gerektiğini ifade eden Açık-Kapalı Prensibi ile ilgilidir. Bu şekilde polimorfizme sahip olabilirsiniz; süper tür ve yöntemleriyle ilgilenen genel bir yönteme sahip olmak, ancak dinamik gönderme yoluyla alt sınıf yöntemi çağrılır. Bu esnektir ve yazılımda gerekli olan (uygulama ayrıntıları hakkında daha az bilgi edinmek) dolaylı yol oluşturmaya yardımcı olur.

Kalıtım kolayca aşırı kullanılır ve sınıflar arasında sert bağımlılıklar ile ek karmaşıklık yaratır. Ayrıca, bir programın yürütülmesi sırasında neler olduğunu anlamak, katmanlar ve yöntem çağrılarının dinamik seçimi nedeniyle oldukça zorlaşır.

Ben beste varsayılan olarak kullanmanızı öneririm. Daha modülerdir ve geç bağlama avantajı sağlar (bileşeni dinamik olarak değiştirebilirsiniz). Ayrıca işleri ayrı ayrı test etmek daha kolaydır. Ve bir sınıftan bir yöntem kullanmanız gerekiyorsa, belirli bir formda olmak zorunda değilsiniz (Liskov İkame Prensibi).


3
Polimorfizme ulaşmanın tek yolu kalıtımın olmadığını belirtmek gerekir. Dekoratör Kalıbı, kompozisyon yoluyla polimorfizm görünümünü sağlar.
BitMask777

1
@ BitMask777: Alt tip polimorfizm sadece bir çeşit polimorfizm, diğeri parametrik polimorfizm, bunun için mirasa ihtiyacınız yok. Daha da önemlisi: kalıtımdan bahsederken, kişi sınıfsal kalıtım anlamına gelir; Birden çok sınıf için ortak bir arabirime sahip olarak alt tip polimorfizminiz olabilir ve kalıtım problemlerini alamazsınız.
egaga

2
@engaga: Yorumunuzu Inheritance is sometimes useful... That way you can have polymorphismkalıtım ve polimorfizm (bağlam dikkate alındığında alt tipleme) kavramlarını birbirine bağlayan olarak yorumladım . Yorumum, yorumunuzda neleri açıkladığınıza dikkat çekmeyi amaçladı: kalıtım polimorfizmi uygulamanın tek yolu değildir ve aslında kompozisyon ve kalıtım arasında karar verirken mutlaka belirleyici faktör değildir.
BitMask777

15

Bir uçağın sadece iki parçası olduğunu varsayalım: motor ve kanatlar.
Bir uçak sınıfı tasarlamanın iki yolu var.

Class Aircraft extends Engine{
  var wings;
}

Artık uçağınız sabit kanatlara sahip olarak başlayabilir
ve onları anında döner kanatlara çevirebilir . Aslında
kanatlı bir motor. Peki ya
motoru anında değiştirmek istersem ne olur ?

Temel sınıf, Enginebir mutatörün
özelliklerini değiştirmek için açığa çıkarır veya ben şu şekilde yeniden tasarlarım Aircraft:

Class Aircraft {
  var wings;
  var engine;
}

Şimdi motorumu anında değiştirebilirim.


Gönderiniz daha önce dikkate almadığım bir noktayı getiriyor - ateşli silah gibi bir şey üzerinde birden fazla parçalı mekanik nesneler benzetmenize devam etmek için, genellikle seri numarası olarak kabul edilen bir seri numarası ile işaretlenmiş bir bölüm var. bir bütün olarak ateşli silahın (bir tabanca için, genellikle çerçeve olurdu). Biri diğer tüm parçaları değiştirebilir ve hala aynı ateşli silahlara sahip olabilir, ancak çerçeve çatlarsa ve değiştirilmesi gerekiyorsa, orijinal tabancadaki diğer tüm parçalarla yeni bir çerçeve monte edilmesinin sonucu yeni bir tabanca olacaktır. Unutmayın ...
Supercat

... bir silahın birden fazla parçasının üzerinde işaretlenmiş seri numaraları olabilmesi, silahın birden fazla kimliğe sahip olabileceği anlamına gelmez. Sadece şasideki seri numarası tabancayı tanımlar; başka herhangi bir parçadaki seri numarası, bu parçaların hangi silahla monte edilmek üzere üretildiğini, herhangi bir zamanda monte edildikleri silah olmayabilir.
supercat


7

Temel sınıf API'sını "kopyalamak" / göstermek istediğinizde, kalıtım kullanırsınız. Yalnızca "kopyalamak" işlevini kullanmak istediğinizde temsilci seçme özelliğini kullanın.

Buna bir örnek: Listeden bir Yığın oluşturmak istiyorsunuz. Yığın yalnızca pop, push ve peek özelliklerine sahiptir. Yığın içinde push_back, push_front, removeAt, et al.-tür işlevsellik istemediğiniz göz önüne alındığında miras kullanmamalısınız.


7

Bu iki yol birlikte iyi yaşayabilir ve aslında birbirlerini destekleyebilir.

Kompozisyon sadece modüler oynuyor: üst sınıfa benzer bir arayüz oluşturuyorsunuz, yeni nesne oluşturuyorsunuz ve çağrıları devrediyorsunuz. Bu nesnelerin birbirini tanıması gerekmiyorsa, kompozisyonun kullanımı oldukça güvenli ve kolaydır. Burada çok fazla olasılık var.

Ancak, ana sınıfın herhangi bir nedenden dolayı deneyimsiz programcı için "çocuk sınıfı" tarafından sağlanan işlevlere erişmesi gerekiyorsa, kalıtım kullanmak için harika bir yer gibi görünebilir. Üst sınıf, alt sınıf tarafından üzerine yazılan kendi soyut "foo ()" adını verebilir ve daha sonra soyut tabana değer verebilir.

Güzel bir fikir gibi görünüyor, ancak birçok durumda, sınıfa, bazı sınıfı temel sınıftan miras almaktan ziyade, foo () uygulayan bir nesne verin (veya foo () sağlanan değeri manuel olarak ayarlayın) belirtilecek foo () işlevi.

Neden?

Çünkü kalıtım, bilgiyi taşımanın zayıf bir yoludur .

Kompozisyonun burada gerçek bir kenarı vardır: ilişki tersine çevrilebilir: "ana sınıf" veya "soyut işçi", belirli bir arabirimi uygulayan herhangi bir özel "alt" nesneyi toplayabilir + herhangi bir alt öğe, kabul eden herhangi bir başka ebeveyn türünün içinde ayarlanabilir. bu tip . Ve herhangi bir sayıda nesne olabilir, örneğin MergeSort veya QuickSort soyut bir Compare -interface uygulayan herhangi bir nesne listesini sıralayabilir. Ya da başka bir deyişle: "foo ()" uygulayan herhangi bir nesne grubu ve "foo ()" olan nesneleri kullanabilen diğer nesne grubu birlikte oynayabilir.

Kalıtım kullanmak için üç gerçek neden düşünebilirim:

  1. Aynı arabirime sahip birçok sınıfınız var ve bunları yazmak için zaman kazanmak istiyorsunuz
  2. Her nesne için aynı Temel Sınıfı kullanmalısınız
  3. Her durumda herkese açık olamayacak özel değişkenleri değiştirmeniz gerekir

Bunlar doğruysa, muhtemelen miras kullanmak gerekir.

Sebep 1 kullanımında kötü bir şey yoktur, nesnelerinizde sağlam bir arayüze sahip olmak çok iyi bir şeydir. Bu, kompozisyon kullanılarak veya kalıtımla yapılabilir, sorun değil - bu arayüz basitse ve değişmezse. Genellikle kalıtım burada oldukça etkilidir.

Nedeni 2 numara ise biraz zorlaşır. Gerçekten sadece aynı temel sınıfı kullanmanız mı gerekiyor? Genel olarak, sadece aynı temel sınıfı kullanmak yeterince iyi değildir, ancak çerçevenizin bir gereği olabilir, önlenemeyen bir tasarım değerlendirmesi.

Ancak, özel değişkenleri, durum 3'ü kullanmak istiyorsanız, sorun yaşayabilirsiniz. Global değişkenleri güvensiz olarak görüyorsanız, özel değişkenlere de güvensiz olmak için miras kullanmayı düşünmelisiniz . Unutmayın, küresel değişkenlerin hepsi kötü değil - veritabanları aslında büyük küresel değişkenler kümesidir. Ama eğer halledebilirsen, o zaman oldukça iyi.


7

Bu soruyu daha yeni programcılar için farklı bir perspektiften ele almak için:

Kalıtım genellikle nesneye yönelik programlamayı öğrendiğimizde erken öğretilir, bu nedenle ortak bir soruna kolay bir çözüm olarak görülür.

Tüm ortak işlevlere ihtiyaç duyan üç sınıfım var. Eğer bir temel sınıf yazıp hepsinden miras alırsam, o zaman hepsinin bu işlevselliği olacak ve sadece bir yerde tutmam gerekecek.

Kulağa harika geliyor, ancak pratikte birkaç nedenden ötürü neredeyse hiç, asla işe yaramaz:

  • Sınıflarımızın sahip olmasını istediğimiz başka fonksiyonlar olduğunu keşfediyoruz. Sınıflara işlevsellik eklemenin yolu miras yoluylaysa, karar vermeliyiz - ondan miras kalan her sınıfın bu işlevselliğe ihtiyacı olmasa da, onu mevcut temel sınıfa ekliyor muyuz? Başka bir temel sınıf oluşturuyor muyuz? Peki ya diğer temel sınıftan zaten miras alınan sınıflar?
  • Temel sınıfımızdan miras kalan sınıflardan sadece biri için, temel sınıfın biraz farklı davranmasını istediğimizi keşfediyoruz. Şimdi geri dönüp baz sınıfımızla uğraşıyoruz, belki bazı sanal yöntemler ekliyoruz veya daha da kötüsü, "A tipi miras alırsam, bunu yap, ama B tipi miras alıyorsam, ." Bu birçok nedenden dolayı kötü. Birincisi, temel sınıfı her değiştirdiğimizde, miras alınan her sınıfı etkili bir şekilde değiştiriyoruz. Bu yüzden A, B, C ve D sınıflarını gerçekten değiştiriyoruz çünkü A sınıfında biraz farklı bir davranışa ihtiyacımız var. Sandığımız kadar dikkatli olduğumuzdan, bu sınıflardan birini, bununla hiçbir ilgisi olmayan nedenlerden dolayı kırabiliriz. sınıflar.
  • Tüm bu sınıfları neden birbirinden miras almaya karar verdiğimizi bilebiliriz, ancak kodumuzu korumak zorunda olan başka biri için mantıklı gelmeyebilir. Onları zor bir seçime zorlayabiliriz - ihtiyacım olan değişikliği yapmak için gerçekten çirkin ve dağınık bir şey mi yapıyorum (önceki madde işaretine bakın) ya da sadece bir demet yeniden yazıyor muyum.

Sonunda, kodumuzu bazı zor düğümlere bağlarız ve bundan hiçbir fayda elde edemeyiz, ancak “Harika, kalıtım hakkında öğrendim ve şimdi kullandım” diyoruz. Bu küçümseyici değildir, çünkü hepimiz yaptık. Ama hepimiz yaptık çünkü kimse bize söylemedi.

Birisi bana "miras üzerine iyilik kompozisyonu" açıklar anlatmaz, miras kullanarak sınıflar arasındaki işlevselliği her paylaşmaya çalıştığımda bunu düşündüm ve çoğu zaman gerçekten iyi çalışmadığını fark ettim.

Panzehir Tek Sorumluluk İlkesidir . Bunu bir kısıtlama olarak düşünün. Benim sınıf gerekir bir şey yapmak. Ben gerekir benim sınıf nasılsa bir şey yapar tanımlayan bir ad verebilir. (Her şeyin istisnası vardır, ancak mutlak kurallar bazen öğrenirken daha iyidir.) Sonuç olarak adlandırılan bir temel sınıf yazamıyorum ObjectBaseThatContainsVariousFunctionsNeededByDifferentClasses. İhtiyacım olan her hangi bir işlevsellik kendi sınıfında olmalıdır ve daha sonra bu işlevselliğe ihtiyaç duyan diğer sınıflar, bu sınıftan miras alınamaz , ona bağlı olabilir .

Aşırı basitleştirme riski altında, bu kompozisyon - birlikte çalışmak için birden fazla sınıf oluşturmak. Ve bu alışkanlığı oluşturduktan sonra, kalıtım kullanmaktan çok daha esnek, sürdürülebilir ve test edilebilir olduğunu görüyoruz.


Sınıfların birden fazla temel sınıfı kullanamayacağı Kalıtım üzerine kötü bir yansıma değil, belirli bir dilin yetenek eksikliğine zayıf bir yansımadır.
iPherian

Bu cevabı yazdığımızdan bu yana, bu yazı yeteneğinin eksikliğini gideren "Bob Amca" dan okudum . Asla birden fazla mirasa izin veren bir dil kullanmadım. Ama geriye dönüp baktığımda, soru "agnostik dil" olarak etiketlendi ve cevabım C # varsayıyor. Ufkumu genişletmem gerek.
Scott Hannen

6

Bir / yanında düşünülmesi gereken hususlar dışında, nesnenizin geçmesi gereken kalıtımın "derinliği" de düşünülmelidir. Beş veya altı miras seviyesinin ötesinde herhangi bir şey beklenmedik döküm ve boks / kutudan çıkarma sorunlarına neden olabilir ve bu durumlarda nesnenizi oluşturmak akıllıca olabilir.


6

Eğer varsa bir -bir olan iki sınıf (örneğin köpek köpek böyle) arasındaki ilişki, sen miras için gidin.

Öte yandan, iki sınıf (öğrencinin dersleri vardır) veya (öğretmen çalışmaları dersleri) arasında bir veya bir sıfat ilişkiniz olduğunda kompozisyon seçersiniz.


Kalıtım ve kalıtım dediniz. miras ve kompozisyon demek istemiyor musun?
trevorKirkby

Hayır. Bir köpek arayüzü de tanımlayabilir ve her köpeğin onu uygulamasına izin verdiğinizde daha fazla SOLID kodu elde edersiniz.
markus

5

Bunu anlamanın basit bir yolu, sınıfınızın bir nesnesinin üst sınıfıyla aynı arabirime sahip olması gerektiğinde kalıtımın kullanılmasıdır , böylece üst sınıfın bir nesnesi olarak işlenebilir (yayınlama). . Ayrıca, türetilmiş bir sınıf nesnesindeki işlev çağrıları kodun her yerinde aynı kalır, ancak çağrılacak özel yöntem çalışma zamanında belirlenir (yani düşük düzeyli uygulama farklıdır, yüksek düzeyli arayüz aynı kalır).

Kompozisyon, aynı arabirime sahip olmak için yeni sınıfa ihtiyacınız olmadığında kullanılmalıdır, yani sınıf uygulamasının, o sınıftaki kullanıcının bilmediği belirli yönlerini gizlemek istediğinizde kullanılmalıdır. Dolayısıyla kompozisyon daha çok kapsüllemeyi (yani uygulamayı gizlemek) destekleme yolundayken, kalıtım soyutlamayı desteklemek anlamına gelir (yani, bir şeyin basitleştirilmiş bir temsilini sağlamak, bu durumda farklı dahili tiplere sahip bir dizi tip için aynı arayüz).


Arayüzden bahsetmek için +1. Bu yaklaşımı sıklıkla var olan sınıfları gizlemek ve kompozisyon için kullanılan nesneyi alay ederek yeni sınıfımı düzgün bir şekilde test edilebilir hale getirmek için kullanıyorum. Bu, yeni nesnenin sahibinin kendisine aday üst sınıfını iletmesini gerektirir.
Kell


4

@Pavel ile hemfikirim, derken kompozisyon için yerler ve miras için yerler var.

Cevabınız bu soruların herhangi birine olumlu cevap veriyorsa, kalıtımın kullanılması gerektiğini düşünüyorum.

  • Sınıfınız polimorfizmden faydalanan bir yapının parçası mı? Örneğin, draw () adında bir yöntem bildiren bir Shape sınıfınız varsa, o zaman açıkça Circle ve Square sınıflarının Shape alt sınıfları olması gerekir, böylece istemci sınıfları belirli alt sınıflara değil Shape'ye bağlıdır.
  • Sınıfınızın başka bir sınıfta tanımlanan üst düzey etkileşimleri yeniden kullanması gerekiyor mu? Şablon yöntemi tasarım deseni miras olmadan uygulanması imkansız olurdu. Tüm genişletilebilir çerçevelerin bu modeli kullandığına inanıyorum.

Bununla birlikte, niyetiniz yalnızca kodun yeniden kullanımıyla ilgiliyse, kompozisyon büyük olasılıkla daha iyi bir tasarım seçimidir.


4

Kalıtım, kodun yeniden kullanımı için çok güçlü bir machanizmdir. Ancak uygun şekilde kullanılması gerekir. Alt sınıf da üst sınıfın bir alt türü ise, kalıtımın doğru kullanıldığını söyleyebilirim. Yukarıda belirtildiği gibi, Liskov İkame İlkesi burada kilit noktadır.

Alt sınıf, alt türle aynı değildir. Alt tür olmayan alt sınıflar oluşturabilirsiniz (ve bu, kompozisyonu kullanmanız gerektiğidir). Bir alt türün ne olduğunu anlamak için, bir türün ne olduğunu açıklamaya başlayalım.

5 sayısının tür tamsayı olduğunu söylediğimizde, 5'in bir dizi olası değere ait olduğunu belirtiyoruz (örnek olarak, Java ilkel türleri için olası değerlere bakın). Ayrıca, toplama ve çıkarma gibi değer üzerinde gerçekleştirebileceğim geçerli bir dizi yöntem olduğunu belirtiyoruz. Ve son olarak, her zaman memnun olan bir dizi özellik olduğunu belirtiyoruz, örneğin, 3 ve 5 değerlerini eklersem, sonuç olarak 8 elde edeceğim.

Başka bir örnek vermek gerekirse, soyut veri türlerini düşünün, Tamsayı kümesi ve Tamsayı listesi, tutabilecekleri değerler tamsayılarla sınırlıdır. Her ikisi de add (newValue) ve size () gibi bir dizi yöntemi destekler. Ve her ikisinin de farklı özellikleri vardır (sınıf değişmez), Setler yinelemelere izin vermezken, Liste yinelemelere izin verir (elbette her ikisinin de tatmin ettiği diğer özellikler vardır).

Alt tür ayrıca, üst tür (veya üst tür) adı verilen başka bir türle ilişkisi olan bir türdür. Alt tür, üst türün özelliklerini (değerler, yöntemler ve özellikler) karşılamalıdır. İlişki, üst türün beklendiği herhangi bir bağlamda, yürütmenin davranışını etkilemeden bir alt tür ile ikame edilebileceği anlamına gelir. Söylediklerimi örneklemek için bazı kodları görelim. Diyelim ki tamsayıların bir listesini yazıyorum (sahte bir dilde):

class List {
  data = new Array();

  Integer size() {
    return data.length;
  }

  add(Integer anInteger) {
    data[data.length] = anInteger;
  }
}

Sonra, tamsayılar kümesini tamsayılar listesinin bir alt sınıfı olarak yazarım:

class Set, inheriting from: List {
  add(Integer anInteger) {
     if (data.notContains(anInteger)) {
       super.add(anInteger);
     }
  }
}

Integers sınıfımız, Tamsayılar Listesinin bir alt sınıfıdır, ancak List sınıfının tüm özelliklerini karşılamaması nedeniyle bir alt tür değildir. Değerler ve yöntemlerin imzası tatmin edici, ancak özellikler tatmin edici değil. Add (Integer) yönteminin davranışı, üst türün özelliklerini koruyarak açıkça değiştirildi. Sınıflarınızın müşterisi açısından düşünün. Tamsayıların bir listesinin beklendiği bir dizi tamsayı alabilirler. İstemci bir değer eklemek isteyebilir ve bu değer Listede zaten mevcut olsa bile bu değeri Listeye eklemek isteyebilir. Ama eğer değer varsa bu davranışı almayacak. Onun için büyük bir sürpriz!

Bu, kalıtımın yanlış kullanımının klasik bir örneğidir. Bu durumda kompozisyon kullanın.

(bir parça: kalıtımı uygun şekilde kullanın ).


3

Duyduğum bir başparmak kuralı, kalıtımın "a-a" ilişkisi ve kompozisyonu "a-has-a" olduğunda kullanılması gerektiğidir. Bununla bile, her zaman kompozisyona yaslanmanız gerektiğini hissediyorum, çünkü çok karmaşıklığı ortadan kaldırıyor.


2

Bileşim v / s Kalıtım geniş bir konudur. Her şeyin sistemin tasarımına bağlı olduğunu düşündüğümden daha iyi olanın gerçek bir cevabı yok.

Genellikle nesne arasındaki ilişki türü bunlardan birini seçmek için daha iyi bilgi sağlar.

İlişki türü "IS-A" ilişkisi ise, Kalıtım daha iyi bir yaklaşımdır. aksi takdirde ilişki türü "HAS-A" ilişkisidir, o zaman kompozisyon daha iyi yaklaşacaktır.

Bu tamamen varlık ilişkisine bağlıdır.


2

Kompozisyon tercih edilse de, Miras ve Kompozisyonun eksilerini vurgulamak istiyorum .

Kalıtım Artıları:

  1. Mantıksal bir “ IS A” ilişkisi kurar . Eğer Araba ve Kamyon iki türü vardır Araç (temel sınıf), çocuk sınıfı A IS taban sınıfı.

    yani

    Araba bir araçtır

    Kamyon bir araçtır

  2. Kalıtım ile bir yeteneği tanımlayabilir / değiştirebilir / genişletebilirsiniz

    1. Temel sınıf hiçbir uygulama sağlamaz ve alt sınıf tüm yöntemi geçersiz kılmak zorundadır (özet) => Bir sözleşme uygulayabilirsiniz
    2. Temel sınıf varsayılan uygulama sağlar ve alt sınıf davranışı değiştirebilir => Sözleşmeyi yeniden tanımlayabilirsiniz
    3. Alt sınıf, ilk ifade olarak super.methodName () öğesini çağırarak temel sınıf uygulamasına uzantı ekler => Bir sözleşmeyi genişletebilirsiniz
    4. Temel sınıf algoritmanın yapısını tanımlar ve alt sınıf algoritmanın bir kısmını geçersiz kılar => Temel sınıf iskeletinde değişiklik yapmadan Template_method uygulayabilirsiniz

Kompozisyon Eksileri:

  1. Kalıtımda alt sınıf, IS A ilişkisi nedeniyle temel sınıf yöntemini uygulamasa bile doğrudan temel sınıf yöntemini çağırabilir . Kompozisyon kullanıyorsanız, içerilen sınıf API'sını ortaya çıkarmak için container sınıfına yöntemler eklemeniz gerekir

örneğin Eğer Araba içeren Araç ve fiyatını almak zorunda Car içinde tanımlandığında, Araç , kod aşağıdaki gibi olacaktır

class Vehicle{
     protected double getPrice(){
          // return price
     }
} 

class Car{
     Vehicle vehicle;
     protected double getPrice(){
          return vehicle.getPrice();
     }
} 

Sanırım soruya cevap vermiyor
almanegra

OP sorusuna yeniden bakabilirsiniz. Ele aldım: Her yaklaşım için hangi değiş tokuşlar var?
Ravindra babu

Bahsettiğiniz gibi, sadece "Miras ve Kompozisyonun eksileri" hakkında konuşuyorsunuz, EACH yaklaşımı veya birbiri üzerinde kullanmanız gereken durumlar için değil
almanegra

artılarını ve eksilerini takas sağlar çünkü miras artıları kompozisyon eksilerini ve kompozisyon eksilerini miras artıları olduğunu.
Ravindra babu

1

Birçok insanın söylediği gibi, önce bir "is-a" ilişkisi olup olmadığı kontrolle başlayacağım. Varsa genellikle aşağıdakileri kontrol ederim:

Temel sınıfın somutlaştırılıp yapılandırılamayacağı. Yani, temel sınıf soyut olmayabilir. Soyut değilse, genellikle kompozisyonu tercih ederim

Örneğin, 1. Muhasebeci bir Çalışandır. Ama olacak değil bir Çalışan nesne örneği olabilir çünkü miras kullanın.

Örneğin 2. Kitap bir Satış Öğesidir. Bir SellingItem somutlaştırılamaz - soyut bir kavramdır. Bu yüzden inheritacne kullanacağım. SellingItem soyut bir temel sınıftır (veya C # arayüzünde )

Bu yaklaşım hakkında ne düşünüyorsun?

Ayrıca, neden kalıtım kullanmalı?

Kalıtım kullanmanın ana nedeni bir kompozisyon şekli değildir - bu yüzden polimorfik davranış elde edebilirsiniz. Eğer polimorfizme ihtiyacınız yoksa, muhtemelen kalıtım kullanmamalısınız.

@MatthieuM. diyor /software/12439/code-smell-inheritance-abuse/12448#comment303759_12448

Kalıtımla ilgili mesele, iki dikey amaç için kullanılabilmesidir:

arayüz (polimorfizm için)

uygulama (kodun yeniden kullanımı için)

REFERANS

  1. Hangi sınıf tasarımı daha iyi?
  2. Miras ve Toplama

1
Neden 'temel sınıf soyut?' Tartışmadaki rakamlar .. LSP: Kaniş nesneleri geçerse Köpekler üzerinde çalışan tüm işlevler çalışır mı? Evet ise, Kaniş Köpek yerine kullanılabilir ve bu nedenle Köpek'ten devralabilir.
Gishu

@Gishu Teşekkürler. Kesinlikle LSP'ye bakacağım. Ama bundan önce, lütfen "temel sınıfın soyut olamayacağı kalıtımın uygun olduğu bir örnek" verebilir misiniz? Bence, kalıtım sadece temel sınıf soyut ise uygulanabilir. Temel sınıfın ayrı olarak başlatılması gerekiyorsa, kalıtım için gitmeyin. Yani, Muhasebeci bir Çalışan olmasına rağmen, miras kullanmayın.
LCJ

1
son zamanlarda WCF okuyor. .Net çerçevesindeki bir örnek, kuyrukların ThreadPool iş parçacığında çalıştığı SynchronizationContext (base + somutlaştırılabilir) örneğidir. Türevler arasında WinFormsSyncContext (UI İş Parçacığına kuyruk) ve DispatcherSyncContext (WPF Dispatcher'a kuyruk)
Gishu

@Gishu Teşekkürler. Bununla birlikte, Banka alanı, İK alanı, Perakende alanı veya diğer popüler alanlara dayalı bir senaryo sunmanız daha yararlı olacaktır.
LCJ

1
Afedersiniz. Ben bu etki alanlarına aşina değilim .. Bir önceki örnek çok genişse başka bir örnek Winforms / WPF Control sınıftır. Temel / genel kontrol somutlaştırılabilir. Türevler Liste Kutuları, Metin Kutuları, vb. Dekoratör, sarmak / süslemek istediği soyut olmayan nesneden türetilir.
Gishu

1

Kimse kalıtımla ortaya çıkabilecek elmas probleminden bahsetmiyorum .

Bir bakışta, B ve C sınıfları A'yı devralırsa ve her ikisi de X yöntemini ve dördüncü sınıf D'yi geçersiz kılarsa, hem B hem de C'den miras alır ve X'i geçersiz kılmazsa, XD'nin hangi uygulamasının kullanılması gerekiyor?

Wikipedia , bu soruda tartışılan konuyla ilgili güzel bir genel bakış sunar.


1
Eğer öyleyse D devralır B ve C değil A., o zaman sınıf A'da yer aldığı X uygulanmasını kullanırım
fabricio

1
@fabricio: teşekkürler, metni düzenledim. Bu arada, bu tür bir senaryo birden fazla sınıf mirasına izin vermeyen dillerde gerçekleşemez, değil mi?
Veverke

evet haklısın .. ve birden fazla mirasa izin veren biriyle hiç çalışmadım (elmas problemindeki örneğe göre) ..
fabricio
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.