Инкапсуляция, полиморфизм
и наследование не решают одну из наиболее
важных проблем: проблему объединения функциональных
свойств нескольких классов в одном классе.
Ситуация, когда необходимо объединить
несколько классов в единое целое – новый
класс, встречается очень часто. Сторонники
одиночного наследования могут сколь угодно
долго высмеивать нелепость множественного
наследования, но ирония – это не решение
проблемы. Однако и множественное наследование
не столько решает данную задачу, сколько
запутывает решение. Допустим, что нам
надо получить класс «автомобиль». Предположим,
что у нас под рукой есть все необходимые
классы: «кузов», «двигатель», «трансмиссия»
и «шасси». Можно ли путём наследования
от этих классов получить новый класс:
«автомобиль»? Конечно, нет. Предком автомобиля
должно быть «транспортное средство» (конный
экипаж, к примеру), но никак ни кузов,
ни двигатель, ни трансмиссия и ни шасси.
Перечисленные классы являются составными
частями автомобиля, но не его суперклассами.
Правильнее будет утверждение, что автомобиль
включает в себя кузов, двигатель, трансмиссию
и шасси, но он не может являться их наследником.
Термин «включает» служит ключом к пониманию
сути. Иными словами, автомобиль является
контейнером для перечисленных классов.
Из этого примера можно сделать и другой
важный вывод, что контейнер – это не механическая
смесь классов, а принципиально иная сущность!
От формальных параметров
до ролей объектов
Ранее уже говорилось о том,
что механизм формальных параметров имел
большое значение в структурном программировании,
поскольку делал подпрограммы независимыми
от глобальных переменных. Не менее важным
является и механизм декларации интерфейса,
который берёт своё начало в модульном
программировании. Суть его состоит в том,
что декларация интерфейса оторвана от
реализации. Как следствие, можно менять
реализации при условии сохранения интерфейса
как у одного и того же модуля или класса,
так и у различных модулей или классов,
например, в случае полиморфных свойств
(виртуальных методов). Таким образом,
виртуализация, как с помощью формальных
параметров, так и с помощью интерфейса,
позволяет получать очень гибкие конструкции,
которые можно легко настраивать под конкретные
задачи и модифицировать в случае необходимости.
Но исчерпываются ли на этом возможности
виртуализации?
Рассматривая возможность
агрегации нескольких объектов в новый
класс – контейнер, необходимо определить
основы агрегации. Каждый объект обладает
некоторым декларированным интерфейсом,
с помощью которого с ним могут взаимодействовать
другие объекты. Контейнер, имея в своём
составе некоторое множество объектов,
может предоставлять вовне весь интерфейс
вложенных объектов или только его часть.
Совокупность интерфейсов, которые использует
контейнер у вложенного объекта, назовём
ролью вложенного объекта. Несколько вложенных
объектов могут играть одинаковые или различные
роли в одном и том же контейнере.
Понятие роли объекта не менее важно для
агрегации, чем формальные параметры подпрограмм
в структурном программировании. Это положение
исходит из того, что понятие роли вводит
виртуальность объектов в контейнере. Действительно,
можно с полным правом сказать, что контейнер
агрегирует объекты, способные выступать
в таких-то ролях, то есть, объектов, имеющих
некоторую определённую совокупность интерфейсов.
Продолжая рассматривать пример с автомобилем,
можно отметить, что один и в один и тот
же автомобиль можно установить несколько
различных моделей (классов) двигателей.
При таких заменах автомобиль не перестаёт
быть автомобилем, а двигатели – двигателями,
даже в случае, если один двигатель карбюраторный,
а другой – дизельный. Аналогично в компьютере
можно заменить, например, один «жёсткий»
диск на другой, или в операционной системе
сменить старые драйвера на более новые.
К сожалению, понятие роли,
как некоторой совокупности свойств (интерфейсов)
объекта не только недооценивается, но
и, что ещё хуже, интерпретируется неверно.
Так, например, Тимоти Бадд в своей книге
«Объектно-ориентированное программирование
в действии» приводит иерархию объектов,
где люди различных профессий являются
подклассами класса «human» (люди) [ТБ
стр. 32]. Однако такой взгляд не выдерживает
никакой критики. Правильнее говорить о
том, что люди могут играть различные роли
(иметь различные профессии) благодаря
наличию тех или иных свойств. В подходе,
предложенном Т. Баддом, неизбежно придётся
постоянно перекраивать иерархию, поскольку
люди могут не только иметь несколько профессий
и качеств, но и приобретать их в течение
жизни. Гончар может быть прекрасным садовником,
неплохим водителем, мужем, любящим отцом,
заботливым дедом и т.п. И это множество
профессий и качеств он может разделять
со всем остальным человечеством. Можно,
конечно, снова вернуться к множественному
наследованию, но во что превратится стройная
и логичная иерархия? К этому стоит ещё
добавить, что часто можно столкнуться
с ситуацией, когда в одной и той же роли
могут выступать принципиально разные сущности.
Например, регулировать движение транспорта
может светофор и сотрудник службы безопасности
движения. Означает ли это, что они являются
«родственниками»? Гораздо логичнее предположить,
что они оба имеют необходимый для регулирования
движения транспорта набор свойств (качеств,
интерфейсов).
Понятие роли динамично,
поскольку оно позволяет непосредственно
при работе системы объявить ролью некоторый
набор интерфейсов и потребовать от системы
список классов, объекты которых способны
выступать в данной роли. При этом каждый
класс может выступать в произвольном количестве
ролей. Для примера можно рассмотреть класс,
который имеет интерфейсы A, B, C, D и
F. В роли «альфа» объединим интерфейсы
A и B, в роли «бета» - интерфейсы B, C
и F, в роли «гамма» - A, C и D и т.д.
Понятно, что подклассы всегда могут выступать
в тех ролях, в которых выступают их суперклассы.
Однако, благодаря тому, что полиморфизм
является понятием независимым от наследования
(обратное неверно), то справедливо будет
и утверждение, что классы, не находящиеся
в наследственной связи, тоже способны
играть одну и ту же роль.
Простота и удобство механизма
виртуальности объектов в контейнере, предоставляемые
агрегацией, позволяют существенно облегчить
разработку сложных систем за счёт перехода
от упрощенных опытных моделей к промышленным
образцам. С другой стороны, на основе
данного вида виртуализации можно моделировать
и реальную эволюцию сложных систем, например,
биологических, технических, социальных.
Но не следует забывать, что этот процесс
возможен только при соблюдении неизменности
интерфейсов. Как следствие, требования
тщательности разработки интерфейсов очень
высоки. Однако, как будет отмечено ниже,
такие требования не являются непреодолимыми,
поскольку, с одной стороны, контейнеры
существенно упрощают вложенные классы,
в том числе и спецификации интерфейсов,
а, с другой стороны, наследование контейнеров,
как обычных классов, приводит и к наследованию
функционала, который представим в виде
ролей вложенных объектов. И, наконец,
у контейнеров, как и любых других классов
можно будет «наращивать» функциональность
по мере необходимости, повышая возможности
системы. Поэтому можно установить порядок
разработки контейнера, определяя тем самым
приоритеты реализации ролей, то есть интерфейсов
вложенных классов.
|