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*/);
}
interfaces
inheritance
composition
MustafaM
sumber
sumber
Jawaban:
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.
sumber
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
this
pointer yang disebut fungsi virtual denganthis
pointer 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 #)
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",
Dan semuanya baik-baik saja.
WayOfDoingUsefulThings
dirancang sedemikian rupa sehingga mengganti satu metode tidak mengubah semantik yang lain ... kecuali tunggu, tidak itu tidak. Sepertinya memang begitu, tetapidoThings
mengubah keadaan yang bisa berubah yang penting. Jadi, meskipun itu tidak memanggil fungsi-fungsi override,sekarang melakukan sesuatu yang berbeda dari yang diharapkan ketika Anda melewatinya a
MyUsefulThings
. Apa yang lebih buruk, Anda mungkin bahkan tidak tahu yangWayOfDoingUsefulThings
membuat janji-janji itu. MungkindealWithStuff
berasal dari perpustakaan yang samaWayOfDoingUsefulThings
dangetStuff()
bahkan tidak diekspor oleh perpustakaan (pikirkan kelas teman di C ++). Lebih buruk lagi, Anda telah mengalahkan pemeriksaan statis bahasa tanpa menyadarinya:dealWithStuff
mengambilWayOfDoingUsefulThings
hanya untuk memastikan bahwa itu akan memilikigetStuff()
fungsi yang berperilaku dengan cara tertentu.Menggunakan komposisi
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
delegation
kata 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
adalah panduan yang baik untuk alasan di atas, dan tidak menimbulkan biaya besar.
sumber
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), misalnyaEntityBrain
(subkelas menerapkan AI atau input pemain),EntityPhysics
(subkelas menerapkan fisika gerakan) danEntityPainter
(subkelas menangani menggambar), dan kelas non-polimorfikEntity
yang 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.
sumber
MonsterVisuals balloonVisuals
danMonsterBehaviour crazyClownBehaviour
dan instantiate aMonster(balloonVisuals, crazyClownBehaviour)
, di sampingMonster(balloonVisuals, balloonBehaviour)
danMonster(crazyClownVisuals, crazyClownBehaviour)
instance yang dipakai di level 1 dan level 15Contoh 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.
sumber
Anda akan melihat siklus ini banyak dalam wacana pengembangan perangkat lunak:
Beberapa fitur atau pola (sebut saja "Pola X") ditemukan berguna untuk tujuan tertentu. Posting blog ditulis memuji kebaikan tentang Pola X.
Hype membuat beberapa orang berpikir Anda harus menggunakan Pola X kapan pun memungkinkan .
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.
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
GOTO
pola ke SQL ke NoSQL dan, ya, warisan. Penangkal racunnya adalah selalu mempertimbangkan konteksnya .Setelah
Circle
turun dariShape
adalah 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.
sumber