5

5


Используя наследование от существующего типа, вы создаете новый тип. Этот новый тип не только содержит все члены существующего типа (хотя члены, помеченные как private, скрыты и недоступны), но и, что еще важнее, повторя¬ет интерфейс базового класса. Значит, все сообщения, которые вы могли по¬сылать базовому классу, вы также вправе посылать и производному классу. А так как мы различаем типы классов по совокупности сообщений, которые мо¬жем им посылать, это означает, что производный класс является частным слу¬чаем базового класса. В предыдущем примере «окружность есть фигура». Экви¬валентность типов, достигаемая при наследовании, является одним из осно¬вополагающих условий понимания смысла объектно-ориентированного про¬граммирования.

Так как и базовый, и производный классы имеют одинаковый основной ин¬терфейс, должна существовать и реализация для этого интерфейса. Другими словами, где-то должен быть код, выполняемый при получении объектом опре¬деленного сообщения. Если вы просто унаследовали класс и больше не пред¬принимали никаких действий, методы из интерфейса базового класса перейдут в производный класс без изменений. Это значит, что объекты производного класса не только однотипны, но и обладают одинаковым поведением, а при этом само наследование теряет смысл.

Существует два способа изменения нового класса по сравнению с базовым классом. Первый достаточно очевиден: в производный класс включаются новые методы. Они уже не являются частью интерфейса базового класса. Видимо, ба¬зовый класс не делал всего, что требовалось в данной задаче, и вы дополнили его новыми методам. Впрочем, такой простой и примитивный подход к насле¬дованию иногда оказывается идеальным решением проблемы. Однако надо внимательно рассмотреть, действительно ли базовый класс нуждается в этих дополнительных методах. Процесс выявления закономерностей и пересмотра архитектуры является повседневным делом в объектно-ориентированном про¬граммировании.

Хотя наследование иногда наводит на мысль, что интерфейс будет дополнен новыми методами (особенно в Java, где наследование обозначается ключевым словом extends, то есть «расширять»), это совсем не обязательно. Второй, более важный способ модификации классов заключается в изменении поведения уже существующих методов базового класса. Это называется переопределением (или замещением) метода.

Для замещения метода нужно просто создать новое определение этого мето¬да в производном классе. Вы как бы говорите: «Я использую тот же метод ин¬терфейса, но хочу, чтобы он выполнял другие действия для моего нового типа».

Отношение «является» в сравнении с «похоже»

При использовании наследования встает очевидный вопрос: следует ли при на¬следовании переопределять только методы базового класса (и не добавлять но¬вые методы, не существующие в базовом классе)? Это означало бы, что произ¬водный тип будет точно такого же типа, как и базовый класс, так как они имеют одинаковый интерфейс. В результате вы можете свободно заменять объ¬екты базового класса объектами производных классов. Можно говорить о пол¬ной замене, и это часто называется принципом замены. В определенном смысле это способ наследования идеален. Подобный способ взаимосвязи базового и производного классов часто называют связью «является тем-то», поскольку можно сказать «круг есть фигура». Чтобы определить, насколько уместным бу¬дет наследование, достаточно проверить, существует ли отношение «является» между классами и насколько оно оправданно.

В иных случаях интерфейс производного класса дополняется новыми эле¬ментами, что приводит к его расширению. Новый тип все еще может приме¬няться вместо базового, но теперь эта замена не идеальна, потому что она не по¬зволяет использовать новые методы из базового типа. Подобная связь описывается выражением «похоже на» (это мой термин); новый тип содержит интерфейс старого типа, но также включает в себя и новые методы, и нельзя сказать, что эти типы абсолютно одинаковы. Для примера возьмем кондиционер.

Предположим, что ваш дом снабжен всем необходимым оборудованием для контроля процесса охлаждения. Представим теперь, что кондиционер сломался и вы заменили его обогревателем, способным как нагревать, так и охлаждать. Обогреватель «похож на» кондиционер, но он способен и на большее. Так как система управления вашего дома способна контролировать только охлаждение, она ограничена в коммуникациях с охлаждающей частью нового объекта. Ин¬терфейс нового объекта был расширен, а существующая система ничего не при¬знает, кроме оригинального интерфейса.

Конечно, при виде этой иерархии становится ясно, что базовый класс «охла¬ждающая система» недостаточно гибок; его следует переименовать в «систему контроля температуры» так, чтобы он включал и нагрев, — и после этого зара¬ботает принцип замены. Тем не менее эта диаграмма представляет пример того, что может произойти в реальности.

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

Взаимозаменяемые объекты и полиморфизм

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

Подобный код не зависит от добавления новых типов, а добавление новых типов является наиболее распространенным способом расширения объектно- ориентированных программ для обработки новых ситуаций. Например, вы мо¬жете создать новый подкласс фигуры (пятиугольник), и это не приведет к из¬менению методов, работающих только с обобщенными фигурами. Возможность простого расширения программы введением новых производных типов очень важна, потому что она заметно улучшает архитектуру программы, в то же время снижая стоимость поддержки программного обеспечения.

Report Page