Переважна композиція стосується не лише поліморфізму. Хоча це є частиною цього, і ви маєте рацію, що (принаймні на номінальних мовах) те, що люди насправді мають на увазі, - це "віддати перевагу комбінації композиції та реалізації інтерфейсу". Але причини віддавати перевагу композиції (за багатьох обставин) є глибокими.
Поліморфізм - це одне, що поводиться різними способами. Отже, дженерики / шаблони є "поліморфною" особливістю, оскільки вони дозволяють одному коду змінювати свою поведінку залежно від типів. Насправді цей тип поліморфізму справді найкраще поводиться і його зазвичай називають параметричним поліморфізмом, оскільки варіація визначається параметром.
Багато мов надають форму поліморфізму, яка називається "перевантаження" або спеціальний поліморфізм, коли кілька процедур з однаковою назвою визначаються спеціальним чином, і коли одна обирається мовою (можливо, найбільш специфічною). Це найменш добре сприйнятий вид поліморфізму, оскільки ніщо не пов'язує поведінку двох процедур, крім розробленої конвенції.
Третім видом поліморфізму є підтип поліморфізм . Тут процедура, визначена для даного типу, може також працювати над цілим сімейством "підтипів" цього типу. Під час реалізації інтерфейсу або розширення класу ви, як правило, заявляєте про свій намір створити підтип. Справжні підтипи регулюються Принципом заміщення Ліскова, що говорить про те, що якщо ви можете довести щось про всі об'єкти в супертипі, ви можете довести це про всі екземпляри підтипу. Життя стає небезпечним, оскільки в таких мовах, як C ++ та Java, люди, як правило, мають невиконані, і часто недокументовані припущення про класи, які можуть бути, а можуть і не бути правдивими щодо їхніх підкласів. Тобто код пишеться так, ніби більше доказується, ніж є насправді, що створює безліч питань, коли ви недбало підтипуєте.
Спадщина насправді не залежить від поліморфізму. Враховуючи деяку річ "T", яка має посилання на себе, успадкування відбувається, коли ви створюєте нову річ "S" з "T", замінюючи посилання "T" на себе на посилання на "S". Це визначення навмисно розпливчасте, оскільки успадкування може траплятися у багатьох ситуаціях, але найпоширенішим є підкласифікація об'єкта, що призводить до заміни this
вказівника, викликаного віртуальними функціями, this
вказівником на підтип.
Спадкування небезпечно, як і всі дуже потужні речі, спадкування має силу спричинити хаос. Наприклад, припустимо, що ви перекриєте метод при успадкуванні від якогось класу: все добре і добре, поки якийсь інший метод цього класу не припустить, що метод, який ви успадковуєте, поводиться певним чином, адже саме так створив його автор оригінального класу . Ви можете частково захиститись від цього, оголосивши всі методи, названі іншим із ваших методів приватними або невіртуальними (остаточними), якщо вони не призначені для переопрацювання. Навіть це не завжди досить добре. Іноді ви можете побачити щось подібне (у псевдо Java, сподіваємось, читається для користувачів C ++ та 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
...
}
}
ви вважаєте, що це прекрасно, і у вас є власний спосіб робити "речі", але ви використовуєте спадщину, щоб придбати здатність робити "більше речі",
class MyUsefulThings extends WayOfDoingUsefulThings{
void doThings {
//my way
}
}
І все добре і добре. WayOfDoingUsefulThings
був розроблений таким чином, що заміна одного методу не змінює семантику жодного іншого ... крім чекання, ні, не було. Це просто виглядає так, як це було, але doThings
змінив стан змін, який мав значення. Тож, навіть якщо він не викликав жодних функцій, що можуть переосмислити,
void dealWithStuff(WayOfDoingUsefulThings bar){
bar.doThings()
use(bar.getStuff());
}
тепер робить щось інше, ніж очікувалося, коли ви передасте це MyUsefulThings
. Що гірше, ви можете навіть не знати, що WayOfDoingUsefulThings
давали ці обіцянки. Можливо, dealWithStuff
походить із тієї самої бібліотеки, що WayOfDoingUsefulThings
і getStuff()
навіть не експортується бібліотекою (придумайте класи друзів на C ++). Що ще гірше, ви перемогли статичну перевірку мови, не усвідомлюючи цього: просто dealWithStuff
взялися за те, WayOfDoingUsefulThings
щоб переконатися, що вона буде getStuff()
функціонувати певним чином.
Використання композиції
class MyUsefulThings implements UsefulThingsInterface{
private way = new WayOfDoingUsefulThings()
void doThings() {
//my way
}
void doMoreThings() {
this.way.doMoreThings();
}
}
повертає безпеку статичного типу. Загалом склад легший у використанні та безпечніший, ніж успадкування при здійсненні підтипів. Це також дозволяє переосмислити остаточні методи, а це означає, що ви повинні вільно оголосити все остаточним / невіртуальним, за винятком інтерфейсів переважної більшості часу.
У кращому світі мови автоматично вставляли блюдо з delegation
ключовим словом. Більшість ні, тому недоліком є більші заняття. Хоча ви можете отримати IDE для написання делегуючого екземпляра для вас.
Тепер життя стосується не лише поліморфізму. Вам не потрібно постійно підтипу. Мета поліморфізму, як правило, повторне використання коду, але це не єдиний спосіб досягти цієї мети. Часто в часі є сенс використовувати композицію, без підтипу поліморфізму, як спосіб управління функціональністю.
Крім того, поведінкове успадкування має свої переваги. Це одна з найпотужніших ідей у галузі інформатики. Справа в тому, що більшість випадків хороші програми OOP можна писати, використовуючи лише інтерфейс успадкування та композиції. Два принципи
- Заборонити спадщину чи дизайн для неї
- Віддайте перевагу композиції
є гарним посібником з вищезазначених причин і не несе великих істотних витрат.