учебники, программирование, основы, введение в,

 

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

Как не следует использовать наследование
Для выработки методологического принципа часто полезно - как показано во многих обсуждениях этой книги - вначале понять, как не следует делать вещи. Понимание того, "что такое плохо", позволяет осознать, "что такое хорошо". Если постоянно тепло, то грушевое дерево не зацветет, ему необходима встряска зимним морозом - тогда оно расцветет весной.
Вот и встряска для нас, любезно предоставленная широко известным во всем мире вузовским учебником, выдержавшим 4 издания, по которому программной инженерии учатся многие студенты. Вот начало текста по поводу множественного наследования:
Множественное наследование позволяет нескольким объектам выступать в роли базовых и поддерживается во многих языках).
Помимо неудачного использования "объектов", вместо классов начало кажется весьма подозрительным. Цитата продолжается:
Характеристики нескольких различных классов объектов
(классы, уже хорошо!)
могут комбинироваться, создавая новый объект.
(Нет, опять неудача.) Далее следует пример множественного наследования:
например, пусть мы имеем класс объектов CAR, инкапсулирующий информацию об автомобиле, и класс PERSON, инкапсулирующий информацию о человеке. Мы можем использовать их для определения
(неужели оправдаются наши наихудшие подозрения?)
нового класса CAR-OWNER, комбинирующего атрибуты CAR и PERSON.
(Они оправдались.) Нас приглашают рассматривать каждый объект CAR-OWNER не только как персону, но и как автомобиль. Для каждого, кто изучал наследование даже на элементарном уровне, это станет сюрпризом.
Несомненно, вы понимаете, что второе отношение является клиентским, а не наследованием, владелец автомобиля является (is) персоной, но имеет (has) автомобиль.
В формальной записи:
class CAR_OWNER inherit
PERSON
feature
my_car: CAR
...
end
В цитируемом тексте обе связи используют отношение наследования. Наиболее интересный пассаж в этом обсуждении следует далее, когда автор советует читателям рассматривать наследование с осторожностью:
Адаптация через наследование имеет тенденцию приводить к избыточной функциональности, что может делать компоненты неэффективными и громоздкими.
И в самом деле, громоздкими - подумаем о владельце машины с крышей, мотором, карбюратором, не говоря уже о четырех колесах и запаске. Эта картина возникла, возможно, под влиянием образчика австралийского юмора, где владелец машины выглядит так, как если бы он являлся своим автомобилем.
Наследование не является тривиальной концепцией, так что мы можем забыть и простить автора процитированного отрывка, но сам пример имеет важную практическую пользу - он помог нам стать немного умнее и напомнил базисное правило наследования:


Правило: Наследование "Is-a" (является)
Не делайте класс B наследником класса A, если нельзя привести аргументы в защиту того, что каждый экземпляр B является также экземпляром A.

Другими словами, мы должны быть способными убеждать, что каждый B is an A (отсюда имя: "is-a").
Вопреки первому впечатлению, это слабое, а не строгое правило, и вот почему:

  • Обратите внимание на фразу "привести аргументы". Мы не требуем доказательства того, что каждый B всегда является A. В большинстве случаев мы оставляем пространство для дискуссии. Верно ли, что каждый "сберегательный счет" (savings account) является "текущим счетом" (checking account)? Здесь нет абсолютного ответа - все зависит от политики банка и вашего анализа свойств различных видов счетов. Возможно, вы решите сделать класс SAVINGS_ ACCOUNT наследником BANK_ACCOUNT или поместить его где-либо еще в структуре наследования. Разумные люди могут все же не согласиться с результатом. Это нестрашно, важно лишь, чтобы был случай, для которого ваши аргументы способны устоять. В нашем контрпримере: нет ситуации, при которой аргументы в пользу того, что CAR_OWNER является CAR, могли бы устоять.
  • Наш взгляд на то, что означает отношение "является", будет довольно либеральным. Он не будет, например, препятствовать наследованию реализации - форме наследования, многими считающейся подозрительной.

Эти наблюдения показывают как полезность, так и ограниченность правила "Is-a". Оно полезно как отрицательное правило, позволяя обнаружить и отвергнуть неподходящее использование наследования. Но как положительное правило оно недостаточно - не все, что проходит тест, заданный правилом, является подходящим случаем наследования.

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

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

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

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

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

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

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

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

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

Трюкачество - да, но все же [C] в основе не отличается от исходного высказывания [A]! Что отсюда следует: слегка изменив точку зрения, можно представить свойство является как имеет.
Рассмотрим структуру нашего объекта, как это делалось в предыдущих лекциях:
Экземпляр SOFTWARE_ENGINEER показывает различные аспекты деятельности инженера-программиста. Вместо представления типа этого объекта как развернутого, можно рассматривать представление в терминах ссылок:
Рассматривайте оба представления как способы визуализации ситуации, ничего более. Оба они исходят, однако, из отношения клиента имеет, интерпретации, в которой каждый инженер-программист несет в себе инженера как одну из своих ипостасей, что полностью согласуется с названием профессии. Одновременно в нем может быть сидит частица поэта и (или) сантехника. Подобные наблюдения могут быть сделаны для любого похожего отношения "является".
Вот почему проблема выбора между клиентом и наследованием не тривиальна - когда отношение "является" законно, то справедлив переход к отношению "иметь".
Обратное неверно. Это наблюдение предохраняет от простых ошибок, очевидно для всякого, кто понимает базисные концепции и, вероятно, объяснимо даже для авторов учебника. Но когда применимо отношение "является", то у него сразу же появляется соперник. Так что два компетентных специалиста могут не придти к одному решению: один выберет наследование, другой предпочтет клиентское отношение.
К счастью, существуют два критерия, помогающих в таких спорах. Иногда они могут не приводить к единственному решению. Но в большинстве практических случаев они без всяких колебаний указывают, какое из отношений является правильным.
Один из этих критериев предпочитает наследование, другой - клиента.

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

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

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", как показано на. Введение ссылок, однако, не сказывается на сути нашего обсуждения.

Поскольку отношение наследования задается между классами, то, приняв первое определение класса, динамически будет невозможно изменить отношение между объектами: инженер всегда останется инженером.
Но для других двух определений модификация возможна: процедура класса "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.

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

Приведем пример, использующий предшествующее правило. Он приводит к широко применимому образцу проектирования - описателям (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, представляющим необходимый инструментарий. Как это выглядит, показано на:
Интересный аспект этого решения в том, что понятие инструментария (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), чье возможное решение обсуждается позже в этой лекции.

Использование наследования: таксономия таксономии

Мощь наследования - это следствие его универсальности. Правда и то, что временами оно наносит вред, заставляя многих авторов вводить ограничения на механизм. Понимая эти опасения, а иногда и разделяя их, отбросим случайные сомнения и страхи и научимся радоваться наследованию во всех его законных вариантах, к исследованию которых мы теперь и переходим.
Дадим обзор правильного использования наследования:

  • наследование подтипов (subtype inheritance);
  • наследование вида (view inheritance);
  • наследование с ограничением (restriction inheritance);
  • наследование с расширением (extension inheritance);
  • наследование с функциональной вариацией (functional variation inheritance);
  • наследование с вариацией типа (type variation inheritance);
  • наследование с конкретизацией (reification inheritance);
  • структурное наследование (structure inheritance);
  • наследование реализации (implementation inheritance);
  • льготное наследование (facility inheritance) с двумя специальными вариантами: наследование констант и абстрактной машины (черного ящика) (constant inheritance и machine inheritance).

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

Область действия правил

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


Правило Наследования
Каждое использование наследования должно принадлежать одной из допустимых категорий.

Это правило утверждает, что все типы наследования известны и что, если встречается ситуация, не покрываемая этими типами, то не следует применять наследование.
Под допустимыми категориями понимаются категории, рассматриваемые в этом разделе. И я надеюсь, что все имеющие смысл ситуации полностью покрываются этим рассмотрением. Но таксономия (введение классификации) может нуждаться в дальнейшем обдумывании. Я нашел немногое в литературе по этой теме, наиболее полезная ссылка на неопубликованные тезисы диссертации [Girod 1991]. Так что вполне возможно, что в этой попытке классификации пропущены некоторые категории. Но правило говорит, что, если вы рассматриваете возможное применение наследования, не укладывающееся в предложенную схему, то следует серьезно подумать, скорее всего, применять его не следует. Если же по зрелому размышлению вы решите применить наследование, то это стоит рассматривать как новый вклад в классификацию.


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

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


Правило Упрощения Наследования
Следует предпочитать наследование, принадлежащее ровно одной допустимой категории.

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


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

Ошибочное использование

Прежде чем рассмотреть правильные случаи, еще раз поговорим об ошибках. Ошибаться - в природе человека, нельзя надеяться на полноту классификации возможных ошибок, но несколько общих ошибок идентифицируются просто.
Первая типичная ошибка связана с путаницей отношений "has" и "is". Класс CAR_OWNER служит примером - экстремальным, но не уникальным. Мне доводилось слышать и видеть и другие подобные примеры, такие как APPLE_PIE, наследуемый от APPLE и от PIE, или (упоминаемый Adele Goldberg) ROSE_TREE, наследуемый от ROSE и от TREE.
Другим типичным примером является таксомания, в котором простое булево свойство, такое как пол персоны (или свойство с несколькими фиксированными значениями, такое как цвет светофора), используется как критерий наследования, хотя нет важных вариантов компонентов, зависящих от свойства.
Третьей типичной ошибкой является наследование по расчету (convenience inheritance), при котором разработчик видит некоторые полезные компоненты класса и создает наследника просто для того, чтобы использовать эти компоненты. Заметьте, использование "наследования реализации" или "наследование компонентов класса" являются допустимыми формами наследования, изучаемыми позже в этой лекции. Ошибка в том, что класс используется как родитель без подходящего отношения is-a между соответствующими абстракциями, а в некоторых случаях вообще без адекватной абстракции.

Общая таксономия

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

  • наследование модели, отражающее отношения "is-a" между абстракциями, характерными для самой модели;
  • программное наследование, выражающее отношения между объектами программной системы, не имеющих очевидных двойников во внешней модели;
  • наследование вариаций - специальный случай, относящийся как к моделям, так и программному наследованию, служащий для описания вариаций семейства классов.

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


Так как классификация сама по себе является таксономией, можно из любопытства задаться вопросом, как применить к ней самой идентифицируемые категории. Это является темой упражнения У6.2.

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

Наследование подтипов

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


Определение: наследование подтипов
Наследование подтипов применимо, если A и B представляют некоторые множества A' и B' внешних объектов, так что B' является подмножеством A', и множество, моделируемое любым другим подтипом, наследуемым от A, не пересекается с B'. Класс A должен быть отложенным.

A' может быть множеством замкнутых фигур, B' - множеством многоугольников, A и B - соответствующие классы. В большинстве практических случаев "внешняя система" не принадлежит миру программ, например, определяет некоторые аспекты деятельности компании (внешними объектами являются специальные и депозитные счета) или часть внешнего мира (с планетами и звездами).
Наследование подтипов является формой наследования ближайшей к иерархической таксономии в ботанике, зоологии и других естественных науках.
(ПОЗВОНОЧНЫЕ МЛЕКОПИТАЮЩИЕ и подобные примеры).
Мы настаиваем, что родитель A должен быть отложенным, поскольку он описывает не полностью специфицированное множество объектов, в то время как наследник B может быть как эффективным, так и отложенным. Следующие две категории рассматривают ситуации, где A может быть эффективным классом.
В одном из следующих разделов эта категория наследования будет рассмотрена детальнее, поскольку она не столь уж проста, как может показаться с первого взгляда.

Наследование c ограничением

Определение: Наследование c Ограничением
Наследование c ограничением применимо, если экземпляры B являются экземплярами A, удовлетворяющими некоторому ограничению, выраженному, если это возможно, как часть инварианта B, не включенного в инвариант A. Любой компонент, введенный в B, должен быть логическим следствием добавленного ограничения. A и B должны быть оба отложенными или оба эффективными.

Типичным примером является: Прямоугольник Квадрат.
Ограничением является утверждение: сторона1 = сторона2 (включается в инвариант класса Квадрат).
Многие математические примеры подпадают под эту категорию.
Последняя часть определения позволяет избежать смешения этой формы наследования с другими, такими как наследование с расширением, допускающим появление полностью новых компонентов у наследника. Здесь в интересах простоты предпочтительно ограничивать новые компоненты теми, что непосредственно следуют из добавленного ограничения. Например, у класса CIRCLE может появиться новый компонент radius, отсутствующий у родительского класса ELLIPSE.
Поскольку допускаются только концептуальные изменения класса A, добавляющие некоторые ограничения в класс B, то оба класса должны быть либо отложенными, либо эффективными.
Наследование с ограничением концептуально близко к наследованию подтипов; последующее обсуждение создания подтипов (subtyping) будет относиться к обеим категориям.

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г.Яндекс.Метрика