Jawaban:
Warisan berganda (disingkat MI) berbau , yang berarti bahwa biasanya , itu dilakukan karena alasan yang buruk, dan itu akan meledak kembali di hadapan sang pengelola.
Ini berlaku untuk warisan, dan bahkan lebih benar untuk warisan ganda.
Apakah objek Anda benar-benar perlu diwarisi dari yang lain? A Car
tidak perlu mewarisi dari Engine
bekerja, atau dari Wheel
. A Car
memiliki satu Engine
dan empat Wheel
.
Jika Anda menggunakan banyak pewarisan untuk menyelesaikan masalah ini alih-alih komposisi, maka Anda telah melakukan kesalahan.
Biasanya, Anda memiliki kelas A
, lalu B
dan C
keduanya mewarisi dari A
. Dan (jangan tanya kenapa) seseorang kemudian memutuskan yang D
harus mewarisi dari B
dan C
.
Saya telah mengalami masalah seperti ini dua kali dalam 8 malam, dan itu menyenangkan untuk dilihat karena:
D
seharusnya tidak diwarisi dari keduanya B
dan C
), karena ini adalah arsitektur yang buruk (pada kenyataannya, C
seharusnya tidak ada sama sekali ...)A
hadir dua kali di kelas cucu-nya D
, dan dengan demikian, memperbarui satu bidang induk A::field
berarti memperbaharuinya dua kali (melalui B::field
dan C::field
), atau membuat sesuatu berjalan salah dan diam, kemudian (barukan pointer B::field
, dan hapus C::field
...)Menggunakan kata kunci virtual dalam C ++ untuk memenuhi syarat warisan menghindari tata letak ganda yang dijelaskan di atas jika ini bukan yang Anda inginkan, tetapi bagaimanapun, dalam pengalaman saya, Anda mungkin melakukan sesuatu yang salah ...
Dalam hierarki Objek, Anda harus mencoba menjaga hierarki sebagai Tree (node memiliki SATU induk), bukan sebagai grafik.
Masalah sebenarnya dengan Diamond of Dread di C ++ ( dengan asumsi desainnya bagus - periksa kode Anda! ), Adalah Anda harus membuat pilihan :
A
ada dua kali dalam tata letak Anda, dan apa artinya? Jika ya, berarti mewarisi dari itu dua kali.Pilihan ini melekat pada masalah, dan dalam C ++, tidak seperti bahasa lain, Anda sebenarnya dapat melakukannya tanpa dogma yang memaksa desain Anda di tingkat bahasa.
Tetapi seperti semua kekuatan, dengan kekuatan itu datang tanggung jawab: Minta desain Anda ditinjau.
Berbagai warisan nol atau satu kelas konkret, dan nol atau lebih antarmuka biasanya Oke, karena Anda tidak akan menemukan Diamond of Dread yang dijelaskan di atas. Sebenarnya, inilah yang dilakukan di Jawa.
Biasanya, apa yang Anda maksudkan ketika C mewarisi dari A
dan B
adalah bahwa pengguna dapat menggunakan C
seolah-olah itu adalah A
, dan / atau seolah-olah itu adalah B
.
Dalam C ++, sebuah antarmuka adalah kelas abstrak yang memiliki:
Warisan berganda dari nol ke satu objek nyata, dan nol atau lebih antarmuka tidak dianggap "bau" (setidaknya, tidak sebanyak).
Pertama, pola NVI dapat digunakan untuk menghasilkan antarmuka, karena kriteria sebenarnya adalah tidak memiliki status (yaitu tidak ada variabel anggota, kecuali this
). Titik antarmuka abstrak Anda adalah untuk menerbitkan kontrak ("Anda dapat memanggil saya dengan cara ini, dan dengan cara ini"), tidak lebih, tidak kurang. Keterbatasan hanya memiliki metode virtual abstrak harus menjadi pilihan desain, bukan kewajiban.
Kedua, dalam C ++, masuk akal untuk mewarisi secara virtual dari antarmuka abstrak, (bahkan dengan biaya tambahan / tipuan). Jika tidak, dan pewarisan antarmuka muncul beberapa kali dalam hierarki Anda, maka Anda akan memiliki ambiguitas.
Ketiga, orientasi objek itu bagus, tetapi bukan The Only Truth Out There TM di C ++. Gunakan alat yang tepat, dan selalu ingat Anda memiliki paradigma lain di C ++ yang menawarkan berbagai jenis solusi.
Terkadang ya.
Biasanya, C
kelas Anda mewarisi dari A
dan B
, dan A
dan B
adalah dua objek yang tidak terkait (yaitu tidak dalam hierarki yang sama, tidak ada kesamaan, konsep yang berbeda, dll.).
Misalnya, Anda dapat memiliki sistem Nodes
dengan koordinat X, Y, Z, dapat melakukan banyak perhitungan geometris (mungkin sebuah titik, bagian dari objek geometris) dan setiap Node adalah Agen Otomatis, yang dapat berkomunikasi dengan agen lain.
Mungkin Anda sudah memiliki akses ke dua perpustakaan, masing-masing dengan ruang nama sendiri (alasan lain untuk menggunakan ruang nama ... Tapi Anda menggunakan ruang nama, bukan?), Satu makhluk geo
dan makhluk lainnyaai
Jadi, Anda memiliki own::Node
turunan sendiri dari ai::Agent
dan geo::Point
.
Inilah saatnya ketika Anda harus bertanya pada diri sendiri apakah sebaiknya Anda tidak menggunakan komposisi. Jika own::Node
benar-benar baik a ai::Agent
dan a geo::Point
, maka komposisi tidak akan berfungsi.
Maka Anda akan membutuhkan banyak pewarisan, setelah Anda own::Node
berkomunikasi dengan agen lain sesuai dengan posisi mereka dalam ruang 3D.
(Anda akan mencatat bahwa ai::Agent
dan geo::Point
benar-benar, sepenuhnya, sepenuhnya tidak terkait ... Ini secara drastis mengurangi bahaya multiple inheritance)
Ada beberapa kasus lain:
this
)Terkadang Anda dapat menggunakan komposisi, dan terkadang MI lebih baik. Intinya adalah: Anda punya pilihan. Lakukan dengan bertanggung jawab (dan minta kode Anda ditinjau).
Sebagian besar waktu, dalam pengalaman saya, tidak. MI bukanlah alat yang tepat, meskipun tampaknya berfungsi, karena dapat digunakan oleh malas untuk menumpuk fitur bersama tanpa menyadari konsekuensinya (seperti membuat Car
keduanya Engine
dan a Wheel
).
Tapi terkadang, ya. Dan pada saat itu, tidak ada yang lebih baik daripada MI.
Tetapi karena MI bau, bersiaplah untuk mempertahankan arsitektur Anda dalam ulasan kode (dan mempertahankannya adalah hal yang baik, karena jika Anda tidak dapat mempertahankannya, maka Anda seharusnya tidak melakukannya).
Dari wawancara dengan Bjarne Stroustrup :
Orang-orang mengatakan dengan benar bahwa Anda tidak memerlukan banyak warisan, karena apa pun yang dapat Anda lakukan dengan banyak warisan juga dapat Anda lakukan dengan satu warisan. Anda cukup menggunakan trik delegasi yang saya sebutkan. Selain itu, Anda tidak memerlukan warisan sama sekali, karena apa pun yang Anda lakukan dengan warisan tunggal Anda juga dapat melakukannya tanpa warisan dengan meneruskan melalui kelas. Sebenarnya, Anda tidak memerlukan kelas apa pun, karena Anda dapat melakukan semuanya dengan pointer dan struktur data. Tetapi mengapa Anda ingin melakukan itu? Kapan nyaman menggunakan fasilitas bahasa? Kapan Anda lebih suka solusi? Saya telah melihat kasus-kasus di mana pewarisan berganda berguna, dan saya bahkan telah melihat kasus-kasus di mana pewarisan berganda yang cukup rumit bermanfaat. Secara umum, saya lebih suka menggunakan fasilitas yang ditawarkan oleh bahasa untuk melakukan penyelesaian masalah
const
- Saya harus menulis solusi yang kikuk (biasanya menggunakan antarmuka dan komposisi) ketika sebuah kelas benar - benar perlu memiliki varian yang bisa berubah dan tidak dapat diubah. Namun, saya tidak pernah sekali pun melewatkan banyak warisan, dan tidak pernah merasa bahwa saya harus menulis solusi karena kurangnya fitur ini. Itulah bedanya. Dalam setiap kasus yang pernah saya lihat, tidak menggunakan MI adalah pilihan desain yang lebih baik, bukan solusi.
Tidak ada alasan untuk menghindarinya dan itu bisa sangat berguna dalam situasi. Anda harus menyadari masalah potensial.
Yang terbesar adalah intan kematian:
class GrandParent;
class Parent1 : public GrandParent;
class Parent2 : public GrandParent;
class Child : public Parent1, public Parent2;
Anda sekarang memiliki dua "salinan" GrandParent di dalam Child.
C ++ telah memikirkan hal ini dan memungkinkan Anda melakukan pewarisan virtual untuk menyelesaikan masalah.
class GrandParent;
class Parent1 : public virtual GrandParent;
class Parent2 : public virtual GrandParent;
class Child : public Parent1, public Parent2;
Selalu tinjau desain Anda, pastikan Anda tidak menggunakan warisan untuk menghemat penggunaan kembali data. Jika Anda dapat mewakili hal yang sama dengan komposisi (dan biasanya Anda bisa) ini adalah pendekatan yang jauh lebih baik.
GrandParent
di Child
. Ada rasa takut terhadap MI, karena orang hanya berpikir mereka mungkin tidak mengerti aturan bahasa. Tetapi siapa pun yang tidak bisa mendapatkan aturan sederhana ini juga tidak bisa menulis program yang tidak sepele.
Lihat w: Warisan Berganda .
Warisan berganda telah menerima kritik dan karenanya tidak diterapkan dalam banyak bahasa. Kritik meliputi:
- Meningkatnya kompleksitas
- Ambiguitas semantik sering diringkas sebagai masalah intan .
- Tidak dapat mewarisi beberapa kali secara eksplisit dari satu kelas
- Urutan warisan mengubah semantik kelas.
Berbagai pewarisan dalam bahasa dengan konstruktor gaya C ++ / Java memperparah masalah pewarisan konstruktor dan rantai pengarah, sehingga menciptakan masalah pemeliharaan dan ekstensibilitas dalam bahasa ini. Objek dalam hubungan warisan dengan metode konstruksi yang sangat bervariasi sulit untuk diterapkan di bawah paradigma chaining konstruktor.
Cara modern untuk menyelesaikan ini menggunakan antarmuka (kelas abstrak murni) seperti COM dan antarmuka Java.
Saya dapat melakukan hal-hal lain menggantikan ini?
Ya kamu bisa. Saya akan mencuri dari GoF .
Warisan publik adalah hubungan IS-A, dan kadang-kadang kelas akan menjadi jenis beberapa kelas yang berbeda, dan kadang-kadang penting untuk mencerminkan hal ini.
"Mixin" juga terkadang bermanfaat. Mereka umumnya kelas kecil, biasanya tidak mewarisi dari apa pun, menyediakan fungsionalitas yang bermanfaat.
Selama hierarki warisan cukup dangkal (sebagaimana seharusnya selalu terjadi), dan dikelola dengan baik, Anda tidak mungkin mendapatkan warisan berlian yang ditakuti. Intan tidak menjadi masalah dengan semua bahasa yang menggunakan banyak pewarisan, tetapi perlakuan C ++ terhadapnya sering kali canggung dan terkadang membingungkan.
Sementara saya mengalami kasus di mana pewarisan berganda sangat berguna, mereka sebenarnya cukup langka. Ini mungkin karena saya lebih suka menggunakan metode desain lain ketika saya tidak benar-benar membutuhkan banyak pewarisan. Saya lebih suka menghindari konstruksi bahasa yang membingungkan, dan mudah untuk membuat kasus warisan di mana Anda harus membaca manual dengan sangat baik untuk mencari tahu apa yang terjadi.
Anda seharusnya tidak "menghindari" pewarisan berganda tetapi Anda harus menyadari masalah yang dapat muncul seperti 'masalah berlian' ( http://en.wikipedia.org/wiki/Diamond_problem ) dan memperlakukan kekuatan yang diberikan kepada Anda dengan hati-hati. , sebagaimana mestinya dengan semua kekuatan.
Dengan risiko menjadi sedikit abstrak, saya merasa terpikir untuk berpikir tentang pewarisan dalam kerangka teori kategori.
Jika kita memikirkan semua kelas dan panah di antara mereka yang menunjukkan hubungan warisan, maka kira-kira seperti ini
A --> B
berarti class B
berasal dari class A
. Perhatikan itu, diberikan
A --> B, B --> C
kita katakan C berasal dari B yang berasal dari A, jadi C juga dikatakan berasal dari A, jadi
A --> C
Selain itu, kami mengatakan bahwa untuk setiap kelas A
yang A
berasal dari sepele A
, dengan demikian model warisan kami memenuhi definisi kategori. Dalam bahasa yang lebih tradisional, kami memiliki kategori Class
dengan objek semua kelas dan morfisme hubungan warisan.
Itu sedikit pengaturan, tetapi dengan itu mari kita lihat Diamond of Doom kami:
C --> D
^ ^
| |
A --> B
Ini adalah diagram yang terlihat teduh, tetapi akan berhasil. Jadi D
mewarisi dari semua A
, B
, dan C
. Lebih jauh, dan semakin dekat dengan menjawab pertanyaan OP, D
juga mewarisi dari superclass manapun A
. Kita bisa menggambar diagram
C --> D --> R
^ ^
| |
A --> B
^
|
Q
Sekarang, masalah yang terkait dengan Diamond of Death di sini adalah kapan C
dan B
berbagi beberapa properti / nama metode dan hal-hal menjadi ambigu; Namun, jika kita memindahkan perilaku bersama apa pun A
maka ambiguitas menghilang.
Masukkan dalam kategori kategoris, kami ingin A
, B
dan C
menjadi seperti itu jika B
dan C
mewarisi dari Q
itu A
dapat ditulis ulang sebagai subkelas dari Q
. Ini membuat A
sesuatu yang disebut pushout .
Ada juga konstruksi simetris yang D
disebut pullback . Ini pada dasarnya adalah kelas berguna paling umum yang bisa Anda buat yang mewarisi dari keduanya B
dan C
. Artinya, jika Anda memiliki kelas lain yang R
diwarisi dari B
dan C
, maka D
adalah kelas di mana R
dapat ditulis ulang sebagai subkelas dari D
.
Memastikan tip berlian Anda adalah mundur dan pushouts memberi kami cara yang bagus untuk secara umum menangani masalah perselisihan nama atau pemeliharaan yang mungkin timbul sebaliknya.
Catatan paercebal 's jawaban terinspirasi ini sebagai nasihat-nasihatnya yang tersirat oleh model di atas mengingat bahwa kita bekerja dalam kategori penuh Kelas dari semua kelas yang mungkin.
Saya ingin menggeneralisasikan argumennya pada sesuatu yang menunjukkan betapa rumitnya hubungan pewarisan berganda yang kuat dan tidak bermasalah.
TL; DR Pikirkan hubungan warisan dalam program Anda sebagai membentuk kategori. Kemudian Anda dapat menghindari masalah Diamond of Doom dengan membuat pushout kelas yang diwariskan multiply dan simetris, membuat kelas induk yang merupakan pullback.
Kami menggunakan Eiffel. Kami memiliki MI yang sangat baik. Jangan khawatir. Tidak ada masalah. Mudah dikelola. Ada kalanya TIDAK menggunakan MI. Namun, ini lebih berguna daripada yang disadari orang karena mereka adalah: A) dalam bahasa berbahaya yang tidak mengelolanya dengan baik - ATAU - B) puas dengan bagaimana mereka telah bekerja di sekitar MI selama bertahun-tahun -OR- C) alasan lain ( terlalu banyak untuk dicantumkan, saya cukup yakin - lihat jawaban di atas).
Bagi kami, menggunakan Eiffel, MI sama wajarnya dengan yang lain dan alat halus lainnya di kotak alat. Terus terang, kami cukup khawatir bahwa tidak ada orang lain yang menggunakan Eiffel. Jangan khawatir. Kami senang dengan apa yang kami miliki dan mengundang Anda untuk melihatnya.
Saat Anda mencari: Catat keamanan Void dan pemberantasan dudukan referensi Null. Sementara kita semua menari di sekitar MI, petunjuk Anda hilang! :-)
Setiap bahasa pemrograman memiliki perlakuan yang sedikit berbeda dari pemrograman berorientasi objek dengan pro dan kontra. Versi C ++ menempatkan penekanan tepat pada kinerja dan memiliki kelemahan yang menyertainya adalah sangat mudah untuk menulis kode yang tidak valid - dan ini berlaku untuk multiple inheritance. Sebagai akibatnya ada kecenderungan untuk menjauhkan programmer dari fitur ini.
Orang lain telah menjawab pertanyaan tentang apakah warisan ganda itu tidak baik. Tetapi kita telah melihat beberapa komentar yang sedikit banyak menyiratkan bahwa alasan untuk menghindarinya adalah karena itu tidak aman. Ya dan tidak.
Seperti yang sering benar dalam C ++, jika Anda mengikuti pedoman dasar Anda dapat menggunakannya dengan aman tanpa harus "melihat-lihat" Anda terus-menerus. Ide kuncinya adalah Anda membedakan jenis definisi kelas khusus yang disebut "campuran"; kelas adalah campuran jika semua fungsi anggotanya adalah virtual (atau virtual murni). Kemudian Anda diizinkan untuk mewarisi dari satu kelas utama dan sebanyak "campuran" yang Anda suka - tetapi Anda harus mewarisi mixin dengan kata kunci "virtual". misalnya
class CounterMixin {
int count;
public:
CounterMixin() : count( 0 ) {}
virtual ~CounterMixin() {}
virtual void increment() { count += 1; }
virtual int getCount() { return count; }
};
class Foo : public Bar, virtual public CounterMixin { ..... };
Saran saya adalah bahwa jika Anda berniat menggunakan kelas sebagai kelas campuran, Anda juga mengadopsi konvensi penamaan untuk memudahkan siapa pun membaca kode untuk melihat apa yang terjadi & untuk memverifikasi Anda bermain dengan aturan pedoman dasar . Dan Anda akan menemukannya bekerja lebih baik jika mix-in Anda memiliki konstruktor default juga, hanya karena cara kerja kelas dasar virtual. Dan ingat untuk membuat semua destructor virtual juga.
Perhatikan bahwa penggunaan kata "mix-in" di sini tidak sama dengan kelas template parameterised (lihat tautan ini untuk penjelasan yang baik) tapi saya pikir ini adalah penggunaan terminologi yang adil.
Sekarang saya tidak ingin memberi kesan bahwa ini adalah satu-satunya cara untuk menggunakan banyak warisan secara aman. Hanya satu cara yang cukup mudah untuk diperiksa.
Anda harus menggunakannya dengan hati-hati, ada beberapa kasus, seperti Masalah Berlian , ketika segala sesuatunya menjadi rumit.
(sumber: learncpp.com )
Printer
seharusnya tidak menjadi PoweredDevice
. A Printer
untuk pencetakan, bukan manajemen daya. Implementasi printer tertentu mungkin harus melakukan beberapa manajemen daya, tetapi perintah manajemen daya ini tidak boleh diekspos langsung ke pengguna printer. Saya tidak bisa membayangkan penggunaan hirarki ini di dunia nyata.
Penggunaan dan Penyalahgunaan Warisan.
Artikel itu melakukan pekerjaan yang bagus untuk menjelaskan warisan, dan itu berbahaya.
Di luar pola intan, multiple inheritance cenderung membuat model objek lebih sulit untuk dipahami, yang pada gilirannya meningkatkan biaya perawatan.
Komposisi secara intrinsik mudah dipahami, dipahami, dan dijelaskan. Ini bisa membosankan untuk menulis kode, tetapi IDE yang bagus (sudah beberapa tahun sejak saya bekerja dengan Visual Studio, tapi tentu saja Java IDEs semuanya memiliki alat pengotomatisasi cara pintas komposisi yang hebat) harus membantu Anda mengatasi rintangan itu.
Juga, dalam hal pemeliharaan, "masalah intan" muncul dalam contoh turunan non-literal juga. Misalnya, jika Anda memiliki A dan B dan kelas C Anda memperpanjang keduanya, dan A memiliki metode 'makeJuice' yang membuat jus jeruk dan Anda mengembangkannya untuk membuat jus jeruk dengan twist jeruk nipis: apa yang terjadi ketika perancang untuk ' B 'menambahkan metode' makeJuice 'yang menghasilkan dan arus listrik? 'A' dan 'B' mungkin kompatibel dengan "orang tua" saat ini , tetapi itu tidak berarti mereka akan selalu seperti itu!
Secara keseluruhan, pepatah cenderung untuk menghindari warisan, dan terutama pewarisan berganda, adalah sehat. Seperti semua maksim, ada pengecualian, tetapi Anda perlu memastikan bahwa ada tanda neon hijau berkedip yang menunjuk pada setiap pengecualian yang Anda kode (dan latih otak Anda sehingga setiap kali Anda melihat pohon warisan seperti itu, Anda menggambar di neon hijau berkedip Anda sendiri tanda), dan Anda memeriksa untuk memastikan semuanya masuk akal sesekali.
what happens when the designer for 'B' adds a 'makeJuice' method which generates and electrical current?
Uhhh, Anda mendapatkan kesalahan kompilasi tentu saja (jika penggunaannya ambigu).
Masalah utama dengan MI objek konkret adalah bahwa Anda jarang memiliki objek yang secara sah harus "Menjadi A DAN menjadi B", sehingga jarang solusi yang tepat berdasarkan alasan logis. Jauh lebih sering, Anda memiliki objek C yang menaati "C dapat bertindak sebagai A atau B", yang dapat Anda capai melalui warisan antarmuka & komposisi. Tetapi jangan membuat kesalahan - warisan dari beberapa antarmuka masih MI, hanya sebagian dari itu.
Khususnya untuk C ++, kelemahan utama dari fitur ini bukanlah EXISTENCE aktual dari Multiple Inheritance, tetapi beberapa konstruksi yang memungkinkannya hampir selalu cacat. Misalnya, mewarisi banyak salinan dari objek yang sama seperti:
class B : public A, public A {};
cacat oleh DEFINISI. Diterjemahkan ke dalam bahasa Inggris ini adalah "B adalah A dan a". Jadi, bahkan dalam bahasa manusia ada ambiguitas yang parah. Apakah maksud Anda "B memiliki 2 As" atau hanya "B adalah A"? Mengizinkan kode patologis seperti itu, dan yang lebih buruk menjadikannya contoh penggunaan, tidak menyukai C ++ ketika membuat kasus untuk mempertahankan fitur dalam bahasa penerus.
Anda dapat menggunakan komposisi dalam preferensi untuk warisan.
Perasaan umum adalah komposisi itu lebih baik, dan itu sangat baik dibahas.
The general feeling is that composition is better, and it's very well discussed.
Itu tidak berarti komposisi lebih baik.
dibutuhkan 4/8 byte per kelas yang terlibat. (Satu pointer ini per kelas).
Ini mungkin tidak akan pernah menjadi perhatian, tetapi jika suatu hari Anda memiliki struktur data mikro yang dikeluarkan miliaran waktu, itu akan menjadi.