Apakah ada alasan "nyata" mengapa pewarisan berganda dibenci?

123

Saya selalu menyukai gagasan memiliki banyak warisan yang didukung dalam suatu bahasa. Paling sering meskipun itu sengaja dilupakan, dan seharusnya "penggantian" adalah antarmuka. Antarmuka tidak mencakup semua tanah yang sama dengan pewarisan berganda, dan pembatasan ini kadang-kadang dapat menyebabkan lebih banyak kode boilerplate.

Satu-satunya alasan dasar yang pernah saya dengar untuk ini adalah masalah berlian dengan kelas dasar. Aku tidak bisa menerimanya. Bagi saya, itu keluar dari banyak sekali seperti, "Yah, itu mungkin untuk mengacaukannya, jadi itu secara otomatis ide yang buruk." Anda dapat mengacaukan apa pun dalam bahasa pemrograman, dan maksud saya apa pun. Saya tidak bisa menganggap ini serius, setidaknya bukan tanpa penjelasan yang lebih mendalam.

Menyadari masalah ini adalah 90% dari pertempuran. Terlebih lagi, saya pikir saya mendengar sesuatu bertahun-tahun yang lalu tentang penyelesaian untuk keperluan umum yang melibatkan algoritma "amplop" atau sesuatu seperti itu (apakah ini membunyikan bel, siapa pun?).

Mengenai masalah berlian, satu-satunya masalah yang berpotensi asli yang dapat saya pikirkan adalah jika Anda mencoba menggunakan perpustakaan pihak ketiga dan tidak dapat melihat bahwa dua kelas yang tampaknya tidak terkait di perpustakaan itu memiliki kelas dasar yang sama, tetapi selain itu dokumentasi, fitur bahasa yang sederhana dapat, katakanlah, mengharuskan Anda secara khusus menyatakan niat Anda untuk membuat berlian sebelum benar-benar mengkompilasi untuk Anda. Dengan fitur seperti itu, setiap ciptaan berlian adalah disengaja, sembrono, atau karena orang tidak menyadari jebakan ini.

Jadi, semua itu dikatakan ... Apakah ada alasan nyata mengapa kebanyakan orang membenci banyak warisan, atau apakah itu semua hanya histeria yang menyebabkan lebih banyak kerugian daripada kebaikan? Adakah sesuatu yang tidak saya lihat di sini? Terima kasih.

Contoh

Mobil meluas WheeledVehicle, KIASpectra memperluas Mobil dan Elektronik, KIASpectra berisi Radio. Mengapa KIASpectra tidak mengandung Elektronik?

  1. Karena ini elektronik. Pewarisan vs komposisi harus selalu berupa hubungan is-a vs. has-a.

  2. Karena ini elektronik. Ada kabel, papan sirkuit, sakelar, dll semuanya naik turun.

  3. Karena ini elektronik. Jika baterai Anda mati di musim dingin, Anda berada dalam banyak masalah seolah-olah semua roda Anda tiba-tiba hilang.

Mengapa tidak menggunakan antarmuka? Ambil nomor 3, misalnya. Saya tidak ingin menulis ini berulang-ulang, dan saya benar - benar tidak ingin membuat kelas pembantu proxy yang aneh untuk melakukan ini juga:

private void runOrDont()
{
    if (this.battery)
    {
        if (this.battery.working && this.switchedOn)
        {
            this.run();
            return;
        }
    }
    this.dontRun();
}

(Kami tidak membahas apakah implementasi itu baik atau buruk.) Anda dapat membayangkan bagaimana mungkin ada beberapa fungsi yang terkait dengan Elektronik yang tidak terkait dengan apa pun di WheeledVehicle, dan sebaliknya.

Saya tidak yakin apakah akan menerima contoh itu atau tidak, karena ada ruang untuk interpretasi di sana. Anda juga bisa berpikir dalam hal Pesawat memperluas Kendaraan dan FlyingObject dan Burung memperluas Hewan dan FlyingObject, atau dalam hal contoh yang jauh lebih murni.

Panzercrisis
sumber
24
itu juga mendorong pewarisan atas komposisi ... (padahal seharusnya sebaliknya)
ratchet freak
34
"Anda dapat mengacaukannya" adalah alasan yang sah untuk menghapus fitur. Desain bahasa modern agak terpecah pada titik itu, tetapi Anda umumnya dapat mengklasifikasikan bahasa menjadi "Daya dari pembatasan" dan "Daya dari fleksibilitas". Tak satu pun dari yang "benar", saya tidak berpikir, mereka berdua memiliki poin kuat untuk dibuat. MI adalah salah satu dari hal-hal yang digunakan untuk Kejahatan lebih sering daripada tidak, dan dengan demikian bahasa yang membatasi menghapusnya. Yang fleksibel tidak, karena "lebih sering daripada tidak" tidak "secara harfiah selalu". Yang mengatakan, mixin / traits adalah solusi yang lebih baik untuk kasus umum, saya pikir.
Phoshi
8
Ada alternatif yang lebih aman untuk pewarisan berganda dalam beberapa bahasa, yang melampaui antarmuka. Lihat Scala's Traits- mereka bertindak seperti antarmuka dengan implementasi opsional, tetapi memiliki beberapa batasan yang membantu mencegah masalah seperti masalah berlian muncul.
KChaloux
5
Lihat juga pertanyaan yang sebelumnya tertutup ini: programmers.stackexchange.com/questions/122480/...
Doc Brown
19
KiaSpectrabukan sebuah Electronic ; ia memiliki Elektronika, dan mungkin merupakan ElectronicCar(yang akan memperpanjang Car...)
Brian S

Jawaban:

68

Dalam banyak kasus, orang menggunakan warisan untuk memberikan sifat ke suatu kelas. Misalnya pikirkan tentang Pegasus. Dengan pewarisan berganda, Anda mungkin tergoda untuk mengatakan Pegasus memperluas Kuda dan Burung karena Anda telah mengklasifikasikan Burung tersebut sebagai binatang dengan sayap.

Namun, Burung memiliki sifat lain yang tidak dimiliki Pegasi. Misalnya, burung bertelur, Pegasi sudah lahir hidup. Jika pewarisan adalah satu-satunya cara Anda melewati sifat berbagi maka tidak ada cara untuk mengecualikan sifat bertelur dari Pegasus.

Beberapa bahasa telah memilih untuk membuat ciri-ciri konstruk eksplisit dalam bahasa. Lainnya dengan lembut membimbing Anda ke arah itu dengan menghapus MI dari bahasa. Either way, saya tidak bisa memikirkan satu kasus di mana saya pikir "Man saya benar-benar membutuhkan MI untuk melakukan ini dengan benar".

Juga mari kita bahas apa warisan BENAR-BENAR. Ketika Anda mewarisi dari kelas, Anda mengambil ketergantungan pada kelas itu, tetapi juga Anda harus mendukung kontrak yang didukung kelas, baik implisit maupun eksplisit.

Ambil contoh klasik persegi yang diwarisi dari persegi panjang. Persegi panjang memperlihatkan properti panjang dan lebar dan juga metode getPerimeter dan getArea. Kuadrat akan mengesampingkan panjang dan lebar sehingga ketika satu diatur yang lain diatur untuk mencocokkan getPerimeter dan getArea akan bekerja sama (2 * panjang + 2 * lebar untuk perimeter dan panjang * lebar untuk area).

Ada satu test case yang rusak jika Anda mengganti implementasi persegi ini untuk persegi panjang.

var rectangle = new Square();
rectangle.length= 5;
rectangle.width= 6;
Assert.AreEqual(30, rectangle.GetArea()); 
//Square returns 36 because setting the width clobbers the length

Cukup sulit untuk memperbaiki keadaan dengan rantai pewarisan tunggal. Semakin buruk ketika Anda menambahkan yang lain ke dalam campuran.


Jebakan yang saya sebutkan dengan Pegasus di MI dan hubungan Rectangle / Square adalah hasil dari desain yang tidak berpengalaman untuk kelas. Pada dasarnya menghindari banyak warisan adalah cara untuk membantu para pengembang pemula menghindari menembak diri mereka sendiri. Seperti semua prinsip desain lainnya, memiliki disiplin dan pelatihan yang didasarkan pada prinsip-prinsip tersebut memungkinkan Anda untuk segera mengetahui kapan saatnya untuk melepaskan diri darinya. Lihat Dreyfus Model of Skill Acquisition , di tingkat Expert, pengetahuan intrinsik Anda melampaui ketergantungan pada prinsip / prinsip. Anda bisa "merasakan" ketika aturan tidak berlaku.

Dan saya setuju bahwa saya agak tertipu dengan contoh "dunia nyata" tentang mengapa MI disukai.

Mari kita lihat kerangka UI. Secara khusus mari kita lihat beberapa widget yang mungkin pada awalnya terlihat seperti hanya kombinasi dari dua lainnya. Seperti ComboBox. ComboBox adalah TextBox yang memiliki DropDownList yang mendukung. Yaitu saya bisa mengetikkan nilai, atau saya bisa memilih dari daftar nilai yang telah ditentukan. Pendekatan naif adalah mewarisi ComboBox dari TextBox dan DropDownList.

Tapi Textbox Anda mendapatkan nilainya dari apa yang diketik pengguna. Sementara DDL mendapatkan nilainya dari apa yang dipilih pengguna. Siapa yang mengambil preseden? DDL mungkin telah dirancang untuk memverifikasi dan menolak input apa pun yang tidak ada dalam daftar nilai aslinya. Apakah kita mengabaikan logika itu? Itu berarti kita harus mengekspos logika internal untuk ditimpa oleh pewaris. Atau lebih buruk, tambahkan logika ke kelas dasar yang hanya ada untuk mendukung subclass (melanggar Prinsip Ketergantungan Inversi ).

Menghindari MI membantu Anda menghindari perangkap ini sama sekali. Dan mungkin menyebabkan Anda mengekstraksi ciri-ciri umum dari widget UI Anda sehingga dapat diterapkan sesuai kebutuhan. Contoh yang sangat baik dari ini adalah Properti Terlampir WPF yang memungkinkan elemen kerangka kerja di WPF untuk menyediakan properti yang elemen kerangka kerja lain dapat digunakan tanpa mewarisi dari elemen kerangka induk.

Misalnya Grid adalah panel tata letak di WPF dan memiliki properti Kolom dan Baris yang menentukan di mana elemen anak harus ditempatkan dalam pengaturan grid. Tanpa properti terlampir, jika saya ingin mengatur Tombol di dalam Kotak, Tombol harus berasal dari Kotak sehingga bisa memiliki akses ke properti Kolom dan Baris.

Pengembang mengambil konsep ini lebih jauh dan menggunakan properti yang dilampirkan sebagai cara perilaku komponen (misalnya di sini adalah posting saya tentang membuat GridView yang dapat disortir menggunakan properti terlampir yang ditulis sebelum WPF menyertakan DataGrid). Pendekatan ini telah diakui sebagai Pola Desain XAML yang disebut Perilaku Terlampir .

Semoga ini memberikan sedikit lebih banyak wawasan tentang mengapa Multiple Inheritance biasanya disukai.

Michael Brown
sumber
14
"Man, aku benar-benar membutuhkan MI untuk melakukan ini dengan benar" - lihat jawaban saya untuk exmaple. Singkatnya, konsep ortogonal yang memenuhi hubungan is-a masuk akal untuk MI. Mereka sangat jarang, tetapi mereka ada.
MrFox
13
Aku benci contoh persegi panjang itu. Ada banyak contoh yang lebih mencerahkan yang tidak membutuhkan mutabilitas.
penanggung jawab
9
Saya suka bahwa jawaban untuk "memberikan contoh nyata" adalah untuk membahas makhluk fiksi. Ironi luar biasa +1
jjathman
3
Jadi Anda mengatakan bahwa banyak pewarisan adalah buruk, karena warisan buruk (Anda semua contoh tentang pewarisan antarmuka tunggal). Oleh karena itu, berdasarkan jawaban ini saja, suatu bahasa harus dihilangkan dari warisan dan antarmuka, atau memiliki banyak warisan.
ctrl-alt-delor
12
Saya tidak berpikir contoh ini benar-benar bekerja ... tidak ada yang mengatakan pegasus adalah burung dan kuda ... Anda mengatakan itu adalah WingedAnimal dan HoofedAnimal. Contoh persegi dan persegi panjang sedikit lebih masuk akal karena Anda menemukan diri Anda dengan objek yang berperilaku berbeda dari yang Anda pikirkan berdasarkan definisi yang ditetapkan. Namun, saya pikir situasi ini akan menjadi kesalahan programmer karena tidak menangkap ini (jika memang ini terbukti menjadi masalah).
richard
60

Adakah sesuatu yang tidak saya lihat di sini?

Mengizinkan banyak pewarisan membuat aturan tentang kelebihan fungsi dan pengiriman virtual jelas lebih rumit, serta implementasi bahasa di sekitar tata letak objek. Desainer / pelaksana bahasa berdampak ini sedikit, dan meningkatkan standar yang sudah tinggi untuk menyelesaikan bahasa, stabil dan diadopsi.

Argumen umum lainnya yang pernah saya lihat (dan buat beberapa kali) adalah bahwa dengan memiliki dua kelas dasar +, objek Anda hampir selalu melanggar Prinsip Tanggung Jawab Tunggal. Baik dua kelas dasar + adalah kelas mandiri yang bagus dengan tanggung jawab mereka sendiri (menyebabkan pelanggaran) atau mereka adalah tipe parsial / abstrak yang saling bekerja sama untuk membuat satu tanggung jawab kohesif tunggal.

Dalam kasus lain ini, Anda memiliki 3 skenario:

  1. A tidak tahu apa-apa tentang B - Hebat, Anda bisa menggabungkan kelas karena Anda beruntung.
  2. A tahu tentang B - Mengapa A tidak hanya mewarisi dari B?
  3. A dan B tahu tentang satu sama lain - Mengapa Anda tidak membuat satu kelas saja? Apa manfaat dari membuat hal-hal ini begitu digabungkan tetapi dapat diganti sebagian?

Secara pribadi, saya pikir multiple inheritance memiliki rap yang buruk, dan bahwa sistem komposisi gaya sifat yang dilakukan dengan baik akan sangat kuat / berguna ... tetapi ada banyak cara yang dapat diterapkan dengan buruk, dan banyak alasannya bukan ide yang baik dalam bahasa seperti C ++.

Mengenai contoh Anda, itu tidak masuk akal. A Kia memiliki elektronik. Ia memiliki mesin. Demikian juga, elektronik itu memiliki sumber daya, yang kebetulan merupakan aki mobil. Warisan, apalagi pewarisan berganda tidak memiliki tempat di sana.

Telastyn
sumber
8
Pertanyaan menarik lainnya adalah bagaimana kelas dasar + kelas dasar mereka diinisialisasi. Enkapsulasi> Warisan.
jozefg
"sistem dilakukan dengan baik komposisi gaya sifat akan benar-benar kuat / berguna ..." - Ini adalah dikenal sebagai mixin , dan mereka yang kuat / berguna. Mixin dapat diimplementasikan menggunakan MI, tetapi Multiple Inheritance tidak diperlukan untuk mixin. Beberapa bahasa mendukung mixin secara inheren, tanpa MI.
BlueRaja - Danny Pflughoeft
Khusus untuk Java, kami sudah memiliki banyak antarmuka dan INVOKEINTERFACE, jadi MI tidak akan memiliki implikasi kinerja yang jelas.
Daniel Lubarov
Tetastyn, saya setuju bahwa contohnya buruk dan tidak melayani pertanyaannya. Warisan bukan untuk kata sifat (elektronik), itu untuk kata benda (kendaraan).
richard
29

Satu-satunya alasan mengapa itu dilarang adalah karena itu memudahkan orang untuk menembak diri mereka sendiri di kaki.

Apa yang biasanya terjadi dalam diskusi semacam ini adalah argumen tentang apakah fleksibilitas memiliki alat lebih penting daripada keamanan tidak menembak kaki Anda. Tidak ada jawaban yang jelas benar untuk argumen itu, karena seperti kebanyakan hal lain dalam pemrograman, jawabannya tergantung pada konteks.

Jika pengembang Anda nyaman dengan MI, dan MI masuk akal dalam konteks apa yang Anda lakukan, maka Anda akan sangat merindukannya dalam bahasa yang tidak mendukungnya. Pada saat yang sama jika tim tidak nyaman dengan itu, atau tidak ada kebutuhan nyata untuk itu dan orang menggunakannya 'hanya karena mereka bisa', maka itu kontraproduktif.

Tapi tidak, tidak ada argumen yang benar-benar meyakinkan yang membuktikan bahwa pewarisan berganda adalah ide yang buruk.

SUNTING

Jawaban atas pertanyaan ini tampaknya bulat. Demi menjadi pendukung iblis, saya akan memberikan contoh bagus tentang pewarisan berganda, di mana tidak melakukannya akan menyebabkan peretasan.

Misalkan Anda sedang merancang aplikasi pasar modal. Anda memerlukan model data untuk sekuritas. Beberapa sekuritas adalah produk ekuitas (saham, trust investasi real estat, dll) yang lain adalah utang (obligasi, obligasi korporasi), yang lain adalah derivatif (opsi, berjangka). Jadi jika Anda menghindari MI, Anda akan membuat pohon warisan yang sangat jelas dan sederhana. Saham akan mewarisi Ekuitas, Obligasi akan mewarisi Hutang. Sejauh ini bagus, tapi bagaimana dengan turunannya? Mereka dapat didasarkan dari produk seperti Ekuitas atau produk seperti Debit? Ok, saya kira kita akan membuat cabang pohon warisan kita lebih banyak. Perlu diingat, beberapa derivatif didasarkan pada produk ekuitas, produk utang, atau tidak keduanya. Jadi pohon warisan kita semakin rumit. Kemudian datanglah analis bisnis dan memberi tahu Anda bahwa sekarang kami mendukung sekuritas yang diindeks (opsi indeks, opsi indeks masa depan). Dan hal-hal ini dapat didasarkan pada Ekuitas, Hutang, atau Derivatif. Ini semakin berantakan! Apakah opsi indeks masa depan saya menghasilkan Ekuitas-> Saham-> Opsi-> Indeks? Mengapa tidak Equity-> Stock-> Index-> ​​Option? Bagaimana jika suatu hari saya menemukan keduanya dalam kode saya (Ini terjadi; kisah nyata)?

Masalahnya di sini adalah bahwa tipe-tipe fundamental ini dapat dicampur dalam permutasi yang tidak secara alami mengambil satu dari yang lain. Benda-benda didefinisikan oleh adalah hubungan, sehingga komposisi sangat tidak masuk akal. Warisan berganda (atau konsep mixin yang serupa) adalah satu-satunya representasi logis di sini.

Solusi nyata untuk masalah ini adalah memiliki jenis Indeks, Hutang, Derivatif, Indeks yang ditentukan dan dicampur menggunakan beberapa pewarisan untuk membuat model data Anda. Ini akan membuat objek yang keduanya, masuk akal, dan mudah dipinjamkan untuk digunakan kembali.

MrFox
sumber
27
Saya sudah bekerja di bidang keuangan. Ada alasan mengapa pemrograman fungsional lebih disukai daripada OO dalam perangkat lunak keuangan. Dan Anda sudah menunjukkannya. MI tidak memperbaiki masalah ini tetapi memperburuknya. Percayalah, saya tidak bisa memberi tahu Anda berapa kali saya mendengar "Hanya seperti ini ... kecuali" ketika berurusan dengan klien keuangan Itu kecuali menjadi kutukan keberadaan saya.
Michael Brown
8
Ini tampaknya sangat bisa dipecahkan dengan antarmuka untuk saya ... pada kenyataannya, tampaknya lebih cocok untuk antarmuka daripada yang lainnya. Equitydan Debtkeduanya mengimplementasikan ISecurity. Derivativememiliki ISecurityproperti. Mungkin itu sendiri ISecurityjika pantas (saya tidak tahu keuangan). IndexedSecuritiessekali lagi berisi properti antarmuka yang diterapkan pada jenis yang diizinkan didasarkan. Jika semuanya ISecurity, maka mereka semua memiliki ISecurityproperti dan dapat disarangkan secara sewenang-wenang ...
Bobson
11
@ Bobson masalahnya adalah Anda harus mengimplementasikan kembali antarmuka untuk masing-masing implementor, dan dalam banyak kasus itu sama saja. Anda dapat menggunakan delegasi / komposisi tetapi kemudian Anda kehilangan akses ke anggota pribadi. Dan karena Jawa tidak memiliki delegasi / lambda ... secara harfiah tidak ada cara yang baik untuk melakukannya. Kecuali mungkin dengan alat Aspect seperti Aspect4J
Michael Brown
10
@ Bobson persis apa yang dikatakan Mike Brown. Ya, Anda dapat merancang solusi dengan antarmuka, tetapi itu akan kikuk. Intuisi Anda untuk menggunakan antarmuka sangat benar, itu adalah keinginan tersembunyi untuk mixin / multiple inheritance :).
MrFox
11
Ini menyentuh keanehan di mana programmer OO lebih suka untuk berpikir dalam hal warisan dan sering gagal untuk menerima pendelegasian sebagai pendekatan OO yang valid, yang biasanya menghasilkan solusi yang lebih ramping, lebih dapat dipertahankan dan dapat dikembangkan. Setelah bekerja di bidang keuangan dan perawatan kesehatan, saya telah melihat kekacauan yang tidak dapat dikelola yang dapat dibuat MI, terutama karena undang-undang pajak dan kesehatan berubah dari tahun ke tahun dan definisi objek tahun LAST tidak berlaku untuk tahun INI (namun masih harus menjalankan fungsi dari tahun tersebut) , dan harus dipertukarkan). Komposisi menghasilkan kode yang lebih ramping yang lebih mudah untuk diuji, dan biayanya lebih murah dari waktu ke waktu.
Shaun Wilson
11

Jawaban lain di sini tampaknya sebagian besar masuk ke teori. Jadi, inilah contoh Python konkret, disederhanakan ke bawah, yang telah saya hancurkan dengan cepat, yang membutuhkan cukup banyak refactoring:

class Foo(object):
   def zeta(self):
      print "foozeta"

class Bar(object):
   def zeta(self):
      print "barzeta"

   def barstuff(self):
      print "barstuff"
      self.zeta()

class Bang(Foo, Bar):
   def stuff(self):
      self.zeta()
      print "---"
      self.barstuff()

z = Bang()
z.stuff()

Barditulis dengan asumsi itu memiliki implementasi sendiri zeta(), yang umumnya merupakan asumsi yang cukup bagus. Subclass harus menimpanya dengan tepat sehingga melakukan hal yang benar. Sayangnya, nama-nama itu hanya kebetulan sama - mereka melakukan hal-hal yang agak berbeda, tetapi Barsekarang memanggil Fooimplementasi:

foozeta
---
barstuff
foozeta

Agak membuat frustasi ketika tidak ada kesalahan yang dilemparkan, aplikasi mulai bertindak sedikit salah, dan perubahan kode yang menyebabkannya (membuat Bar.zeta) sepertinya tidak menjadi masalah.

Izkata
sumber
Bagaimana masalah Diamond dihindari melalui panggilan ke super()?
Panzercrisis
@ Panzercrisis Maaf, jangan pikirkan bagian itu. Saya salah mengartikan masalah yang mana "masalah berlian" biasanya merujuk - Python memiliki masalah terpisah yang disebabkan oleh warisan berbentuk berlian yang super()beredar
Izkata
5
Saya senang C ++ MI dirancang jauh lebih baik. Bug di atas sama sekali tidak mungkin. Anda harus secara manual membuat ambiguasinya sebelum kode tersebut dikompilasi.
Thomas Eding
2
Mengubah urutan warisan Bangke Bar, Foojuga akan memperbaiki masalah - tetapi poin Anda bagus, +1.
Sean Vieira
1
@ThomasEding Anda dapat secara manual disambiguate masih: Bar.zeta(self).
Tyler Crompton
9

Saya berpendapat bahwa tidak ada masalah nyata dengan MI dalam bahasa yang benar . Kuncinya adalah untuk memungkinkan struktur berlian, tetapi mengharuskan subtipe menyediakan override sendiri, alih-alih kompiler memilih salah satu implementasi berdasarkan beberapa aturan.

Saya melakukan ini di Jambu , bahasa yang sedang saya kerjakan. Salah satu fitur dari Guava adalah bahwa kita dapat memanggil implementasi metode supertype spesifik. Jadi mudah untuk menunjukkan implementasi supertipe mana yang harus "diwariskan", tanpa sintaks khusus:

type Sequence[+A] {
  String toString() {
    return "[" + ... + "]";
  }
}

type Set[+A] {
  String toString() {
    return "{" + ... + "}";
  }
}

type OrderedSet[+A] extends Sequence[A], Set[A] {
  String toString() {
    // This is Guava's syntax for statically invoking instance methods
    return Set.toString(this);
  }
}

Jika kami tidak memberikannya OrderedSetsendiri toString, kami akan mendapatkan kesalahan kompilasi. Tidak ada kejutan.

Saya menemukan MI sangat berguna dengan koleksi. Sebagai contoh, saya suka menggunakan RandomlyEnumerableSequencetipe untuk menghindari menyatakan getEnumeratoruntuk array, deques, dan sebagainya:

type Enumerable[+A] {
  Source[A] getEnumerator();
}

type Sequence[+A] extends Enumerable[A] {
  A get(Int index);
}

type RandomlyEnumerableSequence[+A] extends Sequence[A] {
  Source[A] getEnumerator() {
    ...
  }
}

type DynamicArray[A] extends MutableStack[A],
                             RandomlyEnumerableSequence[A] {
  // No need to define getEnumerator.
}

Jika kita tidak memiliki MI, kita dapat menulis RandomAccessEnumeratoruntuk beberapa koleksi untuk digunakan, tetapi harus menulis getEnumeratormetode singkat masih menambahkan boilerplate.

Demikian pula, MI berguna untuk mewarisi implementasi standar equals, hashCodedan toStringuntuk koleksi.

Daniel Lubarov
sumber
1
@AndyzSmith, saya mengerti maksud Anda, tetapi tidak semua konflik warisan adalah masalah semantik yang sebenarnya. Pertimbangkan contoh saya - tidak ada tipe yang membuat janji tentang toStringperilaku, jadi mengesampingkan perilakunya tidak melanggar prinsip substitusi. Anda juga terkadang mendapatkan "konflik" antara metode yang memiliki perilaku yang sama tetapi algoritma yang berbeda, terutama dengan koleksi.
Daniel Lubarov
1
@ Asmageddon setuju, contoh saya hanya melibatkan fungsionalitas. Apa yang Anda lihat sebagai keunggulan mixin? Paling tidak di Scala, sifat-sifat pada dasarnya hanyalah kelas abstrak yang mengizinkan MI - mereka masih dapat digunakan untuk identitas dan masih membawa masalah berlian. Apakah ada bahasa lain di mana mixin memiliki perbedaan yang lebih menarik?
Daniel Lubarov
1
Secara teknis, kelas abstrak dapat digunakan sebagai antarmuka, bagi saya, manfaatnya hanyalah fakta konstruk itu sendiri sebagai "tip penggunaan", yang berarti saya tahu apa yang diharapkan ketika saya melihat kode. Yang mengatakan, saya saat ini sedang coding di Lua, yang tidak memiliki model OOP yang melekat - sistem kelas yang ada umumnya memungkinkan termasuk tabel sebagai mixin, dan yang saya coding menggunakan kelas sebagai mixin. Satu-satunya perbedaan adalah bahwa Anda hanya dapat menggunakan (tunggal) warisan untuk identitas, mixin untuk fungsionalitas (tanpa info identitas), dan antarmuka untuk pemeriksaan lebih lanjut. Mungkin itu entah bagaimana jauh lebih baik, tetapi masuk akal bagi saya.
Llamageddon
2
Mengapa Anda memanggil Ordered extends Set and Sequencea Diamond? Itu hanya sebuah Gabung. Tidak memiliki hattitik. Mengapa Anda menyebutnya berlian? Saya bertanya di sini tetapi sepertinya pertanyaan tabu. Bagaimana Anda tahu bahwa Anda perlu menyebut struktur triangualar ini sebagai Berlian daripada Gabung?
Val
1
@RecognizeEvilasWaste bahwa bahasa memiliki Toptipe implisit yang menyatakan toString, jadi sebenarnya ada struktur berlian. Tapi saya pikir Anda ada benarnya - "bergabung" tanpa struktur berlian menciptakan masalah yang sama, dan sebagian besar bahasa menangani kedua kasus dengan cara yang sama.
Daniel Lubarov
7

Warisan, banyak atau tidak, tidak begitu penting. Jika dua objek dari jenis yang berbeda dapat diganti, itulah yang penting, bahkan jika mereka tidak dihubungkan oleh warisan.

Daftar yang ditautkan dan string karakter memiliki sedikit kesamaan, dan tidak perlu ditautkan oleh pewarisan, tetapi ini berguna jika saya dapat menggunakan lengthfungsi untuk mendapatkan jumlah elemen dalam salah satu dari keduanya.

Warisan adalah trik untuk menghindari implementasi berulang kode. Jika pewarisan menghemat pekerjaan Anda, dan banyak pewarisan menghemat lebih banyak pekerjaan dibandingkan dengan pewarisan tunggal, maka itu semua pembenaran yang diperlukan.

Saya menduga bahwa beberapa bahasa tidak menerapkan banyak pewarisan dengan sangat baik, dan bagi para praktisi dari bahasa-bahasa tersebut, itulah yang dimaksud dengan pewarisan berganda. Sebutkan multiple inheritance ke seorang programmer C ++, dan apa yang terlintas dalam pikiran adalah sesuatu tentang masalah ketika sebuah kelas berakhir dengan dua salinan basis melalui dua jalur pewarisan yang berbeda, dan apakah akan digunakan virtualpada kelas dasar, dan kebingungan tentang bagaimana destructor disebut , dan seterusnya.

Dalam banyak bahasa, pewarisan kelas digabung dengan pewarisan simbol. Ketika Anda menurunkan kelas D dari kelas B, Anda tidak hanya membuat hubungan tipe, tetapi karena kelas-kelas ini juga berfungsi sebagai ruang nama leksikal, Anda berhadapan dengan impor simbol dari ruang nama B ke ruang nama D, di samping semantik dari apa yang terjadi dengan tipe B dan D sendiri. Karenanya, pewarisan berganda membawa masalah bentrok simbol. Jika kita mewarisi dari card_deckdan graphic, keduanya "memiliki" drawmetode, apa artinya dengan drawobjek yang dihasilkan? Sistem objek yang tidak memiliki masalah ini adalah yang ada di Common Lisp. Mungkin tidak secara kebetulan, multiple inheritance digunakan dalam program Lisp.

Implementasi yang buruk, segala sesuatu yang tidak nyaman (seperti multiple inheritance) harus dibenci.

Kaz
sumber
2
Contoh Anda dengan panjang () metode daftar dan wadah string buruk, karena keduanya melakukan hal yang sama sekali berbeda. Tujuan pewarisan bukan untuk mengurangi pengulangan kode. Jika Anda ingin mengurangi pengulangan kode, Anda refactor bagian umum ke kelas baru.
BЈовић
1
@ BЈовић Keduanya tidak melakukan hal yang sama sekali berbeda.
Kaz
1
Menyusun string dan daftar , orang dapat melihat banyak pengulangan. Menggunakan warisan untuk mengimplementasikan metode-metode umum ini tentu saja salah.
BЈовић
1
@ BЈовић Tidak disebutkan dalam jawaban bahwa string dan daftar harus dihubungkan oleh warisan; justru sebaliknya. Perilaku mampu mengganti daftar atau string dalam lengthoperasi berguna secara independen dari konsep pewarisan (yang dalam hal ini tidak membantu: kita tidak mungkin dapat mencapai apa pun dengan mencoba berbagi implementasi antara dua metode lengthfungsi). Namun demikian mungkin ada beberapa pewarisan abstrak: misalnya daftar dan string berurutan jenis (tetapi yang tidak menyediakan implementasi apa pun).
Kaz
2
Juga, pewarisan adalah cara refactoring bagian ke kelas umum: yaitu kelas dasar.
Kaz
1

Sejauh yang saya tahu, bagian dari masalah (selain membuat desain Anda sedikit lebih sulit untuk dipahami (namun lebih mudah untuk kode)) adalah bahwa kompiler akan menghemat ruang yang cukup untuk data kelas Anda, memungkinkan ini sejumlah besar pemborosan memori dalam kasus berikut:

(Contoh saya mungkin bukan yang terbaik, tetapi cobalah untuk mendapatkan inti tentang beberapa ruang memori untuk tujuan yang sama, itu adalah hal pertama yang terlintas di pikiran saya: P)

Mempertimbangkan DDD di mana anjing kelas meluas dari caninus dan pet, caninus memiliki variabel yang menunjukkan jumlah makanan yang harus dimakan (bilangan bulat) dengan nama dietKg, tetapi hewan peliharaan juga memiliki variabel lain untuk tujuan itu biasanya di bawah yang sama nama (kecuali jika Anda menetapkan nama variabel lain, maka Anda akan memiliki kode tambahan kode, yang merupakan masalah awal yang ingin Anda hindari, untuk menangani dan menjaga integritas variabel bouth), maka Anda akan memiliki dua ruang memori untuk tepat tujuan yang sama, untuk menghindari ini, Anda harus memodifikasi kompiler Anda untuk mengenali nama itu di bawah namespace yang sama dan hanya menetapkan ruang memori tunggal untuk data itu, yang sayangnya tidak dapat ditentukan dalam waktu kompilasi.

Anda bisa, tentu saja, merancang bahasa untuk menentukan bahwa variabel tersebut mungkin sudah memiliki ruang yang ditentukan di tempat lain, tetapi pada akhirnya programmer harus menentukan di mana ruang memori yang variabel ini rujuk (dan lagi kode tambahan).

Percayalah pada saya, orang-orang yang menerapkan pemikiran ini sangat keras tentang semua ini, tetapi saya senang Anda bertanya, prespektif Anda yang baik adalah yang mengubah paradigma;), dan pertimbangkan ini, saya tidak mengatakan itu tidak mungkin (tetapi banyak asumsi dan kompiler multi-fase harus diimplementasikan, dan yang benar-benar kompleks), saya hanya mengatakan itu belum ada, jika Anda memulai proyek untuk kompiler Anda sendiri yang mampu melakukan "ini" (multiple inheritance) tolong beri tahu saya, saya Dengan senang hati akan bergabung dengan tim Anda.

Ordiel
sumber
1
Pada titik apa seorang kompiler dapat berasumsi bahwa variabel dengan nama yang sama dari kelas induk yang berbeda aman untuk digabungkan? Apa jaminan yang Anda miliki bahwa caninus.diet dan pet.diet benar-benar melayani tujuan yang sama? Apa yang akan Anda lakukan dengan fungsi / metode dengan nama yang sama?
Mr.Mindor
hahaha Saya sangat setuju dengan Anda @ Mr.Mindor, itulah yang saya katakan dalam jawaban saya, satu-satunya cara yang muncul di pikiran saya untuk mendekati pendekatan itu adalah pemrograman berorientasi prototipe, tapi saya tidak mengatakan itu tidak mungkin. , dan itulah alasan mengapa saya mengatakan bahwa banyak asumsi harus dibuat selama waktu kompilasi, dan beberapa "data" / spesifikasi tambahan harus ditulis oleh pemrogram (namun itu lebih banyak pengkodean, yang merupakan masalah asli)
Ordiel
-1

Untuk beberapa saat sekarang tidak pernah benar-benar terjadi pada saya betapa sangat berbeda beberapa tugas pemrograman dari yang lain - dan seberapa besar membantu jika bahasa dan pola yang digunakan disesuaikan dengan ruang masalah.

Ketika Anda bekerja sendirian atau sebagian besar terisolasi pada kode yang Anda tulis itu adalah ruang masalah yang sama sekali berbeda dengan mewarisi basis kode dari 40 orang di India yang mengerjakannya selama setahun sebelum menyerahkannya kepada Anda tanpa bantuan transisi.

Bayangkan Anda baru saja direkrut oleh perusahaan impian Anda dan kemudian mewarisi basis kode seperti itu. Lebih jauh bayangkan bahwa konsultan telah belajar tentang (dan karena itu tergila-gila dengan) warisan dan multiple-inheritance ... Bisakah Anda menggambarkan apa yang sedang Anda kerjakan.

Ketika Anda mewarisi kode, fitur yang paling penting adalah bahwa kode itu dapat dimengerti dan bagian-bagiannya terisolasi sehingga dapat dikerjakan secara mandiri. Tentu ketika Anda pertama kali menulis struktur kode seperti multiple inheritance dapat menyelamatkan Anda dari duplikasi kecil dan tampaknya sesuai dengan suasana hati logis Anda pada saat itu, tetapi orang berikutnya hanya memiliki lebih banyak hal untuk diurai.

Setiap interkoneksi dalam kode Anda juga membuatnya lebih sulit untuk memahami dan memodifikasi karya secara independen, dua kali lipat dengan pewarisan berganda.

Ketika Anda bekerja sebagai bagian dari tim Anda ingin menargetkan kode yang paling sederhana yang memberi Anda sama sekali tidak ada logika yang berlebihan (Itulah arti sebenarnya KERING, bukan berarti Anda tidak perlu mengetik banyak hanya karena Anda tidak perlu mengubah kode dalam 2 tempat untuk memecahkan masalah!)

Ada cara yang lebih sederhana untuk mencapai kode KERING daripada Multiple Inheritance, jadi memasukkannya dalam bahasa hanya dapat membuka Anda terhadap masalah yang dimasukkan oleh orang lain yang mungkin tidak memiliki tingkat pemahaman yang sama seperti Anda. Ini bahkan menggoda jika bahasa Anda tidak mampu menawarkan Anda cara yang sederhana / tidak rumit untuk menjaga kode Anda KERING.

Bill K
sumber
Lebih jauh bayangkan bahwa konsultan telah belajar tentang - Saya telah melihat begitu banyak bahasa non-MI (misalnya .net) di mana para konsultan menjadi gila dengan DI dan IoC berbasis antarmuka untuk membuat semuanya menjadi berantakan berbelit-belit tak tertembus. MI tidak memperburuk ini, mungkin membuatnya lebih baik.
gbjbaanb
@ gbjbaanb Anda benar, mudah bagi seseorang yang belum pernah mengacaukan fitur apa pun - pada kenyataannya itu hampir merupakan persyaratan untuk mempelajari fitur yang Anda mengacaukannya untuk melihat bagaimana cara terbaik menggunakannya. Saya agak penasaran karena Anda tampaknya mengatakan bahwa tidak memiliki pasukan MI lebih banyak DI / IoC yang belum saya lihat - saya tidak akan berpikir bahwa kode konsultan yang Anda dapatkan dengan semua antarmuka jelek / DI akan menjadi lebih baik juga memiliki pohon warisan banyak-ke-banyak gila, tapi itu bisa menjadi pengalaman saya dengan MI. Apakah Anda memiliki halaman yang bisa saya baca tentang mengganti DI / IoC dengan MI?
Bill K
-3

Argumen terbesar terhadap pewarisan berganda adalah bahwa beberapa kemampuan yang berguna dapat disediakan, dan beberapa aksioma bermanfaat dapat bertahan, dalam kerangka kerja yang sangat membatasi itu (*), tetapi tidak dapat tidak dapat disediakan dan / atau ditahan tanpa batasan seperti itu. Diantara mereka:

  • Kemampuan untuk memiliki beberapa modul yang dikompilasi secara terpisah termasuk kelas-kelas yang mewarisi dari kelas-kelas modul lain, dan mengkompilasi ulang modul yang berisi kelas dasar tanpa harus mengkompilasi ulang setiap modul yang mewarisi dari kelas dasar itu.

  • Kemampuan untuk tipe mewarisi implementasi anggota dari kelas induk tanpa tipe turunan harus mengimplementasikannya kembali

  • Aksioma bahwa setiap instance objek dapat di-upcast atau downcast langsung ke dirinya sendiri atau salah satu dari jenis dasarnya, dan upcast dan downcast tersebut, dalam kombinasi atau urutan, selalu mempertahankan identitas

  • Aksioma bahwa jika kelas turunan menimpa dan rantai ke anggota kelas dasar, anggota basis akan dipanggil langsung oleh kode rantai, dan tidak akan dipanggil di tempat lain.

(*) Biasanya dengan mengharuskan jenis yang mendukung multiple inheritance dideklarasikan sebagai "antarmuka" daripada kelas, dan tidak memungkinkan antarmuka untuk melakukan semua yang dapat dilakukan oleh kelas normal.

Jika seseorang ingin mengizinkan pewarisan berganda yang digeneralisasi, sesuatu yang lain harus memberi jalan. Jika X dan Y keduanya mewarisi dari B, mereka berdua menimpa anggota M yang sama dan rantai ke implementasi basis, dan jika D mewarisi dari X dan Y tetapi tidak menimpa M, maka diberi contoh q dari tipe D, apa yang harus (( B) q) .M () lakukan? Menolak gips semacam itu akan melanggar aksioma yang mengatakan objek apa pun dapat di-upcast ke tipe pangkalan apa pun, tetapi perilaku yang mungkin dilakukan para pemain dan pemanggilan anggota akan melanggar aksioma mengenai metode chaining. Orang bisa meminta bahwa kelas hanya dimuat dalam kombinasi dengan versi tertentu dari kelas dasar yang mereka susun, tetapi itu sering kali canggung. Memiliki runtime yang menolak untuk memuat jenis apa pun di mana leluhur mana pun dapat dijangkau oleh lebih dari satu rute mungkin bisa diterapkan, tetapi akan sangat membatasi kegunaan pewarisan berganda. Mengizinkan jalur pewarisan bersama hanya ketika tidak ada konflik yang akan menciptakan situasi di mana versi lama X akan kompatibel dengan Y lama atau baru, dan Y lama akan kompatibel dengan X lama atau baru, tetapi X baru dan Y baru akan kompatibel, bahkan jika keduanya tidak melakukan sesuatu yang dengan sendirinya harus diharapkan sebagai perubahan yang melanggar.

Beberapa bahasa dan kerangka kerja memungkinkan pewarisan berganda, berdasarkan teori bahwa apa yang diperoleh dari MI lebih penting daripada apa yang harus diberikan untuk mengizinkannya. Biaya MI signifikan, namun, dan untuk banyak kasus antarmuka memberikan 90% manfaat MI dengan sebagian kecil dari biaya.

supercat
sumber
Apakah pemilih yang turun mengklaim untuk berkomentar? Saya percaya bahwa jawaban saya menyediakan informasi yang tidak ada dalam bahasa lain, sehubungan dengan keuntungan semantik yang dapat diterima dengan menolak beberapa pewarisan. Fakta bahwa mengizinkan beberapa warisan akan membutuhkan menyerahkan hal-hal bermanfaat lainnya akan menjadi alasan yang baik bagi siapa saja yang menghargai hal-hal lain lebih dari banyak warisan untuk menentang MI.
supercat
2
Masalah dengan MI mudah dipecahkan atau dihindari sama sekali menggunakan teknik yang sama yang Anda gunakan dengan warisan tunggal. Tak satu pun dari kemampuan / aksioma yang Anda sebutkan secara inheren dihambat oleh MI kecuali untuk yang kedua ... dan jika Anda mewarisi dari dua kelas yang memberikan perilaku berbeda untuk metode yang sama, itu pada Anda untuk menyelesaikan ambiguitas itu pula. Tetapi jika Anda harus melakukan itu, Anda mungkin sudah menyalahgunakan warisan.
cao
@ cHao: Jika seseorang mengharuskan kelas turunan harus menimpa metode induk dan tidak rantai ke mereka (aturan yang sama seperti antarmuka) seseorang dapat menghindari masalah, tapi itu batasan yang cukup parah. Jika seseorang menggunakan pola di mana FirstDerivedFoopenimpaan Bartidak melakukan apa pun selain rantai ke non-virtual yang dilindungi FirstDerivedFoo_Bar, dan SecondDerivedFoomenimpa rantai ke non-virtual yang dilindungi SecondDerivedBar, dan jika kode yang ingin mengakses metode dasar menggunakan metode non-virtual yang dilindungi itu, maka mungkin pendekatan yang bisa diterapkan ...
supercat
... (menghindari sintaksis base.VirtualMethod) tetapi saya tidak tahu adanya dukungan bahasa untuk secara khusus memfasilitasi pendekatan semacam itu. Saya berharap ada cara bersih untuk melampirkan satu blok kode ke metode yang dilindungi non-virtual dan metode virtual dengan tanda tangan yang sama tanpa memerlukan metode virtual "satu-liner" (yang akhirnya mengambil lebih banyak dari satu baris) dalam kode sumber).
supercat
Tidak ada persyaratan inheren dalam MI bahwa kelas tidak memanggil metode dasar, dan tidak ada alasan khusus yang diperlukan. Agak aneh untuk menyalahkan MI, mengingat masalah itu juga mempengaruhi SI.
cao