Сравнение делегирования и композиции с наследованием

Другой важный аспект наследования состоит в том, что оно может быть вредным: наследование может разрушить инкапсуляцию и всегда усиливает связность. Наверняка все согласны с тем, что инкапсуляция — наиболее фундаментальная и важная объектно-ориентированная концепция.

Если это так, то зачем разрушать ее? Если в поддержку инкапсуляции базовый тип содержит защищенные поля, а вы нарушаете целостность оболочки инкапсуляции и открываете внутренности базового класса, то это не может быть хорошо. Ниже рассматриваются доступные альтернативы, которые позволяют разработать лучший дизайн.

Многие описывают наследование как повторное использование типа “белого ящика”. Лучшая форма повторного использования — это “черный ящик”, когда внутренности объекта не открываются внешнему миру. Достичь этого можно, применив отношение включения (containment). Да, это правильно.

Вместо наследования нового класса от другого, можно включить экземпляр этого другого класса в создаваемый, тем самым повторно используя класс включенного типа без нарушения инкапсуляции. Недостаток такого приема связан с тем, что в большинстве языков, включая С#, это потребует чуть больше работы по кодированию, хотя и не намного. Зато в результате получится более адаптируемый дизайн.

В качестве простого примера рассмотрим предметную область, где класс обслуживает некоторого рода специальные сетевые коммуникации.

Назовем этот класс NetworkCommunicator и предположим, что он выглядит следующим образом:

1
2
3
4
5
6
7
8
9
public class NetworkCommunicator {
public void SendData ( DataObject obj )
{
// Отправить данные по сети
}
public DataObject ReceiveData () {
// Принять данные по сети
}
}

Теперь предположим, что позже принято решение, что было бы неплохо иметь объект EncryptedNetworkCommunicator, в котором данные шифруются перед отправкой. Распространенный подход — унаследовать EncryptedNetworkCommunicator от NetworkCommunicator.

Тогда реализация может выглядеть так:

1
2
3
4
5
6
7
8
9
public class EncryptedNetworkCommunicator : NetworkCommunicator
public override void SendData( DataObject obj ) {
// Зашифровать данные
base.SendData( obj );
public override DataObject ReceiveData()
DataObject obj = base.ReceiveData() ;
// Расшифровать данные
return obj;
}

С этим вариантом связан большой недостаток. Прежде всего, хороший дизайн требует, что для модификации функциональности методов базового класса, их следует переопределять.

Для правильного их переопределения они сразу должны быть объявлены как virtual. Это потребует некоторого предвидения будущего при проектировании класса NetworkCommunicator и пометки методов модификатором virtual, но поскольку будущее предсказать не удалось, методы не были помечены как virtual и потому приведенный выше код EncryptedNetworkCommunicator не скомпилируется.

Да, в С# методы можно скрыть, воспользовавшись ключевым словом new при определении методов производного класса. Но если сделать это, то нарушится принцип, гласящий, что наследование моделирует отношение “является” (is-a).

Теперь рассмотрим ситуацию с включением:

1
2
3
4
5
6
7
8
9
10
public class EncryptedNetworkCommunicator
public EncryptedNetworkCommunicator()
contained = new NetworkCommunicator();
public void SendData( DataObject obj )
// Зашифровать данные contained.SendData ( obj );
public DataObject ReceiveData ()
DataObject obj = contained.ReceiveData ();
// Расшифровать данные
return obj;
private NetworkCommunicator contained;

Как видите, работы лишь не намного больше. Но плюс состоит в том, что Network Communicator можно повторно использовать, как если бы он был “черным ящиком”. Разработчики NetworkCommunicator могли сделать его sealed, а вы все равно смогли бы повторно его использовать. Будь он sealed, вы по определению не смогли бы наследовать от него.

Используя NetworkCommunicator через включение, можно даже предоставить общедоступный контракт на контейнере, который будет выглядеть несколько иначе, чем тот, что реализует сам NetworkCommunicator. Такой подход известен как шаблон проектирования Facade (Фасад).

Еще один недостаток применения наследования заключается в том, что оно не динамично. Наследование статично в силу того факта, что определяется на этапе компиляции. Такое положение дел может оказаться, как минимум, весьма ограничивающим. Применяя включение, вы устраняете это ограничение. Однако чтобы сделать это, также следует заручиться поддержкой со стороны хорошего друга — полиморфизма. При этом включающий тип может быть, скажем, интерфейсным типом.

Тогда включаемый объект просто должен поддерживать контракт этого интерфейса, чтобы повторно использоваться контейнером. Более того, этот объект можно изменить во время выполнения. Задумайтесь об этом на минутку. Предположим, что есть объект, представляющий контейнер сортируемых объектов. В этом контейнерном типе предусмотрен алгоритм сортировки по умолчанию.

Если вы реализуете этот алгоритм по умолчанию как включаемый тип, который можно подменить во время выполнения, тогда, если того требует предметная область, вы всегда сможете заменить его собственным специальным алгоритмом сортировки — до тех пор, пока объект нового алгоритма сортировки реализует необходимый интерфейс, ожидаемый контейнерным типом. Такая техника известна как шаблон проектирования Strategy (Стратегия).

В итоге дизайн с динамическими конструкциями является намного более гибким, чем при использовании конструкций статических. Сюда относится и предпочтение включения наследованию во многих случаях повторного использования. Такой тип повторного использования известен как делегирование, поскольку работа делегируется включаемому типу. Включение также предохраняет инкапсуляцию, в то время как наследование ее разрушает. Тем не менее, важно сделать одно предупреждение.

Как почти с любым техническим приемом, существует риск переусердствовать с включением. Для небольших служебных классов, возможно, и не стоит предпринимать слишком много усилий по внедрению включения.

И в некоторых случаях необходимо использовать наследование для реализации специализации. Но если говорить в целом, то дизайн, предпочитающий наследованию включение в качестве механизма повторного использования, обеспечивает намного большую гибкость и гораздо лучше выдерживает испытание временем.

Всегда учитывайте мощь наследования, включая возможные неприятные последствия от злоупотребления им.

Вы можете следить за любыми ответами на эту запись через RSS 2.0 ленту. Вы можете оставить ответ, или trackback с вашего собственного сайта.

Оставьте отзыв

XHTML: Вы можете использовать следующие теги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

 
Rambler's Top100