Mengapa saya lebih suka komposisi daripada warisan?

109

Saya selalu membaca bahwa komposisi lebih disukai daripada warisan. Sebuah posting blog tentang jenis yang berbeda , misalnya, advokat menggunakan komposisi lebih dari warisan, tetapi saya tidak bisa melihat bagaimana polimorfisme tercapai.

Tetapi saya merasa bahwa ketika orang mengatakan lebih suka komposisi, mereka benar-benar lebih suka kombinasi komposisi dan implementasi antarmuka. Bagaimana Anda akan mendapatkan polimorfisme tanpa warisan?

Ini adalah contoh konkret di mana saya menggunakan warisan. Bagaimana ini diubah untuk menggunakan komposisi, dan apa yang akan saya dapatkan?

Class Shape
{
    string name;
  public:
    void getName();
    virtual void draw()=0;
}

Class Circle: public Shape
{
    void draw(/*draw circle*/);
}
MustafaM
sumber
57
Tidak, ketika orang mengatakan lebih suka komposisi, mereka benar-benar lebih suka komposisi, tidak pernah menggunakan warisan . Seluruh pertanyaan Anda didasarkan pada premis yang salah. Gunakan warisan saat itu tepat.
Bill the Lizard
2
Anda mungkin juga ingin melihat pada programmers.stackexchange.com/questions/65179/…
vjones
2
Saya setuju dengan RUU tersebut, saya telah melihat menggunakan warisan praktik umum dalam pengembangan GUI.
Prashant Cholachagudda
2
Anda ingin menggunakan komposisi karena persegi hanya komposisi dua segitiga, sebenarnya saya pikir semua bentuk selain elips hanyalah komposisi segitiga. Polimorfisme adalah tentang kewajiban kontrak dan 100% dihapus dari warisan. Ketika segitiga mendapat banyak keanehan ditambahkan ke dalamnya karena seseorang ingin membuatnya mampu menghasilkan piramida, jika Anda mewarisi dari Segitiga Anda akan mendapatkan semua itu meskipun Anda tidak akan pernah menghasilkan piramida 3d dari segi enam Anda .
Jimmy Hoffa
2
@BilltheLizard Saya pikir banyak orang yang mengatakan itu benar-benar bermaksud untuk tidak pernah menggunakan warisan, tetapi mereka salah.
immibis

Jawaban:

51

Polimorfisme tidak selalu menyiratkan pewarisan. Seringkali warisan digunakan sebagai cara mudah untuk menerapkan perilaku Polimorfik, karena mudah untuk mengklasifikasikan objek berperilaku serupa sebagai memiliki struktur dan perilaku root yang sama sekali umum. Pikirkan semua contoh kode mobil dan anjing yang telah Anda lihat selama bertahun-tahun.

Tetapi bagaimana dengan benda-benda yang tidak sama. Membuat model mobil dan planet akan sangat berbeda, namun keduanya mungkin ingin menerapkan perilaku Move ().

Sebenarnya, pada dasarnya Anda menjawab pertanyaan Anda sendiri ketika mengatakannya "But I have a feeling that when people say prefer composition, they really mean prefer a combination of composition and interface implementation.". Perilaku umum dapat disediakan melalui antarmuka dan gabungan perilaku.

Mengenai mana yang lebih baik, jawabannya agak subyektif, dan benar-benar sampai pada bagaimana Anda ingin sistem Anda bekerja, apa yang masuk akal baik secara kontekstual maupun secara arsitektur, dan betapa mudahnya untuk menguji dan memelihara.

S.Robins
sumber
Dalam praktiknya, seberapa sering "Polimorfisme melalui Antarmuka" muncul, dan apakah itu dianggap normal (sebagai lawan eksploitasi ekspresi langue). Saya berani bertaruh bahwa polimorfisme melalui warisan adalah dengan desain yang cermat, bukan konsekuensi dari bahasa (C ++) yang ditemukan setelah spesifikasinya.
samis
1
Siapa yang akan memanggil move () di sebuah planet dan mobil dan menganggapnya sama?! Pertanyaannya di sini adalah dalam konteks apa mereka harus bisa bergerak? Jika mereka berdua adalah objek 2D dalam game 2D sederhana, mereka dapat mewarisi langkah tersebut, jika mereka adalah objek dalam simulasi data besar-besaran, mungkin tidak masuk akal untuk membiarkan mereka mewarisi dari basis yang sama dan sebagai konsekuensinya Anda harus menggunakan antarmuka
NikkyD
2
@SamusArin Ini menunjukkan up di mana-mana , dan dianggap normal dalam bahasa yang mendukung interface. Apa maksud Anda, "eksploitasi ekspresi bahasa"? Inilah antarmuka untuk apa .
Andres F.
@AndresF. Saya baru saja menemukan "Polimorfisme melalui Antarmuka" dalam sebuah contoh saat membaca "Analisis dan Desain Berorientasi Objek dengan Aplikasi" yang menunjukkan pola Observer. Saya kemudian menyadari jawaban saya.
samis
@AndresF. Saya kira visi saya agak dibutakan tentang ini karena bagaimana saya menggunakan polimorfisme dalam proyek (pertama) terakhir saya. Saya memiliki 5 tipe catatan yang semuanya berasal dari basis yang sama. Bagaimanapun terima kasih atas pencerahan.
samis
79

Memilih komposisi bukan hanya tentang polimorfisme. Meskipun itu adalah bagian dari itu, dan Anda benar bahwa (setidaknya dalam bahasa yang diketik secara nominal) apa yang sebenarnya dimaksud orang adalah "lebih suka kombinasi komposisi dan implementasi antarmuka." Namun, alasan untuk memilih komposisi (dalam banyak situasi) sangat besar.

Polimorfisme adalah tentang satu hal berperilaku dengan berbagai cara. Jadi, generik / templat adalah fitur "polimorfik" sejauh memungkinkan satu kode untuk memvariasikan perilakunya dengan jenis. Bahkan, jenis polimorfisme ini benar-benar berperilaku terbaik dan umumnya disebut sebagai polimorfisme parametrik karena variasi ditentukan oleh parameter.

Banyak bahasa menyediakan bentuk polimorfisme yang disebut "kelebihan beban" atau polimorfisme ad hoc di mana banyak prosedur dengan nama yang sama didefinisikan secara ad hoc, dan di mana seseorang dipilih oleh bahasa (mungkin yang paling spesifik). Ini adalah jenis polimorfisme yang paling tidak berperilaku baik, karena tidak ada yang menghubungkan perilaku kedua prosedur kecuali konvensi yang dikembangkan.

Jenis ketiga polimorfisme adalah subtipe polimorfisme . Di sini prosedur yang didefinisikan pada jenis yang diberikan, juga dapat bekerja pada seluruh keluarga "subtipe" dari jenis itu. Saat Anda mengimplementasikan antarmuka atau memperluas kelas, Anda biasanya menyatakan niat Anda untuk membuat subtipe. Subtipe sejati diatur oleh Prinsip Pergantian Liskov, yang mengatakan bahwa jika Anda dapat membuktikan sesuatu tentang semua objek dalam supertipe Anda dapat membuktikannya tentang semua contoh dalam subtipe. Kehidupan menjadi berbahaya, karena dalam bahasa seperti C ++ dan Java, orang umumnya memiliki asumsi yang diperkuat, dan seringkali tidak berdokumen tentang kelas yang mungkin atau mungkin tidak benar tentang subkelas mereka. Artinya, kode ditulis seolah-olah lebih banyak yang dapat dibuktikan daripada yang sebenarnya, yang menghasilkan sejumlah besar masalah ketika Anda subtipe dengan sembarangan.

Warisan sebenarnya tidak tergantung pada polimorfisme. Diberikan beberapa hal "T" yang memiliki referensi untuk dirinya sendiri, pewarisan terjadi ketika Anda membuat hal baru "S" dari "T" menggantikan referensi "T" untuk dirinya sendiri dengan referensi ke "S". Definisi itu sengaja tidak jelas, karena pewarisan dapat terjadi dalam banyak situasi, tetapi yang paling umum adalah subklasifikasi objek yang memiliki efek mengganti thispointer yang disebut fungsi virtual dengan thispointer ke subtipe.

Warisan adalah berbahaya seperti semua hal yang sangat kuat warisan memiliki kekuatan untuk menyebabkan kekacauan. Sebagai contoh, misalkan Anda mengganti metode ketika mewarisi dari beberapa kelas: semua baik dan bagus sampai beberapa metode lain dari kelas itu mengasumsikan metode yang Anda warisi untuk berperilaku dengan cara tertentu, setelah semua itu adalah bagaimana penulis kelas asli mendesainnya. . Anda dapat melindungi sebagian dari hal ini dengan mendeklarasikan semua metode yang dipanggil oleh orang lain dari metode Anda pribadi atau non-virtual (final), kecuali mereka dirancang untuk diganti. Meskipun ini tidak selalu cukup baik. Terkadang Anda mungkin melihat sesuatu seperti ini (di Java semu, semoga dapat dibaca oleh pengguna C ++ dan C #)

interface UsefulThingsInterface {
    void doThings();
    void doMoreThings();
}

...

class WayOfDoingUsefulThings implements UsefulThingsInterface{
     private foo stuff;
     public final int getStuff();
     void doThings(){
       //modifies stuff, such that ...
       ...
     }
     ...
     void doMoreThings(){
       //ignores stuff
       ...
     }
 }

Anda pikir ini indah, dan memiliki cara Anda sendiri untuk melakukan "hal-hal", tetapi Anda menggunakan warisan untuk memperoleh kemampuan untuk melakukan "lebih banyak Hal",

class MyUsefulThings extends WayOfDoingUsefulThings{
     void doThings {
        //my way
     }
}

Dan semuanya baik-baik saja. WayOfDoingUsefulThingsdirancang sedemikian rupa sehingga mengganti satu metode tidak mengubah semantik yang lain ... kecuali tunggu, tidak itu tidak. Sepertinya memang begitu, tetapi doThingsmengubah keadaan yang bisa berubah yang penting. Jadi, meskipun itu tidak memanggil fungsi-fungsi override,

 void dealWithStuff(WayOfDoingUsefulThings bar){
     bar.doThings()
     use(bar.getStuff());
 }

sekarang melakukan sesuatu yang berbeda dari yang diharapkan ketika Anda melewatinya a MyUsefulThings. Apa yang lebih buruk, Anda mungkin bahkan tidak tahu yang WayOfDoingUsefulThingsmembuat janji-janji itu. Mungkin dealWithStuffberasal dari perpustakaan yang sama WayOfDoingUsefulThingsdan getStuff()bahkan tidak diekspor oleh perpustakaan (pikirkan kelas teman di C ++). Lebih buruk lagi, Anda telah mengalahkan pemeriksaan statis bahasa tanpa menyadarinya: dealWithStuffmengambil WayOfDoingUsefulThingshanya untuk memastikan bahwa itu akan memiliki getStuff()fungsi yang berperilaku dengan cara tertentu.

Menggunakan komposisi

class MyUsefulThings implements UsefulThingsInterface{
     private way = new WayOfDoingUsefulThings()
     void doThings() {
        //my way
     }
     void doMoreThings() {
        this.way.doMoreThings();
     }
}

membawa kembali keamanan tipe statis. Secara umum komposisi lebih mudah digunakan dan lebih aman daripada warisan saat menerapkan subtyping. Ini juga memungkinkan Anda mengganti metode final yang berarti bahwa Anda harus merasa bebas untuk menyatakan semuanya final / non-virtual kecuali di antarmuka sebagian besar waktu.

Dalam dunia yang lebih baik, bahasa akan secara otomatis memasukkan boilerplate dengan delegationkata kunci. Kebanyakan tidak, jadi downside adalah kelas yang lebih besar. Meskipun, Anda bisa mendapatkan IDE Anda untuk menulis contoh delegasi untuk Anda.

Sekarang, hidup bukan hanya tentang polimorfisme. Anda tidak perlu subtipe sepanjang waktu. Tujuan dari polimorfisme umumnya menggunakan kembali kode tetapi itu bukan satu-satunya cara untuk mencapai tujuan itu. Sering kali, masuk akal untuk menggunakan komposisi, tanpa polimorfisme subtipe, sebagai cara mengelola fungsionalitas.

Juga, warisan perilaku memang memiliki kegunaannya. Ini adalah salah satu ide paling kuat dalam ilmu komputer. Hanya saja, sebagian besar waktu, aplikasi OOP yang baik dapat ditulis hanya menggunakan pewarisan dan komposisi antarmuka. Kedua prinsip itu

  1. Larangan warisan atau desain untuk itu
  2. Lebih suka komposisi

adalah panduan yang baik untuk alasan di atas, dan tidak menimbulkan biaya besar.

Philip JF
sumber
4
Jawaban bagus. Saya akan meringkasnya bahwa mencoba mencapai penggunaan kembali kode dengan pewarisan merupakan jalan yang salah. Pewarisan merupakan kendala yang sangat kuat (jika menambahkan "kekuatan" adalah analogi yang salah!) Dan menciptakan ketergantungan yang kuat antara kelas-kelas yang diwariskan. Terlalu banyak dependensi = kode buruk :) Jadi warisan (alias "berperilaku") biasanya bersinar untuk antarmuka terpadu (= menyembunyikan kompleksitas kelas yang diwariskan), untuk hal lain pikirkan dua kali atau gunakan komposisi ...
MaR
2
Ini jawaban yang bagus. +1. Paling tidak di mata saya, kelihatannya komplikasi ekstra itu mungkin merupakan biaya yang besar, dan ini membuat saya pribadi agak ragu-ragu untuk membuat banyak hal karena lebih suka komposisi. Terutama ketika Anda mencoba untuk membuat banyak unit kode yang ramah pengujian melalui antarmuka, komposisi, dan DI (saya tahu ini menambahkan hal-hal lain di dalamnya), tampaknya sangat mudah untuk membuat seseorang menelusuri beberapa file berbeda untuk mencari sangat beberapa detail. Mengapa ini tidak disebutkan lebih sering, bahkan ketika hanya berurusan dengan komposisi atas prinsip desain warisan?
Panzercrisis
27

Alasan orang mengatakan ini adalah karena para pemrogram OOP pemula, yang baru keluar dari kuliah polimorfisme-pewarisan mereka, cenderung menulis kelas-kelas besar dengan banyak metode polimorfik, dan kemudian di suatu tempat, mereka berakhir dengan kekacauan yang tak dapat diatasi.

Contoh khas datang dari dunia pengembangan game. Misalkan Anda memiliki kelas dasar untuk semua entitas gim Anda - pesawat ruang angkasa, monster, peluru, dll .; setiap jenis entitas memiliki subkelasnya sendiri. Pendekatan warisan akan menggunakan metode polimorfik beberapa, misalnya update_controls(), update_physics(), draw(), dll, dan menerapkannya untuk setiap subclass. Namun, ini berarti Anda menggabungkan fungsi yang tidak terkait: itu tidak relevan seperti apa objek terlihat untuk memindahkannya, dan Anda tidak perlu tahu apa-apa tentang AI-nya untuk menggambarnya. Pendekatan komposisi sebaliknya mendefinisikan beberapa kelas dasar (atau antarmuka), misalnya EntityBrain(subkelas menerapkan AI atau input pemain), EntityPhysics(subkelas menerapkan fisika gerakan) dan EntityPainter(subkelas menangani menggambar), dan kelas non-polimorfikEntityyang menampung satu contoh dari masing-masing. Dengan cara ini, Anda dapat menggabungkan tampilan apa pun dengan model fisika apa pun dan AI apa pun, dan karena Anda memisahkannya, kode Anda juga akan jauh lebih bersih. Juga, masalah seperti "Saya ingin monster yang terlihat seperti monster balon di level 1, tetapi berperilaku seperti badut gila di level 15" pergi: Anda cukup mengambil komponen yang sesuai dan menempelkannya.

Perhatikan bahwa pendekatan komposisi masih menggunakan pewarisan dalam setiap komponen; meskipun idealnya Anda hanya akan menggunakan antarmuka dan implementasinya di sini.

"Pemisahan masalah" adalah frase kunci di sini: mewakili fisika, menerapkan AI, dan menggambar entitas, adalah tiga masalah, menggabungkan mereka ke dalam entitas adalah yang keempat. Dengan pendekatan komposisi, setiap masalah dimodelkan sebagai satu kelas.

tammmer
sumber
1
Ini semua baik, dan dianjurkan dalam artikel yang ditautkan dalam pertanyaan awal. Saya akan senang (setiap kali Anda mendapatkan waktu) jika Anda dapat memberikan contoh sederhana yang melibatkan semua entitas ini, sambil juga mempertahankan polimorfisme (dalam C ++). Atau arahkan saya ke sumber. Terima kasih.
MustafaM
Saya tahu jawaban Anda sangat tua tetapi saya melakukan hal yang persis sama seperti yang Anda sebutkan dalam permainan saya dan sekarang pergi untuk komposisi atas pendekatan pewarisan.
Menyelinap
"Aku ingin monster yang terlihat seperti ..." bagaimana ini masuk akal? Antarmuka tidak memberikan implementasi apa pun, Anda harus menyalin tampilan dan kode perilaku dengan satu cara atau yang lain
NikkyD
1
@NikkyD Anda mengambil MonsterVisuals balloonVisualsdan MonsterBehaviour crazyClownBehaviourdan instantiate a Monster(balloonVisuals, crazyClownBehaviour), di samping Monster(balloonVisuals, balloonBehaviour)dan Monster(crazyClownVisuals, crazyClownBehaviour)instance yang dipakai di level 1 dan level 15
Caleth
13

Contoh yang Anda berikan adalah contoh warisan yang merupakan pilihan alami. Saya tidak berpikir siapa pun akan mengklaim bahwa komposisi selalu merupakan pilihan yang lebih baik daripada warisan - itu hanya panduan yang berarti bahwa seringkali lebih baik untuk mengumpulkan beberapa objek yang relatif sederhana daripada membuat banyak objek yang sangat khusus.

Delegasi adalah salah satu contoh cara menggunakan komposisi alih-alih warisan. Delegasi memungkinkan Anda memodifikasi perilaku suatu kelas tanpa subklasifikasi. Pertimbangkan kelas yang menyediakan koneksi jaringan, NetStream. Mungkin wajar untuk subkelas NetStream untuk mengimplementasikan protokol jaringan umum, jadi Anda mungkin datang dengan FTPStream dan HTTPStream. Tetapi alih-alih membuat subkelas HTTPStream yang sangat spesifik untuk satu tujuan, katakanlah, UpdateMyWebServiceHTTPStream, sering kali lebih baik menggunakan contoh HTTPStream biasa dan delegasi yang tahu apa yang harus dilakukan dengan data yang diterimanya dari objek tersebut. Salah satu alasan mengapa ini lebih baik adalah bahwa hal itu menghindari proliferasi kelas yang harus dipertahankan tetapi Anda tidak akan pernah bisa menggunakannya kembali.

Caleb
sumber
11

Anda akan melihat siklus ini banyak dalam wacana pengembangan perangkat lunak:

  1. Beberapa fitur atau pola (sebut saja "Pola X") ditemukan berguna untuk tujuan tertentu. Posting blog ditulis memuji kebaikan tentang Pola X.

  2. Hype membuat beberapa orang berpikir Anda harus menggunakan Pola X kapan pun memungkinkan .

  3. Orang lain kesal melihat Pola X digunakan dalam konteks di mana tidak sesuai, dan mereka menulis posting blog yang menyatakan bahwa Anda tidak harus selalu menggunakan Pola X dan itu berbahaya dalam beberapa konteks.

  4. Serangan balik menyebabkan beberapa orang percaya Pola X selalu berbahaya dan tidak boleh digunakan.

Anda akan melihat siklus hype / serangan balik ini terjadi dengan hampir semua fitur dari GOTOpola ke SQL ke NoSQL dan, ya, warisan. Penangkal racunnya adalah selalu mempertimbangkan konteksnya .

Setelah Circleturun dari Shapeadalah persis bagaimana warisan seharusnya digunakan dalam bahasa OO mendukung warisan.

Aturan praktis "lebih suka komposisi daripada warisan" benar-benar menyesatkan tanpa konteks. Anda harus lebih memilih warisan ketika warisan lebih tepat, tetapi lebih memilih komposisi ketika komposisi lebih tepat. Kalimat tersebut ditujukan kepada orang-orang pada tahap 2 dalam siklus hype, yang berpikir warisan harus digunakan di mana-mana. Tetapi siklus itu telah berlanjut, dan hari ini tampaknya membuat sebagian orang berpikir bahwa warisan itu buruk.

Anggap saja seperti palu versus obeng. Haruskah Anda lebih memilih obeng daripada palu? Pertanyaan itu tidak masuk akal. Anda harus menggunakan alat yang sesuai untuk pekerjaan itu, dan itu semua tergantung pada tugas apa yang perlu Anda lakukan.

JacquesB
sumber