Опубликован: 23.10.2005 | Доступ: свободный | Студентов: 4087 / 201 | Оценка: 4.44 / 4.19 | Длительность: 33:04:00
Специальности: Программист
Лекция 6:

Используйте наследование правильно

Приложение: техника описателей

Приведем пример, использующий предшествующее правило. Он приводит к широко применимому образцу проектирования - описателям (handles).

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

class WINDOW inherit
    GENERAL_WINDOW
    PLATFORM_WINDOW
feature
    ...
end

Класс GENERAL_WINDOW и ему подобные, такие как GENERAL_BUTTON, являются отложенными: они выражают все, что может быть сказано о соответствующих графических объектах и применимых операциях без ссылки на особенности графической платформы. Классы, такие как PLATFORM_WINDOW, обеспечивают связь с графической платформой, такой как Windows, OS/2 Presentation-Manager или Unix Motif; они дают доступ к механизмам, специфическим для данной платформы (встраиваемым в библиотеки, такие как WEL или MEL ).

Класс, такой как WINDOW, будет комбинировать свойства родителей, реализуя отложенные компоненты GENERAL_WINDOW механизмами, обеспечиваемыми PLATFORM_WINDOW.

Класс PLATFORM_WINDOW (как и другие подобные классы) должен присутствовать в нескольких вариантах - по одному на каждую платформу. Эти идентично именуемые классы будут храниться в различных каталогах; инструментарий Ace при компиляции выберет подходящий.

Это решение работает, но его недостаток в том, что понятие WINDOW становится тесно связанным с выбранной платформой. Перефразируя недавний комментарий о наследовании, можно сказать: окно, став однажды окном Motif, всегда им и останется. Это не слишком печально, поскольку трудно вообразить, что однажды, достигнув почтенного возраста, окно Unix вдруг решит стать окном OS/2. Картина становится менее абсурдной при расширении определения платформы - при включении форматов, таких как Postscript или HTML; графический объект может изменять представление, становясь то документом печати, то Web-документом.

Попытаемся выразить тесную связь между GUI-объектами и поддерживающим инструментарием, используя вместо наследования клиентское отношение. Наследственная связь останется между WINDOW и GENERAL_WINDOW, но зависимость от платформы будет представлена клиентской связью с классом TOOLKIT, представляющим необходимый инструментарий. Как это выглядит, показано на рис. 6.6:

Комбинация отношений наследования и клиента

Рис. 6.6. Комбинация отношений наследования и клиента

Интересный аспект этого решения в том, что понятие инструментария (toolkit) становится полноценной абстракцией, представляющей отложенный класс TOOLKIT. Каждый специфический инструментарий, такой как MOTIF или MS_WINDOWS представляется эффективным потомком класса TOOLKIT.

Вот как это работает. Каждый класс, описывающий графические объекты, такие как WINDOW, имеет атрибут, обеспечивающий доступ к соответствующей платформе:

handle: TOOLKIT

Так появляется поле для каждого экземпляра класса. Описатель может быть изменен:

set_handle (new: TOOLKIT) is
            -- Создать новый описатель new для этого объекта
    do
        handle := new
    end

Типичная операция, наследуемая от GENERAL_WINDOW в отложенной форме, реализуется через вызовы платформенного механизма:

display is
            -- Выводит окно на экран
    do
        handle.window_display (Current)
    end

Через описатель графический объект запрашивает платформу, требуя выполнить нужную операцию. Компонент, такой как window_display, в классе TOOLKIT является отложенным, но реализуется его различными потомками, такими как MOTIF.

Заметьте, было бы неверным, глядя на этот пример, придти к заключению: "Ага! Вот ситуация, при которой наследование было избыточным, и данная версия призвана избежать его". Начальная версия вовсе не была ошибочной, она работает довольно хорошо, но менее гибкая, чем вторая. И в основе второй версии лежит наследование, полиморфизм и динамическое связывание, комбинируемое с клиентским отношением. Без иерархии наследования с корнем TOOLKIT, полиморфной сущности handle и динамического связывания компонентов, таких как window_display, все бы это не работало. Вовсе не отвергая наследование, эта техника демонстрирует его более сложную форму.

Техника описателей широко применима к разработке библиотек, поддерживающих совместимость платформ. Помимо графической библиотеки Vision мы применяли ее к библиотеке баз данных Store, где понятие платформы связывается с основанными на SQL различными интерфейсами реляционных баз данных, таких как Oracle, Ingres, Sybase и ODBC.

Таксомания

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

Правило Таксомании (ограничения таксомании)

Каждый наследник обязан ввести новый компонент, или переобъявить наследуемый компонент, или добавить предложение в инвариант класса.

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

Как часто бывает в таких случаях, вернуться к правильному видению - и возвратить новичков на грешную землю - помогает обращение к АТД. Класс является реализацией АТД, частичной или полной. Различные классы, в частности родитель и его наследники, должны описывать различные АТД. Поскольку АТД полностью характеризуется применимыми компонентами и их свойствами, охватываемые утверждениями класса, новый класс должен изменять наследуемые компоненты, вводить новые компоненты и утверждения. Так как предусловие или постусловие можно изменить только при переопределении компонента, то последний случай означает добавление предложения инварианта класса ( наследование с ограничением (restriction inheritance) - одна из категорий в нашей таксономии).

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

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

female: BOOLEAN

или

Female: INTEGER is unique
Male: INTEGER is unique

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

О чем следует помнить, так это о принципе Единого Выбора. Мы научились не доверять явному перечислению вариантов, реализуемых константами unique, из опасения обнаружить в нашем ПО куски кода с условиями выбора в форме:

if female then
    ...
else
    ...

или inspect инструкциями. В данном случае, однако, не стоит особенно беспокоиться:

  • Критика этого стиля связана с тем, что добавление каждого нового варианта приводит к цепной реакции изменений во всей системе, но в подобных случаях можно быть уверенным, что новый пол не появится.
  • Даже при фиксированном множестве вариантов стиль явного if менее эффективен, чем основанный на динамическом связывании вызовов this_person.some_operation, где MALE и FEMALE по-разному определяют some_operation. Но тогда, если необходимо разделять людей по полу, мы нарушаем предпосылки данного обсуждения - отсутствие специфических свойств. Если такие свойства существуют, наследование оправдано.

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