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

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

Покупать или наследовать

Основное правило выбора между двумя возможными межмодульными отношениями - клиентом и наследованием - обманчиво просто: клиент имеет, наследование является. Почему же тогда выбор столь непрост?

Иметь и быть (To have and to be)

Причина в том, что иметь не всегда означает быть, но во многих случаях быть означает иметь.

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

Что можно сказать об обратной ситуации? Рассмотрим простое предложение о двух объектах из обычной программистской жизни:

Каждый инженер-программист является инженером. 	[A]

Очевидно, это хороший пример отношения является. Кажется, трудно думать по-другому - здесь ясно видно, что мы имеем дело со случаем быть, а не иметь. Но перефразируем утверждение:

В каждом инженере-программисте заключена частица инженера.	[B]

Представим его теперь так:

Каждый инженер-программист имеет инженерную составляющую.	[C]

Трюкачество - да, но все же [C] в основе не отличается от исходного высказывания [A]! Что отсюда следует: слегка изменив точку зрения, можно представить свойство является как имеет.

Рассмотрим структуру нашего объекта, как это делалось в предыдущих лекциях:

Объект "инженер-программист" как агрегат

Рис. 6.3. Объект "инженер-программист" как агрегат

Экземпляр SOFTWARE_ENGINEER показывает различные аспекты деятельности инженера-программиста. Вместо представления типа этого объекта как развернутого, можно рассматривать представление в терминах ссылок:

Другое возможное представление

Рис. 6.4. Другое возможное представление

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

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

Обратное неверно. Это наблюдение предохраняет от простых ошибок, очевидно для всякого, кто понимает базисные концепции и, вероятно, объяснимо даже для авторов учебника. Но когда применимо отношение "является", то у него сразу же появляется соперник. Так что два компетентных специалиста могут не придти к одному решению: один выберет наследование, другой предпочтет клиентское отношение.

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

Один из этих критериев предпочитает наследование, другой - клиента.

Правило изменений

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

Одним из определяющих свойств наследования является то, что это отношение между классами, а не между объектами. Мы интерпретировали свойство "Класс B наследует от класса A " как "каждый объект B является объектом A ". Следует помнить, что это свойство не в силах изменить никакой объект - только класс может достичь такого результата. Свойство характеризует ПО, но не его отдельное выполнение.

Для отношения клиента ограничения слабее. Если объект типа B имеет компонент типа A (либо подобъект, либо ссылку) вполне возможно изменить этот компонент - ограничением служит лишь система типов.

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

Объект и подобъект

Рис. 6.5. Объект и подобъект
class SOFTWARE_ENGINEER_1 inherit
        ENGINEER

feature

        ...

end

Она могла быть точно так же получена через отношение клиента:

class SOFTWARE_ENGINEER_2 feature

    the_engineer_in_me: ENGINEER
    ...

end

Фактически оно могло быть и таким:

class SOFTWARE_ENGINEER_3 feature

    the_truly_important_part_of_me: VOCATION
    ...

end

Для удовлетворения ограничений системы типов класс ENGINEER должен быть потомком класса VOCATION.

Строго говоря, последние два варианта представляют слегка отличную ситуацию. Если предположить, что ни один из заданных классов не является развернутым, то вместо подобъектов в последних двух случаях объекты " software engineer " будут содержать ссылки на объекты " engineer ", как показано на рис.6.4. Введение ссылок, однако, не сказывается на сути нашего обсуждения.

Поскольку отношение наследования задается между классами, то, приняв первое определение класса, динамически будет невозможно изменить отношение между объектами: инженер всегда останется инженером.

Но для других двух определений модификация возможна: процедура класса "software engineer" может присвоить новое значение полю соответствующего объекта (полю the_engineer_in_me или the_truly_important_part_of_me ). В случае класса SOFTWARE_ENGINEER_2 новое значение должно быть типа ENGINEER или совместимого с ним; для класса SOFTWARE_ENGINEER_3 оно может быть любого типа, совместимого с VOCATION ( Профессия ). Такая программа способна моделировать инженера-программиста, который после многих лет притязаний стать настоящим инженером, наконец, покончил с этой составляющей своей личности и решил стать поэтом или сантехником. ("Не надо оваций. Графа Монте-Кристо из меня не вышло. Придется переквалифицироваться в управдомы".)

Это приводит к нашему первому критерию:

Правило изменений

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

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

По настоящему интересный случай имеет место для SOFTWARE_ENGINEER_3. Для SOFTWARE_ENGINEER_2 можно заменить инженерный компонент на другой, но того же инженерного типа. Но для SOFTWARE_ENGINEER_3 класс VOCATION может быть более высокого уровня, вероятнее всего, отложенным, так что атрибут может (благодаря полиморфизму) представлять объекты многих возможных типов, согласованных с VOCATION.

Это также означает, что, хотя решение использует клиента как первичное отношение, но на практике в своей окончательной форме оно часто использует дополнительно отношение наследования. Это станет особенно ясно, когда мы придем к понятию описателя (handle).

Правило полиморфизма

Займемся теперь критерием, требующим наследования и исключающим клиента. Этот критерий прост: он основан на полиморфизме. При изучении наследования мы видели, что для объявления в форме:

x: C

x обозначает в период выполнения (предполагая, что класс C не является развернутым) полиморфную ссылку. Другими словами, x может быть присоединен как к прямому экземпляру C, так и к экземпляру потомков C. Это свойство представляет ключевой вклад в мощность и гибкость ОО-метода, особенно из-за следствий - возможности определения полиморфных структур данных, подобных LIST [C], которые могут содержать экземпляры любого из потомков C.

В нашем примере это означает, что, выбрав решение SOFTWARE_ENGINEER_1 - форму, в которой класс является наследником ENGINEER, клиент может объявить сущность:

eng: ENGINEER

Эта сущность в период выполнения может быть присоединена к объекту типа SOFTWARE_ENGINEER_1. Можно иметь список инженеров, базу данных, включающую инженеров-механиков, инженеров-химиков наряду с программистами.

Методологическое напоминание: использование слов, не относящихся к программе, облегчает понимание концепций, но это нужно делать с осторожностью, особенно для антропологических примеров. Объекты нашего интереса являются программными объектами, поэтому, когда мы говорим "a software engineer", то это фактически означает экземпляр класса SOFTWARE_ENGINEER_1.

Такие полиморфные эффекты требуют наследования: в случае SOFTWARE_ENGINEER_2 или SOFTWARE_ENGINEER_3 сущности или структуры данных типа ENGINEER не могут непосредственно означать объекты "software engineer".

Обобщая это наблюдение, характерное не только для этого примера, приходим к правилу, дополняющему правило изменений:

Правило полиморфизма

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

Резюме

Хотя оно и не вводит новых концепций, следующее правило удобно как итог обсуждения критериев, высказывающихся за и против наследования.

Выбор между клиентом и наследованием

При решении, как выразить зависимость между классами B и A, применяйте следующие критерии:

  • CI1 Если каждый экземпляр B изначально имеет компонент типа A, но этот компонент в период выполнения может нуждаться в замене объектом другого типа, сделайте B клиентом A.
  • CI2 Если необходимо, чтобы сущности типа A обозначали объекты типа B или в полиморфных структурах, содержащих объекты типа A, некоторые могли быть типа B, сделайте B наследником A.