Предпочтительная композиция - это не только полиморфизм. Хотя это является частью этого, и вы правы в том, что (по крайней мере, в номинально типизированных языках) люди на самом деле имеют в виду «предпочесть сочетание композиции и реализации интерфейса». Но причины для предпочтения композиции (во многих обстоятельствах) глубокие.
Полиморфизм - это то, что ведет себя по-разному. Таким образом, дженерики / шаблоны являются «полиморфной» функцией, поскольку они позволяют одному куску кода изменять свое поведение в зависимости от типов. Фактически, этот тип полиморфизма действительно лучше всего ведет себя и обычно упоминается как параметрический полиморфизм, потому что изменение определяется параметром.
Многие языки предоставляют форму полиморфизма, называемую «перегрузкой» или специальным полиморфизмом, когда несколько процедур с одним и тем же именем определяются специальным образом, и когда одна из них выбирается языком (возможно, наиболее конкретным). Это наименее хорошо управляемый вид полиморфизма, поскольку ничто не связывает поведение двух процедур, кроме разработанного соглашения.
Третий тип полиморфизма - это полиморфизм подтипа . Здесь процедура, определенная для данного типа, также может работать с целым семейством «подтипов» этого типа. Когда вы реализуете интерфейс или расширяете класс, вы, как правило, заявляете о своем намерении создать подтип. Истинные подтипы подчиняются принципу замещения ЛисковаЭто говорит о том, что если вы можете доказать что-то обо всех объектах в супертипе, вы можете доказать это обо всех экземплярах в подтипе. Однако жизнь становится опасной, поскольку в таких языках, как 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();
}
}
возвращает безопасность статического типа. В общем случае композиция проще в использовании и безопаснее наследования при реализации подтипов. Это также позволяет вам переопределить final методы, что означает, что вы можете свободно объявлять все final / non-virtual за исключением интерфейсов, которые подавляющее большинство времени.
В лучшем мире языки будут автоматически вставлять шаблон с delegation
ключевым словом. Большинство этого не делает, поэтому недостатком являются большие классы. Хотя вы можете заставить свою IDE написать делегирующий экземпляр для вас.
Теперь жизнь не только о полиморфизме. Вам не нужно постоянно вводить подтипы. Целью полиморфизма обычно является повторное использование кода, но это не единственный способ достижения этой цели. Часто имеет смысл использовать композицию без полиморфизма подтипа в качестве способа управления функциональностью.
Кроме того, поведенческое наследование имеет свое применение. Это одна из самых сильных идей в информатике. Просто в большинстве случаев хорошие ООП-приложения могут быть написаны с использованием только наследования интерфейса и композиций. Два принципа
- Запретить наследство или дизайн для него
- Предпочитаю состав
являются хорошим руководством по вышеуказанным причинам и не несут существенных затрат.