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 dengan this
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 #)
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. WayOfDoingUsefulThings
dirancang sedemikian rupa sehingga mengganti satu metode tidak mengubah semantik yang lain ... kecuali tunggu, tidak itu tidak. Sepertinya memang begitu, tetapi doThings
mengubah 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 WayOfDoingUsefulThings
membuat janji-janji itu. Mungkin dealWithStuff
berasal dari perpustakaan yang sama WayOfDoingUsefulThings
dan getStuff()
bahkan tidak diekspor oleh perpustakaan (pikirkan kelas teman di C ++). Lebih buruk lagi, Anda telah mengalahkan pemeriksaan statis bahasa tanpa menyadarinya: dealWithStuff
mengambil WayOfDoingUsefulThings
hanya 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 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
- Larangan warisan atau desain untuk itu
- Lebih suka komposisi
adalah panduan yang baik untuk alasan di atas, dan tidak menimbulkan biaya besar.