Итак, какие же преимущества есть у композиции перед наследованием?
1. Нет конфликта имён, возможного при наследовании.
2. Возможность смены агрегируемого объекта в runtime.
3. Полная замена агрегируемого объекта в классах, производных от класса, включающего агрегируемый объект.
В последних двух случаях очень желательно, чтобы сменяемые агрегируемые объекты имели общий интерфейс. А в третьем – чтобы метод, возвращающий такой объект, был виртуальным.
Если рассматривать, например, C#, не поддерживающий множественное наследование, но позволяющий наследовать от множества интерфейсов и создавать методы расширения для этих интерфейсов, то можно выделить ещё два плюса (речь в данном случае может идти только о поведениях (алгоритмах) в рамках паттерна «Стратегия»):
4. Агрегируемое поведение (алгоритм) может включать в себя другие объекты. Что в частности позволяет переиспользовать посредством агрегации другое поведение.
5. При агрегации есть возможность скрыть определённую часть реализации, а также исходные параметры, необходимые поведению, посредством передачи их через конструктор (при наследовании поведению придётся запрашивать их через методы/свойства собственного интерфейса).
Но как же минусы? Неужели их нет?
1. Итак, если нам необходима возможность смены поведения извне, то композиция, по сравнению с наследованием, имеет принципиально другой тип отношений между объектом поведения и объектом, его использующим. Если при наследовании от абстрактного поведения мы имеем отношение 1:1, то при агрегации и возможности установки поведения извне мы получаем отношение 1:many. Т.е. один и тот же объект поведения может использоваться несколькими объектами-владельцами. Это порождает проблемы с общим для нескольких таких объектов-владельцев состоянием поведения.
Разрешить эту ситуацию можно, запретив установку поведения извне или доверив его, например, generic-методу:
void SetBehavior<TBehavior>()
запретив тем самым создание поведения кем-либо, кроме объекта-владельца. Однако мы не можем запретить использовать поведение «где-то ещё». В языках без сборщика мусора (GC) это порождает понятные проблемы. Конечно, в таких языках можно неправомерно обратиться по ссылке и на сам объект-владелец, но, раздавая отделённые объекты поведения направо и налево, мы получаем в разы больше шансов получить exception.
2. Агрегация (и это, пожалуй, главный нюанс) отличается от наследования в первую очередь тем, что агрегируемый объект не является объектом-владельцем и не содержит информации о нём. Нередки ситуации, когда коду, взаимодействующему с поведением, необходим и сам объект-владелец (например, для получения информации о том, какими ещё поведениями он обладает).
В таком случае, нам придётся или передавать в такой код нетипизированный объект (как object или void*), или создавать дополнительный интерфейс для объекта-владельца (некий IBehaviorOwner), или хранить в поведении циклическую ссылку на объект-владелец. Понятно, что каждый из этих вариантов имеет свои минусы и ещё больше усложняет код. Более того, различные типы поведений могут зависеть друг от друга (и в это вполне допустимо, особенно если они находятся в некоем закрытом самодостаточном модуле).
3. Ну и последний минус — это конечно же производительность. Если объектов-владельцев достаточно много, то создание и уничтожение вместо одного объекта двух или более может не остаться незамеченным.
Получается, что утверждение «композиция всегда лучше наследования» в ряде случаев спорно и не должно являться догмой. Особенно это касается языков, позволяющих множественное наследование и не имеющих GC. Если в какой-либо ситуации перечисленные выше плюсы не важны, и заранее известно, что при работе с определёнными типами у вас не будет возможности их использовать, стоит всё-таки рассмотреть вариант наследования.
ссылка на оригинал статьи http://habrahabr.ru/post/177447/
Добавить комментарий