Lebih suka komposisi daripada warisan?

1604

Mengapa lebih suka komposisi daripada warisan? Imbalan apa yang ada untuk setiap pendekatan? Kapan sebaiknya Anda memilih warisan daripada komposisi?

Hanya baca
sumber
3
Lihat juga desain kelas mana yang lebih baik
maccullt
40
Ada artikel bagus tentang pertanyaan itu di sini . Pendapat pribadi saya adalah bahwa tidak ada prinsip "lebih baik" atau "lebih buruk" untuk desain. Ada desain "tepat" dan "tidak memadai" untuk tugas konkret. Dengan kata lain - saya menggunakan warisan atau komposisi, tergantung situasinya. Tujuannya adalah untuk menghasilkan kode yang lebih kecil, lebih mudah dibaca, digunakan kembali, dan akhirnya diperluas.
m_pGladiator
1
dalam satu kalimat, pewarisan bersifat publik jika Anda memiliki metode publik dan Anda mengubahnya, ia mengubah api yang dipublikasikan. jika Anda memiliki komposisi dan objek yang dikomposisi telah berubah, Anda tidak perlu mengubah api yang Anda terbitkan.
Tomer Ben David

Jawaban:

1189

Lebih suka komposisi daripada pewarisan karena lebih mudah ditempa / mudah dimodifikasi nanti, tetapi jangan gunakan pendekatan selalu-buat. Dengan komposisi, mudah untuk mengubah perilaku saat bepergian dengan Dependency Injection / Setters. Warisan lebih kaku karena sebagian besar bahasa tidak memungkinkan Anda untuk berasal dari lebih dari satu jenis. Jadi angsa lebih atau kurang dimasak setelah Anda berasal dari TypeA.

Tes asam saya untuk di atas adalah:

  • Apakah TypeB ingin mengekspos antarmuka lengkap (semua metode publik tidak kurang) dari TypeA sehingga TypeB dapat digunakan di mana TypeA diharapkan? Menunjukkan Warisan .

    • misalnya biplane Cessna akan mengekspos antarmuka lengkap pesawat terbang, jika tidak lebih. Sehingga membuatnya cocok untuk diturunkan dari Airplane.
  • Apakah TypeB hanya menginginkan sebagian / bagian dari perilaku yang diekspos oleh TypeA? Menunjukkan perlunya Komposisi.

    • mis. Seekor Burung mungkin hanya membutuhkan perilaku terbang Pesawat. Dalam hal ini, masuk akal untuk mengekstraknya sebagai antarmuka / kelas / keduanya dan menjadikannya anggota dari kedua kelas.

Pembaruan: Baru saja kembali ke jawaban saya dan tampaknya sekarang tidak lengkap tanpa menyebutkan secara spesifik Prinsip Pergantian Liskov dari Barbara Liskov sebagai ujian untuk 'Haruskah saya mewarisi dari jenis ini?'

Gishu
sumber
81
Contoh kedua adalah langsung dari Pola Desain Kepala Pertama ( amazon.com/First-Design-Patterns-Elisabeth-Freeman/dp/… ) buku :) Saya akan sangat merekomendasikan buku itu kepada siapa pun yang mencari pertanyaan ini di Google.
Jeshurun
4
Ini sangat jelas, tetapi mungkin melewatkan sesuatu: "Apakah TypeB ingin mengekspos antarmuka lengkap (semua metode publik tidak kurang) dari TypeA sehingga TypeB dapat digunakan di mana TypeA diharapkan?" Tetapi bagaimana jika ini benar, dan TypeB juga mengekspos antarmuka lengkap TypeC? Dan bagaimana jika TypeC belum dimodelkan?
Tristan
4
Anda menyinggung apa yang saya pikir harus menjadi tes paling dasar: "Jika objek ini dapat digunakan oleh kode yang mengharapkan objek dari (apa yang akan) tipe dasar". Jika jawabannya ya, objek tersebut harus mewarisi. Jika tidak, maka mungkin seharusnya tidak. Jika saya memiliki pemabuk saya, bahasa akan memberikan kata kunci untuk merujuk ke "kelas ini", dan menyediakan sarana untuk mendefinisikan kelas yang harus berperilaku seperti kelas lain, tetapi tidak dapat disubstitusikan untuk itu (kelas seperti itu akan memiliki semua "ini referensi kelas "diganti dengan dirinya sendiri).
supercat
22
@ Alexey - intinya adalah 'Bisakah saya melewati Cessna biplane ke semua klien yang mengharapkan pesawat tanpa mengejutkan mereka?'. Jika ya, maka kemungkinan Anda menginginkan warisan.
Gishu
9
Saya benar-benar berjuang untuk memikirkan contoh di mana pewarisan akan menjadi jawaban saya, saya sering menemukan agregasi, komposisi, dan antarmuka menghasilkan solusi yang lebih elegan. Banyak contoh di atas mungkin bisa dijelaskan dengan lebih baik menggunakan pendekatan-pendekatan itu ...
Stuart Wakefield
415

Pikirkan penahanan sebagai memiliki hubungan. Mobil "memiliki" mesin, seseorang "memiliki" nama, dll.

Pikirkan warisan sebagai adalah hubungan. Mobil "adalah" kendaraan, seseorang "adalah" mamalia, dll.

Saya tidak mengambil kredit untuk pendekatan ini. Saya mengambilnya langsung dari Edisi Kedua Kode Lengkap oleh Steve McConnell , Bagian 6.3 .

Nick Zalutskiy
sumber
107
Ini tidak selalu merupakan pendekatan yang sempurna, itu hanyalah pedoman yang baik. Prinsip Substitusi Liskov jauh lebih akurat (gagal kurang).
Bill K
41
"Mobil saya punya kendaraan." Jika Anda menganggap itu sebagai kalimat terpisah, bukan dalam konteks pemrograman, itu sama sekali tidak masuk akal. Dan itulah inti dari teknik ini. Jika kedengarannya canggung, mungkin salah.
Nick Zalutskiy
36
@Nick Tentu, tapi "Mobil saya memiliki VehicleBehavior" lebih masuk akal (saya kira kelas "Vehicle" Anda bisa dinamai "VehicleBehavior"). Jadi Anda tidak dapat mendasarkan keputusan Anda pada perbandingan "memiliki" vs "adalah", Anda harus menggunakan LSP, atau Anda akan membuat kesalahan
Tristan
35
Alih-alih "adalah" memikirkan "berperilaku seperti." Warisan adalah tentang mewarisi perilaku, bukan hanya semantik.
ybakos
4
@ybakos "Berperilaku seperti" dapat dicapai melalui antarmuka tanpa perlu warisan. Dari Wikipedia : "Implementasi komposisi atas warisan biasanya dimulai dengan pembuatan berbagai antarmuka yang mewakili perilaku yang harus ditunjukkan oleh sistem ... Dengan demikian, perilaku sistem diwujudkan tanpa warisan."
DavidRR
210

Jika Anda memahami perbedaannya, lebih mudah untuk dijelaskan.

Kode Prosedural

Contohnya adalah PHP tanpa menggunakan kelas (khususnya sebelum PHP5). Semua logika dikodekan dalam satu set fungsi. Anda dapat memasukkan file-file lain yang mengandung fungsi pembantu dan sebagainya dan melakukan logika bisnis Anda dengan meneruskan data ke berbagai fungsi. Ini bisa sangat sulit untuk dikelola ketika aplikasi tumbuh. PHP5 mencoba untuk memperbaiki ini dengan menawarkan desain yang lebih berorientasi objek.

Warisan

Ini mendorong penggunaan kelas. Warisan adalah salah satu dari tiga prinsip desain OO (pewarisan, polimorfisme, enkapsulasi).

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

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

Ini adalah warisan di tempat kerja. Karyawan "adalah" Orang atau warisan dari Orang. Semua hubungan pewarisan adalah hubungan "is-a". Karyawan juga membayangi properti Judul dari Orang, artinya Karyawan. Judul akan mengembalikan Judul untuk Karyawan, bukan Orang.

Komposisi

Komposisi lebih disukai daripada warisan. Sederhananya Anda harus:

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);

Komposisi biasanya "memiliki" atau "menggunakan hubungan". Di sini kelas Karyawan memiliki Seseorang. Itu tidak mewarisi dari Orang tetapi mendapatkan objek Orang diteruskan kepadanya, itulah sebabnya ia "memiliki" Orang.

Komposisi atas Warisan

Sekarang katakan Anda ingin membuat jenis Manajer sehingga Anda berakhir dengan:

class Manager : Person, Employee {
   ...
}

Contoh ini akan berfungsi dengan baik, bagaimana jika Orang dan Karyawan keduanya menyatakan Title? Haruskah Manajer. Judul mengembalikan "Manajer Operasi" atau "Tuan"? Dalam komposisi ambiguitas ini lebih baik ditangani:

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

Objek Manajer disusun sebagai Karyawan dan Orang. Perilaku Judul diambil dari karyawan. Komposisi eksplisit ini menghilangkan ambiguitas antara lain dan Anda akan menemukan lebih sedikit bug.

aleemb
sumber
6
Untuk warisan: Tidak ada ambiguitas. Anda menerapkan kelas Manajer berdasarkan persyaratan. Jadi, Anda akan mengembalikan "Manajer Operasi" jika itu yang ditentukan oleh persyaratan Anda, jika tidak, Anda hanya akan menggunakan implementasi kelas dasar. Anda juga bisa menjadikan Person sebagai kelas abstrak dan karenanya memastikan kelas hilir menerapkan properti Title.
Raj Rao
68
Penting untuk diingat bahwa orang mungkin mengatakan "Komposisi atas warisan" tetapi itu tidak berarti "Komposisi selalu atas Warisan". "Is a" berarti warisan dan mengarah ke penggunaan kembali kode. Karyawan adalah Orang (Karyawan tidak memiliki orang).
Raj Rao
36
Contohnya membingungkan. Pekerja adalah orang, jadi harus menggunakan warisan. Anda tidak boleh menggunakan komposisi untuk contoh ini, karena itu adalah hubungan yang salah dalam model domain, bahkan jika secara teknis Anda dapat mendeklarasikannya dalam kode.
Michael Freidgeim
15
Saya tidak setuju dengan contoh ini. Pegawai adalah Orang, yang merupakan kasus buku teks tentang penggunaan warisan secara tepat. Saya juga berpikir bahwa "masalah" redefinisi bidang Judul tidak masuk akal. Fakta bahwa Employee.Title membayangi Person.Title adalah tanda pemrograman yang buruk. Lagi pula, apakah "Tuan" dan "Manajer Operasi" benar-benar merujuk pada aspek yang sama dari seseorang (huruf kecil)? Saya akan mengganti nama Employee.Title, dan dengan demikian dapat merujuk atribut Judul dan JobTitle dari seorang Karyawan, yang keduanya masuk akal dalam kehidupan nyata. Lebih lanjut, tidak ada alasan untuk Manajer (lanjutan ...)
Radon Rosborough
9
(... lanjutan) untuk mewarisi dari Orang dan Karyawan - setelah semua, Karyawan sudah mewarisi dari Orang. Dalam model yang lebih kompleks, di mana seseorang mungkin menjadi Manajer dan Agen, memang benar bahwa pewarisan berganda dapat digunakan (hati-hati!), Tetapi akan lebih disukai di banyak lingkungan untuk memiliki kelas Peran abstrak yang darinya Manajer (berisi Karyawan s / dia mengelola) dan Agen (berisi Kontrak, dan informasi lainnya) mewarisi. Kemudian, seorang Karyawan adalah-Orang yang memiliki-Peran ganda. Dengan demikian, baik komposisi dan warisan digunakan dengan benar.
Radon Rosborough
142

Dengan semua manfaat tak terbantahkan yang diberikan oleh warisan, inilah beberapa kelemahannya.

Kerugian Warisan:

  1. Anda tidak dapat mengubah implementasi yang diwarisi dari kelas super saat runtime (jelas karena warisan didefinisikan pada waktu kompilasi).
  2. Inheritance memaparkan subclass ke detail implementasi kelas induknya, itulah mengapa sering dikatakan bahwa inheritance merusak enkapsulasi (dalam arti bahwa Anda benar-benar harus fokus pada antarmuka bukan implementasi, jadi menggunakan kembali oleh subklasifikasi tidak selalu disukai).
  3. Kopling ketat yang disediakan oleh warisan membuat implementasi subclass sangat terikat dengan penerapan kelas super bahwa setiap perubahan dalam implementasi induk akan memaksa sub kelas untuk berubah.
  4. Penggunaan kembali yang berlebihan dengan sub-klasifikasi dapat membuat tumpukan warisan sangat dalam dan sangat membingungkan.

Di sisi lain, komposisi objek didefinisikan pada saat runtime melalui objek yang memperoleh referensi ke objek lain. Dalam kasus seperti itu objek-objek ini tidak akan pernah dapat mencapai data yang dilindungi satu sama lain (tidak ada istirahat enkapsulasi) dan akan dipaksa untuk menghormati antarmuka masing-masing. Dan dalam hal ini juga, dependensi implementasi akan jauh lebih sedikit daripada dalam kasus pewarisan.

Galilyou
sumber
5
Ini adalah salah satu jawaban yang lebih baik, menurut saya - saya akan menambahkan bahwa mencoba memikirkan kembali masalah Anda dalam hal komposisi, dalam pengalaman saya, cenderung mengarah ke kelas yang lebih kecil, lebih sederhana, lebih mandiri, lebih dapat digunakan kembali, dan lebih dapat digunakan kembali. , dengan lingkup tanggung jawab yang lebih jelas, lebih kecil, dan lebih terfokus. Seringkali ini berarti ada sedikit kebutuhan untuk hal-hal seperti injeksi ketergantungan atau mengejek (dalam tes) karena komponen yang lebih kecil biasanya dapat berdiri sendiri. Hanya pengalaman saya saja. YMMV :-)
mindplay.dk
3
Paragraf terakhir dalam posting ini benar-benar diklik untuk saya. Terima kasih.
Salx
87

Alasan lain, sangat pragmatis, untuk lebih memilih komposisi daripada warisan berkaitan dengan model domain Anda, dan memetakannya ke database relasional. Sangat sulit untuk memetakan pewarisan ke model SQL (Anda berakhir dengan semua jenis solusi peretasan, seperti membuat kolom yang tidak selalu digunakan, menggunakan tampilan, dll). Beberapa ORML mencoba menangani ini, tetapi selalu menjadi rumit dengan cepat. Komposisi dapat dengan mudah dimodelkan melalui hubungan kunci asing antara dua tabel, tetapi warisan jauh lebih sulit.

Tim Howland
sumber
81

Sementara dalam kata-kata singkat saya akan setuju dengan "Lebih suka komposisi daripada warisan", sangat sering bagi saya itu terdengar seperti "lebih suka kentang daripada coca-cola". Ada tempat untuk warisan dan tempat untuk komposisi. Anda perlu memahami perbedaan, maka pertanyaan ini akan hilang. Apa artinya bagi saya adalah "jika Anda akan menggunakan warisan - pikirkan lagi, kemungkinan Anda perlu komposisi".

Anda harus memilih kentang daripada coca cola saat Anda ingin makan, dan coca cola daripada kentang ketika Anda ingin minum.

Membuat subkelas harus berarti lebih dari sekadar cara yang nyaman untuk memanggil metode superclass. Anda harus menggunakan pewarisan ketika subclass "is-a" kelas super baik secara struktural dan fungsional, ketika itu dapat digunakan sebagai superclass dan Anda akan menggunakannya. Jika bukan itu masalahnya - itu bukan warisan, tetapi sesuatu yang lain. Komposisi adalah ketika objek Anda terdiri dari yang lain, atau memiliki hubungan dengan mereka.

Jadi bagi saya kelihatannya jika seseorang tidak tahu apakah dia membutuhkan warisan atau komposisi, masalah sebenarnya adalah dia tidak tahu apakah dia ingin minum atau makan. Pikirkan domain masalah Anda lebih lanjut, pahami dengan lebih baik.

Pavel Feldman
sumber
5
Alat yang tepat untuk pekerjaan yang tepat. Palu bisa lebih baik dalam memukul benda daripada kunci pas, tapi itu tidak berarti kita harus melihat kunci pas sebagai "palu inferior". Warisan dapat membantu ketika hal-hal yang ditambahkan ke subclass diperlukan untuk objek berperilaku sebagai objek superclass. Sebagai contoh, pertimbangkan kelas dasar InternalCombustionEnginedengan kelas turunan GasolineEngine. Yang terakhir menambahkan hal-hal seperti busi, yang tidak dimiliki oleh kelas dasar, tetapi menggunakan hal itu sebagai bilah InternalCombustionEngineakan menyebabkan busi digunakan.
supercat
61

Warisan cukup menarik terutama yang berasal dari tanah prosedural dan sering terlihat elegan. Maksud saya semua yang perlu saya lakukan adalah menambahkan sedikit fungsionalitas ini ke beberapa kelas lain, kan? Nah, salah satu masalahnya adalah itu

warisan mungkin merupakan bentuk penggabungan terburuk yang Anda miliki

Kelas dasar Anda memecah enkapsulasi dengan mengekspos detail implementasi ke subclass dalam bentuk anggota yang dilindungi. Ini membuat sistem Anda kaku dan rapuh. Namun kelemahan yang lebih tragis adalah subclass baru membawa semua bagasi dan pendapat dari rantai pewarisan.

Artikel, Inheritance is Evil: The Epic Fail dari DataAnnotationsModelBinder , membahas contoh ini dalam C #. Ini menunjukkan penggunaan pewarisan ketika komposisi seharusnya digunakan dan bagaimana itu bisa direactored.

Mike Valenty
sumber
Warisan tidak baik atau buruk, itu hanya kasus Komposisi khusus. Di mana, memang, subclass menerapkan fungsi yang mirip dengan superclass. Jika subkelas yang Anda usulkan tidak menerapkan kembali tetapi hanya menggunakan fungsionalitas dari superclass, maka Anda telah menggunakan Warisan secara salah. Itu adalah kesalahan programmer, bukan refleksi tentang Warisan.
iPherian
42

Dalam Java atau C #, objek tidak dapat mengubah tipenya setelah instantiated.

Jadi, jika objek Anda perlu tampil sebagai objek yang berbeda atau berperilaku berbeda tergantung pada keadaan atau kondisi objek, maka gunakan Komposisi : Lihat Pola Desain Negara dan Strategi .

Jika objek harus berjenis sama, maka gunakan Inheritance atau implement interfaces.

dance2die
sumber
10
+1 Saya semakin jarang menemukan bahwa warisan bekerja di sebagian besar situasi. Saya lebih suka berbagi / mewarisi antarmuka dan komposisi objek .... atau itu disebut agregasi? Jangan tanya saya, saya punya gelar EE !!
kenny
Saya percaya bahwa ini adalah skenario yang paling umum di mana "komposisi atas warisan" berlaku karena keduanya bisa sesuai secara teori. Misalnya, dalam sistem pemasaran Anda mungkin memiliki konsep a Client. Kemudian, muncul konsep baru yang PreferredClientmuncul kemudian. Haruskah PreferredClientmewarisi Client? Klien yang lebih disukai adalah klien, toh? Ya, tidak terlalu cepat ... seperti yang Anda katakan objek tidak dapat mengubah kelasnya saat runtime. Bagaimana Anda membuat model client.makePreferred()operasi? Mungkin jawabannya terletak pada penggunaan komposisi dengan konsep yang hilang, Accountmungkin?
plalx
Daripada memiliki jenis Clientkelas yang berbeda , mungkin hanya ada satu yang merangkum konsep Accountyang bisa menjadi StandardAccountatau PreferredAccount...
plalx
40

Tidak menemukan jawaban yang memuaskan di sini, jadi saya menulis yang baru.

Untuk memahami mengapa " lebih memilih komposisi daripada pewarisan", pertama-tama kita perlu mendapatkan kembali asumsi yang dihilangkan dalam idiom singkat ini.

Ada dua manfaat warisan: subtyping dan subclassing

  1. Subtyping berarti menyesuaikan diri dengan tanda tangan jenis (antarmuka), yaitu satu set API, dan satu dapat menimpa bagian dari tanda tangan untuk mencapai polimorfisme subtyping.

  2. Subclassing berarti penggunaan kembali implisit implementasi metode.

Dengan dua manfaat tersebut muncul dua tujuan berbeda untuk melakukan pewarisan: berorientasi subtyping dan berorientasi penggunaan kembali kode.

Jika penggunaan kembali kode adalah satu - satunya tujuan, subclassing dapat memberikan satu lebih dari apa yang dia butuhkan, yaitu beberapa metode publik dari kelas induk tidak masuk akal untuk kelas anak. Dalam hal ini, alih-alih lebih menyukai komposisi daripada warisan, komposisi dituntut . Di sinilah pula gagasan "is-a" vs. "has-a" berasal.

Jadi hanya ketika subtyping dimaksudkan, yaitu untuk menggunakan kelas baru nanti secara polimorfik, kita menghadapi masalah dalam memilih pewarisan atau komposisi. Ini adalah asumsi yang dihilangkan dalam idiom singkat yang sedang dibahas.

Untuk subtipe adalah untuk menyesuaikan dengan tanda tangan jenis, ini berarti komposisi harus selalu mengekspos jumlah API yang tidak sedikit. Sekarang trade off menendang:

  1. Warisan menyediakan penggunaan kembali kode langsung jika tidak diganti, sementara komposisi harus mengode ulang setiap API, bahkan jika itu hanya pekerjaan delegasi yang sederhana.

  2. Warisan menyediakan rekursi terbuka langsung melalui situs polimorfik internal this, yaitu menggunakan metode override (atau tipe genap ) dalam fungsi anggota lain, baik publik atau pribadi (meskipun tidak disarankan ). Rekursi terbuka dapat disimulasikan melalui komposisi , tetapi membutuhkan usaha ekstra dan mungkin tidak selalu layak (?). Ini jawaban untuk pertanyaan digandakan berbicara sesuatu yang serupa.

  3. Warisan memperlihatkan anggota yang dilindungi . Ini memecah enkapsulasi dari kelas induk, dan jika digunakan oleh subkelas, ketergantungan lain antara anak dan orang tuanya diperkenalkan.

  4. Komposisi memiliki kecocokan inversi kontrol, dan ketergantungannya dapat disuntikkan secara dinamis, seperti yang ditunjukkan dalam pola dekorator dan pola proksi .

  5. Komposisi memiliki manfaat pemrograman berorientasi kombinator , yaitu bekerja dengan cara seperti pola komposit .

  6. Komposisi segera mengikuti pemrograman ke antarmuka .

  7. Komposisi memiliki manfaat pewarisan berganda yang mudah .

Dengan mempertimbangkan trade off di atas, kami karenanya lebih memilih komposisi daripada warisan. Namun untuk kelas yang terkait erat, yaitu ketika penggunaan kembali kode implisit benar-benar membuat manfaat, atau kekuatan magis rekursi terbuka diinginkan, pewarisan akan menjadi pilihan.

lcn
sumber
34

Secara pribadi saya belajar untuk selalu lebih suka komposisi daripada warisan. Tidak ada masalah terprogram yang dapat Anda pecahkan dengan pewarisan yang tidak dapat Anda pecahkan dengan komposisi; meskipun Anda mungkin harus menggunakan Antarmuka (Java) atau Protokol (Obj-C) dalam beberapa kasus. Karena C ++ tidak tahu hal seperti itu, Anda harus menggunakan kelas basis abstrak, yang berarti Anda tidak bisa sepenuhnya menghilangkan warisan dalam C ++.

Komposisi seringkali lebih logis, memberikan abstraksi yang lebih baik, enkapsulasi yang lebih baik, penggunaan kembali kode yang lebih baik (terutama dalam proyek yang sangat besar) dan kecil kemungkinannya untuk memecahkan apa pun dari kejauhan hanya karena Anda membuat perubahan terisolasi di mana saja dalam kode Anda. Ini juga memudahkan untuk menegakkan " Prinsip Tanggung Jawab Tunggal ", yang sering diringkas sebagai " Seharusnya tidak ada lebih dari satu alasan bagi suatu kelas untuk berubah. ", Dan itu berarti bahwa setiap kelas ada untuk tujuan tertentu dan itu harus hanya memiliki metode yang terkait langsung dengan tujuannya. Juga memiliki pohon warisan yang sangat dangkal membuatnya lebih mudah untuk menjaga ikhtisar bahkan ketika proyek Anda mulai menjadi sangat besar. Banyak orang berpikir bahwa warisan mewakili dunia nyata kitacukup bagus, tapi itu tidak benar. Dunia nyata menggunakan komposisi jauh lebih banyak daripada warisan. Hampir setiap objek dunia nyata yang dapat Anda pegang di tangan Anda telah tersusun dari objek dunia nyata lainnya yang lebih kecil.

Namun, ada kekurangan komposisi. Jika Anda melewatkan warisan sama sekali dan hanya fokus pada komposisi, Anda akan melihat bahwa Anda sering harus menulis beberapa baris kode tambahan yang tidak diperlukan jika Anda telah menggunakan warisan. Anda juga kadang-kadang dipaksa untuk mengulangi sendiri dan ini melanggar Prinsip KERING(KERING = Jangan Ulangi Diri Sendiri). Komposisi juga sering membutuhkan pendelegasian, dan suatu metode hanya memanggil metode lain dari objek lain tanpa kode lain di sekitar panggilan ini. "Pemanggilan metode ganda" semacam itu (yang mungkin dengan mudah mencakup pemanggilan metode tripel atau empat kali lipat dan bahkan lebih jauh dari itu) memiliki kinerja yang jauh lebih buruk daripada pewarisan, di mana Anda cukup mewarisi metode orang tua Anda. Memanggil metode yang diwarisi mungkin sama cepatnya dengan memanggil metode yang tidak diwarisi, atau mungkin sedikit lebih lambat, tetapi biasanya masih lebih cepat dari dua panggilan metode yang berurutan.

Anda mungkin telah memperhatikan bahwa sebagian besar bahasa OO tidak mengizinkan banyak pewarisan. Meskipun ada beberapa kasus di mana pewarisan berganda benar-benar dapat membelikan Anda sesuatu, tetapi itu lebih merupakan pengecualian daripada aturan. Setiap kali Anda mengalami situasi di mana Anda berpikir "pewarisan berganda akan menjadi fitur yang sangat keren untuk menyelesaikan masalah ini", Anda biasanya berada pada titik di mana Anda harus memikirkan kembali warisan secara keseluruhan, karena bahkan mungkin memerlukan beberapa baris kode tambahan , solusi berdasarkan komposisi biasanya akan menjadi jauh lebih elegan, fleksibel dan bukti masa depan.

Warisan adalah fitur yang sangat keren, tapi saya khawatir ini sudah terlalu sering digunakan beberapa tahun terakhir. Orang memperlakukan warisan sebagai satu palu yang dapat memakukan semuanya, terlepas dari apakah itu benar-benar paku, sekrup, atau mungkin sesuatu yang sama sekali berbeda.

Mecki
sumber
"Banyak orang berpikir bahwa warisan mewakili dunia nyata kita dengan cukup baik, tetapi itu bukan kebenaran." Begitu banyak ini! Berlawanan dengan hampir semua tutorial pemrograman di dunia, memodelkan objek dunia nyata sebagai rantai pewarisan mungkin merupakan ide yang buruk dalam jangka panjang. Anda harus menggunakan warisan hanya ketika ada hubungan yang sangat jelas, bawaan, sederhana. Suka TextFileadalah a File.
neonblitzer
25

Aturan umum saya: Sebelum menggunakan warisan, pertimbangkan apakah komposisi lebih masuk akal.

Alasan: Subkelas biasanya berarti lebih banyak kerumitan dan keterhubungan, yaitu lebih sulit untuk diubah, dipertahankan, dan diukur tanpa membuat kesalahan.

Jawaban yang jauh lebih lengkap dan konkret dari Tim Boudreau of Sun:

Masalah umum untuk penggunaan warisan seperti yang saya lihat adalah:

  • Tindakan yang tidak bersalah dapat memiliki hasil yang tidak terduga - Contoh klasik dari ini adalah panggilan ke metode yang dapat ditimpa dari superclass constructor, sebelum bidang instance subclass telah diinisialisasi. Di dunia yang sempurna, tidak ada yang akan melakukan itu. Ini bukan dunia yang sempurna.
  • Ini menawarkan godaan sesat bagi subclassers untuk membuat asumsi tentang urutan pemanggilan metode dan semacamnya - asumsi semacam itu cenderung tidak stabil jika superclass dapat berkembang dari waktu ke waktu. Lihat juga analogi pemanggang roti dan teko kopi saya .
  • Kelas menjadi lebih berat - Anda tidak perlu tahu apa pekerjaan superclass Anda lakukan dalam konstruktornya, atau berapa banyak memori yang akan digunakan. Jadi membangun beberapa objek ringan yang tidak bersalah bisa jauh lebih mahal daripada yang Anda pikirkan, dan ini dapat berubah seiring waktu jika superclass berevolusi
  • Ini mendorong ledakan subclass . Classloading menghabiskan waktu, lebih banyak kelas membutuhkan memori. Ini mungkin bukan masalah sampai Anda berurusan dengan aplikasi pada skala NetBeans, tetapi di sana, kami memiliki masalah nyata dengan, misalnya, menu menjadi lambat karena tampilan pertama menu memicu pemuatan kelas besar-besaran. Kami memperbaiki ini dengan beralih ke sintaksis yang lebih deklaratif dan teknik lainnya, tetapi itu membutuhkan waktu untuk memperbaikinya juga.
  • Itu membuat lebih sulit untuk mengubah hal-hal nanti - jika Anda telah membuat publik kelas, menukar superclass akan merusak subclass - itu adalah pilihan yang, setelah Anda membuat kode publik, Anda menikah dengan. Jadi, jika Anda tidak mengubah fungsionalitas nyata ke superclass Anda, Anda mendapatkan lebih banyak kebebasan untuk mengubah hal-hal nanti jika Anda gunakan, daripada memperpanjang hal yang Anda butuhkan. Sebagai contoh, ambil subkelas JPanel - ini biasanya salah; dan jika subclass bersifat publik di suatu tempat, Anda tidak pernah mendapatkan kesempatan untuk meninjau kembali keputusan itu. Jika itu diakses sebagai JComponent getThePanel (), Anda masih bisa melakukannya (petunjuk: memaparkan model untuk komponen-komponen di dalamnya sebagai API Anda).
  • Hirarki objek tidak menskala (atau membuatnya skalanya nanti jauh lebih sulit daripada merencanakan ke depan) - ini adalah masalah klasik "terlalu banyak lapisan". Saya akan membahas ini di bawah, dan bagaimana pola AskTheOracle dapat menyelesaikannya (meskipun mungkin menyinggung puritan OOP).

...

Pandangan saya tentang apa yang harus dilakukan, jika Anda mengizinkan warisan, yang dapat Anda ambil dengan sebutir garam adalah:

  • Paparkan tidak ada bidang, selamanya, kecuali konstanta
  • Metode harus abstrak atau final
  • Sebut tidak ada metode dari konstruktor superclass

...

semua ini berlaku kurang untuk proyek-proyek kecil daripada yang besar, dan lebih sedikit untuk kelas swasta daripada yang publik

Peter Tseng
sumber
25

Mengapa lebih suka komposisi daripada warisan?

Lihat jawaban lain.

Kapan Anda bisa menggunakan warisan?

Sering dikatakan bahwa kelas Bardapat mewarisi kelas Fooketika kalimat berikut ini benar:

  1. sebuah bar adalah foo

Sayangnya, tes di atas saja tidak dapat diandalkan. Gunakan yang berikut ini sebagai gantinya:

  1. bar adalah foo, DAN
  2. bar dapat melakukan semua hal yang dapat dilakukan oleh foos.

Memastikan tes pertama bahwa semua getter dari Foomasuk akal di Bar(= sifat bersama), sedangkan tes kedua memastikan bahwa semua setter dari Foomasuk akal di Bar(fungsi = bersama).

Contoh 1: Anjing -> Hewan

Seekor anjing adalah binatang DAN anjing dapat melakukan segala sesuatu yang dapat dilakukan binatang (seperti bernapas, sekarat, dll.). Oleh karena itu, kelas Dog dapat mewarisi kelas tersebut Animal.

Contoh 2: Lingkaran - / -> Elips

Lingkaran adalah elips TETAP lingkaran tidak bisa melakukan semua yang bisa dilakukan elips. Misalnya, lingkaran tidak bisa meregang, sedangkan elips bisa. Oleh karena itu, kelas Circle tidak dapat mewarisi kelas Ellipse.

Ini disebut masalah Circle-Ellipse , yang sebenarnya bukan masalah, hanya bukti yang jelas bahwa tes pertama saja tidak cukup untuk menyimpulkan bahwa pewarisan itu mungkin. Secara khusus, contoh ini menyoroti bahwa kelas turunan harus memperluas fungsi kelas dasar, jangan pernah batasi . Jika tidak, kelas dasar tidak dapat digunakan secara polimorfik.

Kapan Anda harus menggunakan warisan?

Bahkan jika Anda dapat menggunakan warisan bukan berarti Anda harus : menggunakan komposisi selalu merupakan pilihan. Warisan adalah alat yang ampuh yang memungkinkan penggunaan kembali kode implisit dan pengiriman dinamis, tetapi ia datang dengan beberapa kelemahan, itulah sebabnya komposisi sering lebih disukai. Pertukaran antara warisan dan komposisi tidak jelas, dan menurut saya paling baik dijelaskan dalam jawaban lcn .

Sebagai aturan praktis, saya cenderung memilih pewarisan daripada komposisi ketika penggunaan polimorfik diharapkan menjadi sangat umum, dalam hal ini kekuatan pengiriman dinamis dapat mengarah ke API yang jauh lebih mudah dibaca dan elegan. Misalnya, memiliki kelas polimorfik Widgetdalam kerangka kerja GUI, atau kelas polimorfik Nodedi pustaka XML memungkinkan untuk memiliki API yang jauh lebih mudah dibaca dan intuitif untuk digunakan daripada apa yang akan Anda miliki dengan solusi murni berdasarkan komposisi.

Prinsip Pergantian Liskov

Asal tahu saja, metode lain yang digunakan untuk menentukan apakah pewarisan itu mungkin disebut Prinsip Substitusi Liskov :

Fungsi yang menggunakan pointer atau referensi ke kelas dasar harus dapat menggunakan objek dari kelas turunan tanpa menyadarinya

Pada dasarnya, ini berarti bahwa pewarisan adalah mungkin jika kelas dasar dapat digunakan secara polimorfik, yang saya percaya setara dengan pengujian kami "sebuah bar adalah foo dan bar dapat melakukan semua yang dapat dilakukan foo".

Boris Dalstein
sumber
Skenario lingkaran-elips dan persegi panjang adalah contoh yang buruk. Subkelas selalu lebih kompleks daripada superclass mereka, jadi masalahnya dibuat-buat. Masalah ini diselesaikan dengan membalik hubungan. Elips berasal dari lingkaran dan persegi panjang berasal dari kotak. Sangat konyol menggunakan komposisi dalam skenario ini.
Fuzzy Logic
@ FuzzyLogic Setuju, tetapi pada kenyataannya, posting saya tidak pernah menganjurkan untuk menggunakan komposisi dalam kasus ini. Saya hanya mengatakan bahwa masalah elips lingkaran adalah contoh yang bagus mengapa "is-a" bukanlah ujian yang baik untuk menyimpulkan bahwa Circle seharusnya berasal dari Ellipse. Setelah kami menyimpulkan bahwa sebenarnya, Circle seharusnya tidak berasal dari Ellipse karena melanggar LSP, maka opsi yang memungkinkan adalah membalikkan hubungan, atau menggunakan komposisi, atau menggunakan kelas templat, atau menggunakan desain yang lebih kompleks yang melibatkan kelas tambahan atau fungsi pembantu, dll ... dan keputusan jelas harus diambil berdasarkan kasus per kasus.
Boris Dalstein
1
@ FuzzyLogic Dan jika Anda ingin tahu tentang apa yang akan saya anjurkan untuk kasus spesifik Circle-Ellipse: Saya akan menganjurkan tidak menerapkan kelas Lingkaran. Masalah dengan membalikkan hubungan adalah bahwa itu juga melanggar LSP: bayangkan fungsinya computeArea(Circle* c) { return pi * square(c->radius()); }. Jelas rusak jika melewati Ellipse (apa arti radius () bahkan artinya?). Ellipse bukan Circle, dan karena itu seharusnya tidak berasal dari Circle.
Boris Dalstein
computeArea(Circle *c) { return pi * width * height / 4.0; }Sekarang generik.
Fuzzy Logic
2
@ FuzzyLogic Saya tidak setuju: Anda menyadari bahwa ini berarti bahwa Lingkaran kelas mengantisipasi keberadaan kelas turunan Ellipse, dan oleh karena itu menyediakan width()dan height()? Bagaimana jika sekarang pengguna perpustakaan memutuskan untuk membuat kelas lain yang disebut "EggShape"? Haruskah itu juga berasal dari "Lingkaran"? Tentu saja tidak. Bentuk telur bukan lingkaran, dan elips juga bukan lingkaran, jadi tidak ada yang berasal dari Lingkaran karena itu menghancurkan LSP. Metode yang melakukan operasi pada kelas Circle * membuat asumsi kuat tentang apa lingkaran itu, dan melanggar asumsi ini hampir pasti akan menyebabkan bug.
Boris Dalstein
19

Warisan sangat kuat, tetapi Anda tidak bisa memaksanya (lihat: masalah lingkaran-elips ). Jika Anda benar-benar tidak dapat sepenuhnya yakin tentang hubungan subtipe "is-a" yang sebenarnya, maka yang terbaik adalah memilih komposisi.

Yukondude
sumber
15

Warisan menciptakan hubungan yang kuat antara subclass dan kelas super; subclass harus mengetahui detail implementasi super class. Menciptakan kelas super jauh lebih sulit, ketika Anda harus berpikir tentang bagaimana itu dapat diperluas. Anda harus mendokumentasikan invarian kelas dengan hati-hati, dan nyatakan apa metode lain yang bisa digunakan metode yang dapat ditimpa secara internal.

Warisan kadang berguna, jika hirarki benar-benar mewakili hubungan is-a-a. Ini terkait dengan Prinsip Terbuka-Tertutup, yang menyatakan bahwa kelas harus ditutup untuk modifikasi tetapi terbuka untuk ekstensi. Dengan begitu Anda dapat memiliki polimorfisme; untuk memiliki metode generik yang berhubungan dengan tipe super dan metodenya, tetapi melalui pengiriman dinamis metode subclass dipanggil. Ini fleksibel, dan membantu menciptakan tipuan, yang sangat penting dalam perangkat lunak (untuk mengetahui lebih sedikit tentang detail implementasi).

Warisan mudah digunakan secara berlebihan, dan menciptakan kompleksitas tambahan, dengan ketergantungan antar kelas. Juga memahami apa yang terjadi selama pelaksanaan program menjadi sangat sulit karena lapisan dan pemilihan metode panggilan yang dinamis.

Saya akan menyarankan menggunakan menulis sebagai default. Ini lebih modular, dan memberikan manfaat ikatan yang lebih lambat (Anda dapat mengubah komponen secara dinamis). Juga lebih mudah untuk menguji hal-hal secara terpisah. Dan jika Anda perlu menggunakan metode dari kelas, Anda tidak dipaksa dari bentuk tertentu (Prinsip Pergantian Liskov).

egaga
sumber
3
Perlu dicatat bahwa warisan bukan satu-satunya cara untuk mencapai polimorfisme. Pola Dekorator memberikan penampilan polimorfisme melalui komposisi.
BitMask777
1
@ BitMask777: Subtipe polimorfisme hanya satu jenis polimorfisme, yang lain adalah polimorfisme parametrik, Anda tidak perlu warisan untuk itu. Juga yang lebih penting: ketika berbicara tentang warisan, yang satu berarti warisan kelas; .yaitu Anda dapat memiliki subtipe polimorfisme dengan memiliki antarmuka umum untuk beberapa kelas, dan Anda tidak mendapatkan masalah pewarisan.
egaga
2
@engaga: Saya menafsirkan komentar Anda Inheritance is sometimes useful... That way you can have polymorphismsebagai penghubung keras konsep-konsep pewarisan dan polimorfisme (subtyping diasumsikan diberikan konteksnya). Komentar saya dimaksudkan untuk menunjukkan apa yang Anda klarifikasi dalam komentar Anda: bahwa warisan bukan satu-satunya cara untuk menerapkan polimorfisme, dan pada kenyataannya tidak selalu menjadi faktor penentu ketika memutuskan antara komposisi dan warisan.
BitMask777
15

Misalkan pesawat hanya memiliki dua bagian: mesin dan sayap.
Lalu ada dua cara mendesain kelas pesawat terbang.

Class Aircraft extends Engine{
  var wings;
}

Sekarang pesawat Anda dapat mulai dengan memiliki sayap tetap
dan mengubahnya menjadi sayap putar dengan cepat. Ini pada dasarnya
mesin dengan sayap. Tetapi bagaimana jika saya ingin mengganti
mesin dengan cepat juga?

Entah kelas dasar Enginememperlihatkan mutator untuk mengubah
propertinya, atau saya mendesain ulang Aircraftsebagai:

Class Aircraft {
  var wings;
  var engine;
}

Sekarang, saya dapat mengganti mesin saya dengan cepat juga.

simplfuzz
sumber
Posting Anda memunculkan poin yang tidak pernah saya pertimbangkan sebelumnya - untuk melanjutkan analogi benda mekanik Anda dengan banyak bagian, pada sesuatu seperti senjata api, umumnya ada satu bagian yang ditandai dengan nomor seri, yang nomor serinya dianggap sebagai bahwa senjata api secara keseluruhan (untuk pistol, biasanya akan menjadi bingkai). Satu dapat mengganti semua bagian lain dan masih memiliki senjata api yang sama, tetapi jika bingkai retak dan perlu diganti, hasil merakit bingkai baru dengan semua bagian lain dari senjata asli akan menjadi senjata baru. Perhatikan bahwa ...
supercat
... fakta bahwa banyak bagian dari suatu senjata mungkin memiliki nomor seri yang ditandai pada mereka tidak berarti bahwa suatu senjata dapat memiliki banyak identitas. Hanya nomor seri pada bingkai yang mengidentifikasi pistol; nomor seri pada bagian lain mengidentifikasi senjata apa yang diproduksi oleh bagian-bagian yang dirakit, yang mungkin bukan senjata yang dirakit pada waktu tertentu.
supercat
7

Saat Anda ingin "menyalin" / Mengekspos API kelas dasar, Anda menggunakan warisan. Saat Anda hanya ingin "menyalin" fungsionalitas, gunakan delegasi.

Salah satu contohnya: Anda ingin membuat Stack out of a List. Stack hanya memiliki pop, push, dan intip. Anda tidak boleh menggunakan pewarisan karena Anda tidak ingin fungsi push_back, push_front, removeAt, dkk. Di Stack.

Anzurio
sumber
7

Dua cara ini dapat hidup bersama dengan baik dan benar-benar saling mendukung.

Komposisi hanya memainkannya modular: Anda membuat antarmuka yang mirip dengan kelas induk, membuat objek baru dan mendelegasikan panggilan ke sana. Jika benda-benda ini tidak perlu saling mengenal, itu cukup aman dan mudah digunakan komposisi. Ada banyak kemungkinan di sini.

Namun, jika kelas induk karena alasan tertentu perlu mengakses fungsi yang disediakan oleh "kelas anak" untuk programmer yang tidak berpengalaman, itu mungkin terlihat seperti tempat yang bagus untuk menggunakan warisan. Kelas induk hanya dapat memanggil abstrak itu sendiri "foo ()" yang ditimpa oleh subkelas dan kemudian dapat memberikan nilai ke basis abstrak.

Sepertinya ide yang bagus, tetapi dalam banyak kasus lebih baik hanya memberi kelas objek yang mengimplementasikan foo () (atau bahkan mengatur nilai yang disediakan foo () secara manual) daripada mewarisi kelas baru dari beberapa kelas dasar yang membutuhkan fungsi foo () akan ditentukan.

Mengapa?

Karena pewarisan adalah cara yang buruk dalam memindahkan informasi .

Komposisi memiliki keunggulan nyata di sini: hubungannya dapat dibalik: "kelas induk" atau "pekerja abstrak" dapat mengagregasi objek "anak" tertentu yang mengimplementasikan antarmuka tertentu + setiap anak dapat diatur di dalam jenis induk lain, yang menerima itu tipe . Dan bisa ada sejumlah objek, misalnya MergeSort atau QuickSort dapat mengurutkan daftar objek yang mengimplementasikan abstrak -bandingkan antarmuka. Atau dengan kata lain: grup objek apa pun yang menerapkan "foo ()" dan grup objek lain yang dapat memanfaatkan objek yang memiliki "foo ()" dapat bermain bersama.

Saya dapat memikirkan tiga alasan nyata untuk menggunakan warisan:

  1. Anda memiliki banyak kelas dengan antarmuka yang sama dan Anda ingin menghemat waktu menulisnya
  2. Anda harus menggunakan Kelas Dasar yang sama untuk setiap objek
  3. Anda perlu memodifikasi variabel pribadi, yang tidak dapat publik dalam hal apa pun

Jika ini benar, maka mungkin perlu menggunakan warisan.

Tidak ada yang buruk dalam menggunakan alasan 1, itu hal yang sangat baik untuk memiliki antarmuka yang solid pada objek Anda. Ini dapat dilakukan menggunakan komposisi atau dengan warisan, tidak masalah - jika antarmuka ini sederhana dan tidak berubah. Biasanya warisan cukup efektif di sini.

Jika alasannya adalah nomor 2, itu menjadi sedikit rumit. Apakah Anda benar-benar hanya perlu menggunakan kelas dasar yang sama? Secara umum, hanya menggunakan kelas dasar yang sama tidak cukup baik, tetapi mungkin merupakan persyaratan kerangka kerja Anda, pertimbangan desain yang tidak dapat dihindari.

Namun, jika Anda ingin menggunakan variabel pribadi, case 3, maka Anda mungkin dalam masalah. Jika Anda menganggap variabel global tidak aman, maka Anda harus mempertimbangkan menggunakan pewarisan untuk mendapatkan akses ke variabel pribadi juga tidak aman . Ingat, variabel global tidak semuanya buruk - basis data pada dasarnya adalah kumpulan besar variabel global. Tetapi jika Anda bisa mengatasinya, maka itu cukup baik.

Tero Tolonen
sumber
7

Untuk menjawab pertanyaan ini dari perspektif yang berbeda untuk programmer yang lebih baru:

Warisan sering diajarkan sejak dini ketika kita mempelajari pemrograman berorientasi objek, jadi itu dipandang sebagai solusi mudah untuk masalah umum.

Saya memiliki tiga kelas yang semuanya membutuhkan fungsionalitas umum. Jadi jika saya menulis kelas dasar dan memiliki semuanya mewarisi itu, maka mereka semua akan memiliki fungsi itu dan saya hanya perlu mempertahankannya di satu tempat.

Kedengarannya hebat, tetapi dalam praktiknya hampir tidak pernah berhasil, karena salah satu dari beberapa alasan:

  • Kami menemukan bahwa ada beberapa fungsi lain yang kami inginkan untuk dimiliki oleh kelas kami. Jika cara kita menambahkan fungsionalitas ke kelas adalah melalui pewarisan, kita harus memutuskan - apakah kita menambahkannya ke kelas dasar yang ada, meskipun tidak setiap kelas yang mewarisi darinya membutuhkan fungsionalitas itu? Apakah kita membuat kelas dasar lain? Tapi bagaimana dengan kelas yang sudah diwarisi dari kelas dasar lainnya?
  • Kami menemukan bahwa hanya untuk satu kelas yang mewarisi dari kelas dasar kami, kami ingin kelas dasar berperilaku sedikit berbeda. Jadi sekarang kita kembali dan mengotak-atik kelas dasar kita, mungkin menambahkan beberapa metode virtual, atau lebih buruk lagi, beberapa kode yang mengatakan, "Jika saya mewarisi tipe A, lakukan ini, tetapi jika saya mewarisi tipe B, lakukan itu . " Itu buruk karena banyak alasan. Pertama adalah bahwa setiap kali kita mengubah kelas dasar, kita secara efektif mengubah setiap kelas warisan. Jadi kita benar-benar mengubah kelas A, B, C, dan D karena kita memerlukan perilaku yang sedikit berbeda di kelas A. Hati-hati seperti yang kita pikirkan, kita mungkin mematahkan salah satu kelas itu untuk alasan yang tidak ada hubungannya dengan mereka kelas.
  • Kita mungkin tahu mengapa kita memutuskan untuk membuat semua kelas ini diwarisi dari satu sama lain, tetapi mungkin tidak (mungkin tidak akan) masuk akal bagi orang lain yang harus menjaga kode kita. Kita mungkin memaksa mereka ke dalam pilihan yang sulit - apakah saya melakukan sesuatu yang sangat jelek dan berantakan untuk membuat perubahan yang saya butuhkan (lihat poin sebelumnya) atau apakah saya hanya menulis ulang banyak dari ini.

Pada akhirnya, kami mengikat kode kami dalam beberapa simpul yang sulit dan tidak mendapat manfaat apa pun darinya kecuali bahwa kami dapat berkata, "Keren, saya belajar tentang warisan dan sekarang saya menggunakannya." Itu tidak dimaksudkan untuk merendahkan karena kita semua sudah melakukannya. Tetapi kami semua melakukannya karena tidak ada yang mengatakan kepada kami untuk tidak melakukannya.

Segera setelah seseorang menjelaskan "mendukung komposisi daripada warisan" kepada saya, saya berpikir kembali setiap kali saya mencoba untuk berbagi fungsionalitas antara kelas menggunakan warisan dan menyadari bahwa sebagian besar waktu itu tidak benar-benar berfungsi dengan baik.

Penangkal adalah Prinsip Tanggung Jawab Tunggal . Anggap saja sebagai kendala. Kelas saya harus melakukan satu hal. Saya harus bisa memberi nama kelas saya yang entah bagaimana menggambarkan satu hal yang dilakukannya. (Ada pengecualian untuk semuanya, tetapi aturan absolut kadang-kadang lebih baik ketika kita belajar.) Karena itu saya tidak bisa menulis kelas dasar yang disebut ObjectBaseThatContainsVariousFunctionsNeededByDifferentClasses. Apa pun fungsi berbeda yang saya butuhkan harus berada di kelasnya sendiri, dan kemudian kelas lain yang membutuhkan fungsionalitas itu dapat bergantung pada kelas itu, bukan mewarisi darinya.

Dengan risiko penyederhanaan yang berlebihan, itu adalah komposisi - menyusun beberapa kelas untuk bekerja bersama. Dan begitu kita membentuk kebiasaan itu, kita mendapati bahwa itu jauh lebih fleksibel, dapat dipertahankan, dan dapat diuji daripada menggunakan warisan.

Scott Hannen
sumber
Bahwa kelas tidak dapat menggunakan beberapa kelas dasar bukanlah refleksi yang buruk pada Warisan tetapi refleksi yang buruk pada kurangnya kemampuan bahasa tertentu.
iPherian
Sejak menulis jawaban ini, saya membaca posting ini dari "Paman Bob" yang membahas tentang kurangnya kemampuan. Saya tidak pernah menggunakan bahasa yang memungkinkan pewarisan berganda. Tetapi melihat ke belakang, pertanyaan itu ditandai "bahasa agnostik" dan jawaban saya mengasumsikan C #. Saya perlu memperluas wawasan saya.
Scott Hannen
6

Selain memiliki / memiliki pertimbangan, seseorang juga harus mempertimbangkan "kedalaman" warisan yang harus dilalui objek Anda. Apa pun yang melebihi lima atau enam tingkat warisan dapat menyebabkan masalah casting dan tinju / unboxing yang tidak terduga, dan dalam kasus-kasus itu mungkin lebih bijaksana untuk menyusun objek Anda sebagai gantinya.

Jon Limjap
sumber
6

Ketika Anda memiliki hubungan is-a antara dua kelas (anjing contoh adalah anjing), Anda pergi untuk warisan.

Di sisi lain ketika Anda memiliki memiliki-a atau hubungan sifat antara dua kelas (siswa memiliki program) atau (studi guru kursus), Anda memilih komposisi.

Amir Aslam
sumber
Anda mengatakan warisan dan warisan. bukankah maksud Anda warisan dan komposisi?
trevorKirkby
Tidak, tidak. Anda juga bisa mendefinisikan antarmuka anjing dan membiarkan setiap anjing menerapkannya dan Anda akan berakhir dengan lebih banyak kode SOLID.
markus
5

Cara sederhana untuk memahami hal ini adalah pewarisan harus digunakan ketika Anda membutuhkan objek kelas Anda untuk memiliki antarmuka yang sama dengan kelas induknya, sehingga warisan tersebut dapat diperlakukan sebagai objek dari kelas induk (upcasting) . Selain itu, pemanggilan fungsi pada objek kelas turunan akan tetap sama di mana-mana dalam kode, tetapi metode spesifik untuk memanggil akan ditentukan saat runtime (yaitu implementasi level rendah berbeda, antarmuka level tinggi tetap sama).

Komposisi harus digunakan ketika Anda tidak perlu kelas baru untuk memiliki antarmuka yang sama, yaitu Anda ingin menyembunyikan aspek-aspek tertentu dari implementasi kelas yang tidak perlu diketahui oleh pengguna kelas itu. Jadi komposisi lebih di jalan mendukung enkapsulasi (yaitu menyembunyikan implementasi) sementara warisan dimaksudkan untuk mendukung abstraksi (yaitu memberikan representasi yang disederhanakan dari sesuatu, dalam hal ini antarmuka yang sama untuk berbagai jenis dengan internal yang berbeda).

YS
sumber
+1 untuk menyebutkan antarmuka. Saya sering menggunakan pendekatan ini untuk menyembunyikan kelas yang ada dan membuat unit baru saya dapat diuji dengan baik dengan mengejek objek yang digunakan untuk komposisi. Ini mengharuskan pemilik objek baru untuk memberikannya kelas calon induk sebagai gantinya.
Kell
4

Subtyping sesuai dan lebih kuat di mana invarian dapat dihitung , jika tidak gunakan komposisi fungsi untuk diperpanjang.

Shelby Moore III
sumber
4

Saya setuju dengan @Pavel, ketika dia berkata, ada tempat untuk komposisi dan ada tempat untuk warisan.

Saya pikir warisan harus digunakan jika jawaban Anda adalah afirmatif untuk semua pertanyaan ini.

  • Apakah kelas Anda merupakan bagian dari struktur yang mendapat manfaat dari polimorfisme? Misalnya, jika Anda memiliki kelas Shape, yang mendeklarasikan metode yang disebut draw (), maka kami jelas membutuhkan kelas Circle dan Square untuk menjadi subkelas Bentuk, sehingga kelas klien mereka akan bergantung pada Bentuk dan bukan pada subkelas tertentu.
  • Apakah kelas Anda perlu menggunakan kembali interaksi tingkat tinggi yang didefinisikan di kelas lain? The Metode Template pola desain tidak mungkin untuk melaksanakan tanpa warisan. Saya percaya semua kerangka kerja yang diperluas menggunakan pola ini.

Namun, jika niat Anda murni untuk menggunakan kembali kode, maka komposisi yang paling mungkin adalah pilihan desain yang lebih baik.

Parag
sumber
4

Warisan adalah machanism yang sangat kuat untuk penggunaan kembali kode. Namun perlu digunakan dengan benar. Saya akan mengatakan bahwa pewarisan digunakan dengan benar jika subclass juga merupakan subtipe dari kelas induk. Sebagaimana disebutkan di atas, Prinsip Pergantian Liskov adalah poin utama di sini.

Subkelas tidak sama dengan subtipe. Anda dapat membuat subclass yang bukan subtipe (dan ini adalah saat Anda harus menggunakan komposisi). Untuk memahami apa subtipe itu, mari kita mulai memberikan penjelasan tentang apa itu tipe.

Ketika kita mengatakan bahwa angka 5 adalah tipe integer, kita menyatakan bahwa 5 milik satu set nilai yang mungkin (sebagai contoh, lihat nilai yang mungkin untuk tipe primitif Java). Kami juga menyatakan bahwa ada serangkaian metode yang valid yang dapat saya lakukan pada nilai seperti penjumlahan dan pengurangan. Dan akhirnya kami menyatakan bahwa ada satu set properti yang selalu puas, misalnya, jika saya menambahkan nilai 3 dan 5, saya akan mendapatkan 8 sebagai hasilnya.

Untuk memberikan contoh lain, pikirkan tentang tipe data abstrak, Kumpulan bilangan bulat dan Daftar bilangan bulat, nilai yang dapat mereka pegang dibatasi untuk bilangan bulat. Keduanya mendukung serangkaian metode, seperti add (newValue) dan size (). Dan mereka berdua memiliki sifat yang berbeda (kelas invarian), Set tidak memungkinkan duplikat sementara Daftar memang memungkinkan duplikat (tentu saja ada properti lain yang mereka berdua memuaskan).

Subtipe juga merupakan tipe, yang memiliki hubungan dengan tipe lain, disebut tipe induk (atau supertipe). Subtipe harus memenuhi fitur (nilai, metode, dan properti) dari tipe induk. Relasi berarti bahwa dalam konteks apa pun di mana supertipe diharapkan, dapat diganti oleh subtipe, tanpa memengaruhi perilaku eksekusi. Mari kita lihat beberapa kode untuk memberikan contoh apa yang saya katakan. Misalkan saya menulis Daftar bilangan bulat (dalam semacam bahasa semu):

class List {
  data = new Array();

  Integer size() {
    return data.length;
  }

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

Kemudian, saya menulis Set bilangan bulat sebagai subclass dari Daftar bilangan bulat:

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

Kelas himpunan bilangan bulat kami adalah subkelas Daftar Bilangan Bulat, tetapi bukan subtipe, karena tidak memenuhi semua fitur dari kelas Daftar. Nilai-nilai, dan tanda tangan metode puas tetapi properti tidak. Perilaku metode add (Integer) telah jelas diubah, tidak mempertahankan properti tipe induk. Pikirkan dari sudut pandang klien kelas Anda. Mereka mungkin menerima Set bilangan bulat di mana Daftar bilangan bulat diharapkan. Klien mungkin ingin menambahkan nilai dan mendapatkan nilai itu ditambahkan ke Daftar bahkan jika nilai itu sudah ada di Daftar. Tapi dia tidak akan mendapatkan perilaku itu jika nilainya ada. Kejutan besar untuknya!

Ini adalah contoh klasik dari penggunaan warisan yang tidak tepat. Gunakan komposisi dalam hal ini.

(sebuah fragmen dari: gunakan warisan dengan benar ).

Enrique Molinari
sumber
3

Aturan praktis yang saya dengar adalah pewarisan harus digunakan ketika hubungan dan komposisi "is-a" ketika "has-a". Bahkan dengan itu saya merasa bahwa Anda harus selalu bersandar pada komposisi karena menghilangkan banyak kerumitan.


sumber
2

Komposisi v / s Warisan adalah subjek yang luas. Tidak ada jawaban nyata untuk apa yang lebih baik karena saya pikir itu semua tergantung pada desain sistem.

Umumnya jenis hubungan antar objek memberikan informasi yang lebih baik untuk memilih salah satunya.

Jika tipe relasi adalah relasi "IS-A" maka Inheritance adalah pendekatan yang lebih baik. jika tidak, tipe relasi adalah relasi "HAS-A" maka komposisi akan mendekati dengan lebih baik.

Ini sepenuhnya tergantung pada hubungan entitas.

Shami Qureshi
sumber
2

Meskipun Komposisi lebih disukai, saya ingin menyoroti kelebihan Warisan dan kontra Komposisi .

Kelebihan Warisan:

  1. Itu membangun hubungan " IS A" yang logis . Jika Mobil dan Truk adalah dua jenis Kendaraan (kelas dasar), kelas anak adalah kelas dasar.

    yaitu

    Mobil adalah Kendaraan

    Truk adalah Kendaraan

  2. Dengan pewarisan, Anda dapat menentukan / memodifikasi / memperluas kemampuan

    1. Kelas dasar tidak memberikan implementasi dan sub-kelas harus menimpa metode lengkap (abstrak) => Anda dapat menerapkan kontrak
    2. Kelas dasar menyediakan implementasi standar dan sub-kelas dapat mengubah perilaku => Anda dapat mendefinisikan ulang kontrak
    3. Sub-kelas menambahkan ekstensi ke implementasi kelas dasar dengan memanggil super.methodName () sebagai pernyataan pertama => Anda dapat memperpanjang kontrak
    4. Kelas dasar mendefinisikan struktur algoritma dan sub-kelas akan menimpa bagian dari algoritma => Anda dapat mengimplementasikan Template_method tanpa perubahan kerangka kelas dasar

Kekurangan Komposisi:

  1. Dalam warisan, subclass dapat secara langsung memanggil metode kelas dasar meskipun itu tidak menerapkan metode kelas dasar karena hubungan IS A. Jika Anda menggunakan komposisi, Anda harus menambahkan metode dalam kelas wadah untuk mengekspos API kelas yang terkandung

mis. Jika Mobil mengandung Kendaraan dan jika Anda harus mendapatkan harga Mobil , yang telah ditentukan dalam Kendaraan , kode Anda akan seperti ini

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

class Car{
     Vehicle vehicle;
     protected double getPrice(){
          return vehicle.getPrice();
     }
} 
Ravindra babu
sumber
Saya pikir itu tidak menjawab pertanyaan
almanegra
Anda dapat melihat kembali pertanyaan OP. Saya telah membahas: Imbalan apa yang ada untuk setiap pendekatan?
Ravindra babu
Seperti yang Anda sebutkan, Anda hanya berbicara tentang "pro Warisan dan kontra Komposisi", dan bukan pengorbanan untuk pendekatan SETIAP atau kasus di mana Anda harus menggunakan satu sama lain
almanegra
pro dan kontra memberikan trade-off karena pro warisan adalah kontra komposisi dan kontra komposisi pro kelebihan warisan.
Ravindra babu
1

Seperti yang dikatakan banyak orang, saya akan mulai dengan cek - apakah ada hubungan "is-a". Jika ada, saya biasanya memeriksa yang berikut:

Apakah kelas dasar dapat dipakai. Yaitu, apakah kelas dasar bisa non-abstrak. Kalau bisa non-abstrak saya biasanya lebih suka komposisi

Misalnya 1. Akuntan adalah Karyawan. Tapi saya tidak akan menggunakan warisan karena objek Karyawan dapat dipakai.

Misal 2. Buku adalah Item Jual. Sellingitem tidak bisa dipakai - itu adalah konsep abstrak. Karenanya saya akan menggunakan inheritacne. SellingItem adalah kelas dasar abstrak (atau antarmuka dalam C #)

Apa pendapat Anda tentang pendekatan ini?

Juga, saya mendukung jawaban @anon di Mengapa menggunakan warisan sama sekali?

Alasan utama untuk menggunakan warisan bukan sebagai bentuk komposisi - itu adalah agar Anda bisa mendapatkan perilaku polimorfik. Jika Anda tidak membutuhkan polimorfisme, Anda mungkin tidak boleh menggunakan warisan.

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

Masalah dengan pewarisan adalah bahwa ia dapat digunakan untuk dua tujuan ortogonal:

antarmuka (untuk polimorfisme)

implementasi (untuk penggunaan kembali kode)

REFERENSI

  1. Desain kelas mana yang lebih baik?
  2. Pewarisan vs Agregasi
LCJ
sumber
1
Saya tidak yakin mengapa 'kelas dasar abstrak?' angka dalam diskusi .. LSP: apakah semua fungsi yang beroperasi pada Anjing berfungsi jika benda Poodle dilewatkan? Jika ya, maka Poodle dapat menggantikan Dog dan karenanya dapat mewarisi dari Dog.
Gishu
@Gishu Terima kasih. Saya pasti akan melihat LSP. Tetapi sebelum itu, dapatkah Anda memberikan "contoh di mana pewarisan adalah tepat di mana kelas dasar tidak dapat abstrak". Apa yang saya pikirkan adalah, pewarisan hanya berlaku jika kelas dasar abstrak. Jika kelas dasar perlu dipakai secara terpisah, jangan pergi untuk warisan. Artinya, meskipun Akuntan adalah Karyawan, jangan gunakan warisan.
LCJ
1
telah membaca WCF belakangan ini. Contoh dalam kerangka .net adalah SynchronizationContext (basis + dapat dipakai) yang antrian bekerja ke thread ThreadPool. Derivasi termasuk WinFormsSyncContext (antrian ke UI Thread) dan DispatcherSyncContext (antrian ke WPF Dispatcher)
Gishu
@Gishu Terima kasih. Namun, akan lebih bermanfaat jika Anda dapat memberikan skenario berdasarkan domain Bank, domain SDM, domain Ritel atau domain populer lainnya.
LCJ
1
Maaf. Saya tidak terbiasa dengan domain-domain itu .. Contoh lain jika yang sebelumnya terlalu tumpul adalah kelas Kontrol di WinForms / WPF. Kontrol dasar / generik dapat dipakai. Derivasi termasuk Listboxes, Textboxes, dll. Sekarang saya pikir itu, pola Desain Dekorator adalah contoh yang baik IMHO dan berguna juga. Dekorator berasal dari objek non-abstrak yang ingin dibungkus / didekorasi.
Gishu
1

Saya melihat tidak ada yang menyebutkan masalah berlian , yang mungkin timbul dengan warisan.

Sekilas, jika kelas B dan C mewarisi A dan keduanya menimpa metode X, dan kelas keempat D, mewarisi dari B dan C, dan tidak menimpa X, implementasi XD mana yang seharusnya digunakan?

Wikipedia menawarkan tinjauan yang bagus tentang topik yang sedang dibahas dalam pertanyaan ini.

Veverke
sumber
1
D mewarisi B dan C bukan A. Jika demikian, maka akan menggunakan implementasi X yang ada di kelas A.
fabricio
1
@ fabricio: terima kasih, saya mengedit teks. Omong-omong, skenario seperti itu tidak dapat terjadi dalam bahasa yang tidak memungkinkan pewarisan banyak kelas, apakah saya benar?
Veverke
ya Anda benar .. dan saya tidak pernah bekerja dengan yang memungkinkan pewarisan berganda (seperti contoh dalam masalah intan) ..
fabricio