Mengapa lebih suka komposisi daripada warisan? Imbalan apa yang ada untuk setiap pendekatan? Kapan sebaiknya Anda memilih warisan daripada komposisi?
Mengapa lebih suka komposisi daripada warisan? Imbalan apa yang ada untuk setiap pendekatan? Kapan sebaiknya Anda memilih warisan daripada komposisi?
Jawaban:
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 .
Apakah TypeB hanya menginginkan sebagian / bagian dari perilaku yang diekspos oleh TypeA? Menunjukkan perlunya Komposisi.
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?'
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 .
Jika Anda memahami perbedaannya, lebih mudah untuk dijelaskan.
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.
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 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.
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.
Dengan semua manfaat tak terbantahkan yang diberikan oleh warisan, inilah beberapa kelemahannya.
Kerugian Warisan:
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.
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.
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.
InternalCombustionEngine
dengan kelas turunan GasolineEngine
. Yang terakhir menambahkan hal-hal seperti busi, yang tidak dimiliki oleh kelas dasar, tetapi menggunakan hal itu sebagai bilah InternalCombustionEngine
akan menyebabkan busi digunakan.
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
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.
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.
Client
. Kemudian, muncul konsep baru yang PreferredClient
muncul kemudian. Haruskah PreferredClient
mewarisi 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, Account
mungkin?
Client
kelas yang berbeda , mungkin hanya ada satu yang merangkum konsep Account
yang bisa menjadi StandardAccount
atau PreferredAccount
...
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
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.
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:
Warisan menyediakan penggunaan kembali kode langsung jika tidak diganti, sementara komposisi harus mengode ulang setiap API, bahkan jika itu hanya pekerjaan delegasi yang sederhana.
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.
Warisan memperlihatkan anggota yang dilindungi . Ini memecah enkapsulasi dari kelas induk, dan jika digunakan oleh subkelas, ketergantungan lain antara anak dan orang tuanya diperkenalkan.
Komposisi memiliki kecocokan inversi kontrol, dan ketergantungannya dapat disuntikkan secara dinamis, seperti yang ditunjukkan dalam pola dekorator dan pola proksi .
Komposisi memiliki manfaat pemrograman berorientasi kombinator , yaitu bekerja dengan cara seperti pola komposit .
Komposisi segera mengikuti pemrograman ke antarmuka .
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.
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.
TextFile
adalah a File
.
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
Lihat jawaban lain.
Sering dikatakan bahwa kelas Bar
dapat mewarisi kelas Foo
ketika kalimat berikut ini benar:
- sebuah bar adalah foo
Sayangnya, tes di atas saja tidak dapat diandalkan. Gunakan yang berikut ini sebagai gantinya:
- bar adalah foo, DAN
- bar dapat melakukan semua hal yang dapat dilakukan oleh foos.
Memastikan tes pertama bahwa semua getter dari Foo
masuk akal di Bar
(= sifat bersama), sedangkan tes kedua memastikan bahwa semua setter dari Foo
masuk 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.
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 Widget
dalam kerangka kerja GUI, atau kelas polimorfik Node
di 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.
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".
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.
computeArea(Circle *c) { return pi * width * height / 4.0; }
Sekarang generik.
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.
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.
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).
Inheritance is sometimes useful... That way you can have polymorphism
sebagai 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.
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 Engine
memperlihatkan mutator untuk mengubah
propertinya, atau saya mendesain ulang Aircraft
sebagai:
Class Aircraft {
var wings;
var engine;
}
Sekarang, saya dapat mengganti mesin saya dengan cepat juga.
Anda harus memiliki melihat The Liskov Pergantian Prinsip di Paman Bob SOLID prinsip-prinsip desain kelas. :)
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.
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:
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.
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:
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.
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.
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.
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).
Subtyping sesuai dan lebih kuat di mana invarian dapat dihitung , jika tidak gunakan komposisi fungsi untuk diperpanjang.
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.
Namun, jika niat Anda murni untuk menggunakan kembali kode, maka komposisi yang paling mungkin adalah pilihan desain yang lebih baik.
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 ).
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.
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.
Meskipun Komposisi lebih disukai, saya ingin menyoroti kelebihan Warisan dan kontra Komposisi .
Kelebihan Warisan:
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
Dengan pewarisan, Anda dapat menentukan / memodifikasi / memperluas kemampuan
Kekurangan Komposisi:
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();
}
}
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
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.