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?
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?
Yanıtlar:
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 .
TypeB, TypeA tarafından ortaya konan davranışın yalnızca bir kısmını mı istiyor? Kompozisyon ihtiyacını gösterir .
Güncelleme: Cevabım geri döndü ve şimdi Barbara Liskov'un Liskov İkame Prensibi'nden 'Bu türden miras almalıyım?'
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 .
Farkı anlarsanız, açıklamak daha kolaydır.
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.
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 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.
Ş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.
Mirasın sağladığı inkar edilemez faydalarla, burada bazı dezavantajları var.
Kalıtımın Dezavantajları:
Ö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.
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.
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.
InternalCombustionEngine
tü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 InternalCombustionEngine
bujilerin kullanılmasına neden olur.
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,
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.
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 .
Client
. Sonra, yeni bir PreferredClient
pop-up konsepti daha sonra ortaya çıkıyor. Miras PreferredClient
almalı Client
mı? 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, Account
belki de?
Client
sınıflara sahip olmak yerine , belki Account
de bir StandardAccount
ya da bir olabilecek bir kavramını kapsayan bir tane var PreferredAccount
...
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
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.
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:
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.
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.
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.
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 .
Kompozisyon, birleştirici odaklı programlama avantajına sahiptir , yani bileşik desen gibi çalışır .
Kompozisyon hemen bir arayüze programlamayı takip eder .
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.
Ş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.
TextFile
bir File
.
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
Diğer cevaplara bakın.
Aşağıdaki cümle doğru olduğunda genellikle bir sınıfın bir sınıfı Bar
devralabileceği söylenir Foo
:
- bir bar foo
Ne yazık ki, yukarıdaki test tek başına güvenilir değildir. Bunun yerine aşağıdakileri kullanın:
- bir bar bir foo ve
- barlar foos'un yapabileceği her şeyi yapabilir.
Tüm bu ilk test olmasını sağlar getters arasında Foo
yer yapmak anlamında Bar
(= paylaşılan özellikleri), ikinci testi emin olur ise tüm belirleyiciler arasında Foo
yer 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.
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, Widget
GUI çerçevelerinde bir polimorfik sınıfa veya Node
XML 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.
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".
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.
computeArea(Circle *c) { return pi * width * height / 4.0; }
Şimdi genel.
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.
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.
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).
Inheritance is sometimes useful... That way you can have polymorphism
kalı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.
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, Engine
bir 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.
Bob Amca'nın SOLID sınıf tasarım ilkelerinde Liskov İkame İlkesine bir göz atmanız gerekir . :)
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.
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:
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.
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:
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.
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.
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.
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).
Değişmezlerin numaralandırılabildiği yerlerde alt tipleme uygun ve daha güçlüdür , aksi takdirde genişletilebilirlik için fonksiyon kompozisyonu kullanın.
@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.
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.
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 ).
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.
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.
Kompozisyon tercih edilse de, Miras ve Kompozisyonun eksilerini vurgulamak istiyorum .
Kalıtım Artıları:
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
Kalıtım ile bir yeteneği tanımlayabilir / değiştirebilir / genişletebilirsiniz
Kompozisyon Eksileri:
ö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();
}
}
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
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.