Основы объектно-ориентированного проектирования

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

Разбить на страницы
Показывать лекцию целиком

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

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

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

Множественное наследование позволяет нескольким объектам выступать в роли базовых и поддерживается во многих языках (ссылка на первое издание этой книги [M1988]).

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

Характеристики нескольких различных классов объектов

(классы, уже хорошо!)

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

(Нет, опять неудача.) Далее следует пример множественного наследования:

например, пусть мы имеем класс объектов CAR, инкапсулирующий информацию об автомобиле, и класс PERSON, инкапсулирующий информацию о человеке. Мы можем использовать их для определения

(неужели оправдаются наши наихудшие подозрения?)

нового класса CAR-OWNER, комбинирующего атрибуты CAR и PERSON.

(Они оправдались.) Нас приглашают рассматривать каждый объект CAR-OWNER не только как персону, но и как автомобиль. Для каждого, кто изучал наследование даже на элементарном уровне, это станет сюрпризом.

Несомненно, вы понимаете, что второе отношение является клиентским, а не наследованием, владелец автомобиля является (is) персоной, но имеет (has) автомобиль.

(рис 6.1) Походящая модель

В формальной записи:

class CAR_OWNER inherit
    PERSON
feature
    my_car: CAR
    ...
end

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

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

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

(рис 6.2) Рисунок Джеффа Хокинга (голова его похожа на его же авто с открытыми дверцами)

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

Правило: Наследование "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]! Что отсюда следует: слегка изменив точку зрения, можно представить свойство является как имеет.

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

    (рис 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.
  • Приложение: техника описателей

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

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

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

    Дадим обзор правильного использования наследования:

  • наследование подтипов (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 между соответствующими абстракциями, а в некоторых случаях вообще без адекватной абстракции.

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

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

    (рис 6.7) Классификация допустимых категорий наследования

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

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

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

    Следующие далее определения используют имя A для родительского класса и B для наследника:

    (рис 6.8) Соглашение именования при определении категорий наследования

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

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

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

    Определение: наследование подтипов

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

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

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

    ( ПОЗВОНОЧНЫЕ $$\gets$$ МЛЕКОПИТАЮЩИЕ и подобные примеры).

    Мы настаиваем, что родитель A должен быть отложенным, поскольку он описывает не полностью специфицированное множество объектов, в то время как наследник B может быть как эффективным, так и отложенным. Следующие две категории рассматривают ситуации, где A может быть эффективным классом.

    В одном из следующих разделов эта категория наследования будет рассмотрена детальнее, поскольку она не столь уж проста, как может показаться с первого взгляда.

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

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

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

    Типичным примером является: Прямоугольник $$\gets$$ Квадрат.

    Ограничением является утверждение: сторона1 = сторона2 (включается в инвариант класса Квадрат ).

    Многие математические примеры подпадают под эту категорию.

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

    Поскольку допускаются только концептуальные изменения класса A, добавляющие некоторые ограничения в класс B, то оба класса должны быть либо отложенными, либо эффективными.

    Наследование с ограничением концептуально близко к наследованию подтипов; последующее обсуждение создания подтипов (subtyping) будет относиться к обеим категориям.

    Наследование с расширением

    Определение: Наследование с Расширением

    Наследование с расширением применимо, когда B вводит компоненты, не представленные в A и не применимые к прямым экземплярам A. Класс A должен быть эффективным.

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

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

    class A feature a1: INTEGER end
    class B inherit
        A
    feature
        b1: REAL
    end

    Рассмотрим каждый экземпляр класса A как одноэлементное множество (которое можно записать как <n>, где n целое), а каждый экземпляр B - как пару, содержащую целое и вещественное (например, пару <1, -2.5> ). Множество пар MB не является подмножеством одноэлементного множества MA. Верно обратное, отношение "быть подмножеством" имеет место в обратном направлении, поскольку существует отображение один-к-одному между MA и множеством всех пар, имеющих данный второй элемент.

    Обнаружение того факта, что отношение "быть подмножеством" не выполняется, делает наследование расширением довольно подозрительным. Например, в ранней версии уважаемой ОО-библиотеки (не от ISE) класс RECTANGLE был наследником SQUARE, в отличие от изучаемого нами способа. Причина простая: класс SQUARE имеет атрибут side ; класс RECTANGLE наследует его, добавляя новый компонент other_side. Этот проект был подвергнут критике, он был пересмотрен с обращением наследования.

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

    Соответствующая схема используется и при разработке ОО-ПО. Конечно, класс SQUARE должен быть наследником RECTANGLE, а не наоборот, но можно предложить легитимные примеры. Класс MOVING_POINT (в приложениях кинематики) может наследовать от чисто графического класса POINT и добавлять компонент speed, описывающую величину и направление скорости. Другой пример, в текстовом процессоре класс CHAPTER может наследовать от DOCUMENT, добавляя специфические свойства - текущую позицию лекции в книге и процедуру ее сохранения.

    Подходящая математическая модель

    (Читатели - не математики могут пропустить этот раздел.)

    Для успокоения совести следует разрешить видимый парадокс, отмеченный выше (обнаружение того, что MB не является подмножеством MA ), так как мы хотим, чтобы некоторое отношение подмножества имело место между наследником и родителем. И это отношение реально существует, парадокс лишь показывает, что декартово произведение атрибутов не является подходящей моделью для моделирования класса. Рассмотрим класс:

    class C feature
        c1: T1
        c2: T2
        c3: T3
    end

    Мы не должны выбирать в качестве математической модели C' - множества экземпляров C - декартово произведение T'1 _ T'2 _ T'3, где знак штрих ' указывает на рекурсивное использование модели множеств, приводящее к парадоксу (наряду с другими недостатками).

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

  • A1 Функция определена для c1, c2 и c3.
  • A2 Множество VALUE (множество цели для функции) является супермножеством $$T'1 \cup T'2 \cup T'3$$.
  • A3 Значения функции для c1 лежат в T'1 и так далее.
  • Тогда, если вспомнить, что функция является специальным случаем отношения и что отношение является множеством пар (например, в ранее упоминаемом случае экземпляр класса A может быть промоделирован функцией {<a1, 25>}, а экземпляр класса B - {<a1, 1>, <b1, -2.5>} ), мы получаем ожидаемое свойство - B' является подмножеством A'. Заметьте, здесь уже элементы обоих множеств являются парами и первая функция задает все возможные отображения второго атрибута.

    Заметьте также, что принципиально важно установить свойство A1 как "Функция определена для...", но не в виде "Областью определения функции является...", что ограничивало бы область множеством {c1, c2 c3}, не позволяя потомкам добавлять свои собственные атрибуты. Как результат такого подхода, каждый программный объект моделируется неограниченным числом математических объектов.

    Это обсуждение дает только схему математической модели. С деталями использования частичных функций для моделирования кортежей и общими математическими основами можно ознакомиться в [M 1990].

    Наследование вариаций

    (Читатели - не математики, добро пожаловать!) Перейдем теперь ко второму семейству категорий - наследованию вариаций.

    Определение: Наследование вариаций типа и функций

    Наследование вариаций применяется, если B переопределяет некоторые компоненты A ; A и B являются оба либо отложенными, либо эффективными. Класс B не должен вводить никаких новых компонентов за исключением тех, что непосредственно необходимы переопределяемым компонентам. Здесь рассматриваются два случая:

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

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

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

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

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

    perpendicular: SEGMENT is
            -- Сегмент, повернутый на 90 градусов
        ...

    Затем определим наследника DOTTED_SEGMENT, дающего графическое представление пунктирными, а не непрерывными линиями. В этом классе perpendicular должен возвращать результат типа DOTTED_SEGMENT, так что необходимо переопределить тип. Этого бы не требовалось, если бы изначально результат объявлялся как like Current. Так что, будь у вас доступ к источнику и его автору, можно было бы предложить модифицировать оригинал, не нанося ущерба существующим клиентам. Но если нет возможности модифицировать оригинал или по ряду причин закрепленное объявление не подходит оригиналу (вероятно, из-за потребностей других потомков), то возможность переопределить тип может стать палочкой-выручалочкой.

    При наследовании функциональной вариации изменяются тела некоторых компонентов. Если, как обычно в таком случае, компонент уже эффективен, то это означает изменение реализации. Спецификация компонента, заданная утверждением, также может измениться. Также возможно, хотя и реже встречается, иметь функциональную вариацию между двумя отложенными классами, в этом случае будут меняться только утверждения. Это может повлечь изменения в некоторых функциях, отложенных или эффективных, связанных с утверждениями, или даже добавление новых компонентов.

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

    Отмена эффективизации

    Определение: Наследование с Отменой эффективизации

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

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

  • При множественном наследовании сливаются компоненты, наследуемые от двух различных родителей. Если один из них отложенный, а другой эффективный, то слияние произойдет автоматически при условии совпадения имен (возможно после переименования), эффективная версия будет определять реализацию. Но если обе версии эффективны, то следует провести потерю эффективизации одной версии, отдавая предпочтение другой версии.
  • Хотя абстракция соответствует потребностям, но повторно используемый класс слишком конкретен для наших целей. Отмена эффективизации позволит удалит нежеланную реализацию. Перед использованием этого решения следует рассмотреть альтернативу - реорганизовать иерархию наследования, сделав более конкретный класс наследником нового отложенного класса. По понятным причинам это не всегда возможно, например из-за отсутствия доступа к исходному коду. Отмена эффективизации может в таких случаях обеспечить полезную форму обобщения.
  • Для этой категории наследования класс B будет отложенным; A обычно эффективным, но может быть частично отложенным.

    Наследование с конкретизацией

    Перейдем теперь к третьей и последней группе категорий - программному наследованию.

    Определение: Наследование с конкретизацией

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

    Примером, используемым многократно в предыдущих лекциях, является отложенный класс TABLE, описывающий таблицы самой общей природы. Конкретизация ведет к потомкам SEQUENTIAL_ TABLE и HASH_TABLE, все еще отложенным. Заключительная конкретизация SEQUENTIAL_TABLE приводит к эффективным классам ARRAYED_TABLE, LINKED_TABLE, FILE_TABLE.

    Термин "конкретизация" (reification), введенный Георгом Лукасом, происходит от латинского слова, означающего "превращение в вещь". Он используется в спецификациях и методе разработки VDM.

    Структурное наследование

    Определение: структурное наследование

    Структурное наследование применяется, если A, отложенный класс, представляет общее структурное свойство, а B, который может быть отложенным или эффективным, представляет некоторый тип объектов, обладающих этим свойством.

    Обычно A представляет математическое свойство, которым может обладать некоторое множество объектов. Например, A может быть классом COMPARABLE, поставляемым с такими операциями, как infix "<" и infix ">=", представляющим объекты с заданным отношением полного порядка. Класс, которому необходимо отношение порядка, такой как STRING, становится наследником COMPARABLE.

    Наследование таким способом от нескольких родителей является обычным приемом. Например, класс INTEGER в библиотеке Kernel наследует от COMPARABLE и класса NUMERIC (с такими компонентами, как infix "+" и infix "*" ), задающего арифметические свойства. (Класс NUMERIC более точно представляет математическое понятие кольца.)

    В чем разница между конкретизацией и структурным наследованием? При конкретизации B представляет то же понятие, что и A, отличаясь большей степенью реализации; при структурном наследовании B представляет собственную абстракцию, для которой A задает лишь один из аспектов, такой как порядок на объектах или наличие арифметических операций.

    Валден и Нерсон заметили, что новички иногда верят, что они используют подобную форму наследования, подменяя фактически имеющее место отношение "is" вариантом схемы "car-owner" ( AIRPLANE наследуется от VENTILATION_SYSTEM ). Они указывают, что этой ошибки просто избежать благодаря абсолютному критерию, не оставляющему места для сомнений или двусмысленности:

    При схеме наследования, хотя наследуемые свойства являются вторичными, они все же являются свойствами всего объекта, описываемого классом. Если мы делаем AIRPLANE наследником COMPARABLE , то отношение порядка применимо к каждому самолету как к целому, но свойства VENTILATION_SYSTEM не таковы. Компонент stop VENTILATION_SYSTEM не прекращает полет самолета.

    Заключение в этом примере очевидно: AIRPLANE должен быть клиентом, а не наследником класса VENTILATION_SYSTEM.

    Наследование реализации

    Определение: Наследование Реализации

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

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

    Случай наследования константных атрибутов и однократных функций покрывается следующим вариантом.

    Льготное наследование

    Льготное наследование является схемой, в которой родитель представляет коллекцию полезных компонентов, предназначенных для использования его потомками.

    Определение: Льготное наследование

    Льготное наследование применяется, если A существует единственно в целях обеспечения множества логически связанных компонентов, дающих преимущества его потомкам, таким как B. Двумя общими вариантами являются:

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

    Иногда, как в примерах, которые появятся чуть позже, при льготном наследовании используется только один вариант - константы или абстрактная машина, но в других случаях, как для класса EXCEPTIONS, родительский класс предоставляет как константы (такие как коды исключений Incorrect_inspect_value ), так и подпрограммы (такие как trigger для возбуждения исключения разработчика). Так как при нашем обсуждении категории наследования рассматриваются как непересекающиеся, то льготное наследование с двумя пересекающимися вариантами рассматривается как одна категория.

    При наследовании констант как A, так и B являются эффективными. При наследовании абстрактной машины ситуация более гибкая, но B должно быть, по меньшей мере, столь же эффективно как и A.

    В деталях льготное наследование еще будет обсуждаться в данной лекции.

    Использование наследования с отложенными и эффективными классами

    В следующей таблице обобщены правила, определяющие для каждой категории, должны ли родитель и его потомок быть эффективными или отложенными классами. "Вариация" покрывает случаи вариации типа и функциональной вариации. Элементы, помеченные символом *, появляются более чем в одном входе.

    Отложенные и эффективные наследники и их родители
    ПотомокРодитель
    ОтложенныйЭффективный
    Отложенный

    Константы*

    Ограничение*

    Структура*

    Тип*

    Потеря эффективизации*

    Вариация*

    Вид

    Расширение*

    Потеря эффективизации*

    Эффективный

    Константы*

    Конкретизация

    Структура*

    Тип*

    Константы*

    Расширение*

    Реализация

    Ограничение*

    Вариация*

    Один механизм, или несколько?

    Заметьте, это обсуждение предполагает в качестве основы раннюю презентацию, определяющую смысл наследования (см. лекцию 14 курса "Основы объектно-ориентированного программирования").

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

    Такое разделение нанесло бы больше вреда, чем принесло пользы. Вот несколько доводов.

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

    Практическим следствием были бы бесполезные методологические обсуждения: предположим, вы хотите наследовать от класса итератора, такого как LINEAR_ITERATOR ; следует ли использовать наследование модуля или наследование типа? Можно приводить аргументы в защиту одного и другого решения. Вклад этого предложения в критерий качества нашего ПО и скорость его создания будут фактически нулевыми.

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

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

  • Переопределение полезно как для подтипов (вспомните RECTANGLE, переопределяющий perimeter от POLYGON ) и для расширения модуля (принцип Открыт-Закрыт требует при наследовании модуля сохранения гибкости изменений, без чего будет потеряно одно из главных преимуществ ОО-метода).
  • Переименование полезно при наследовании модуля. Полагать его неподходящим при наследовании типа (см. [Breu 1995]) представляется серьезным ограничением. При моделировании внешней системы варианты некоторого понятия могут вводить специальную терминологию, которую желательно сохранить в ПО. Класс STATE_INSTITUTIONS в географической или выборной информационной системе может иметь потомка LOUISIANA_INSTITUTIONS, отражающего особенности политической структуры штата Луизиана, поэтому вполне ожидаемо желание потомка переименовать компонент counties, задающий список округов штатов, в parishes - имя, используемое для округа в данном штате.
  • Дублируемое наследование может встретиться для любой из форм. Так как можно ожидать, что только наследование модуля сохранит полиморфную подстановку, то при наследовании типов тут же возникнет необходимость разбора случаев и предложения select со всеми недостатками при появлении новых случаев. Появляются и другие вопросы - когда разделять компоненты, а когда их дублировать.
  • При введении в язык новых механизмов они взаимодействуют друг с другом и с другими механизмами языка. Должны ли мы защитить класс от совместного наследования и модуля, и типа? Если да, то будут возмущены разработчики, использующие класс двумя возможными способами, если нет, мы откроем ящик Пандоры, грозящий появлением множества проблем - конфликтов имен, переопределений и так далее.
  • Все это ради преимуществ пуристской точки зрения - ограниченной и спорной. Нет ничего плохого в защите спорной точки зрения, но следует быть крайне осторожным в нововведениях и учитывать их последствия для пользователей языка. И снова примером может служить Эдсгар Дейкстра в исследовании goto. Он не только в деталях объяснил все недостатки этой инструкции, основываясь на теории конструирования ПО и процесса его выполнения, но и показал, как можно без труда заменить этот механизм. В данном же случае убедительные аргументы не представлены, по крайней мере, я не увидел, почему "плохо" иметь единый механизм, покрывающий как наследование модулей, так и наследование типа.

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

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

    Наследование подтипов и скрытие потомков

    Первая категория наследования из нашего списка, вероятно, единственная, с которой согласится каждый, по меньшей мере, тот, кто принимает наследование: то, что мы можем назвать чистым наследованием подтипов (типов).

    Большая часть данного обсуждения применима и к наследованию с ограничением, чье принципиальное отличие состоит в том, что от родителя не требуется быть отложенным классом.

    Определение подтипа

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

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

  • FIGURE $$\gets$$ CLOSED_FIGURE $$\gets$$ POLYGON $$\gets$$ QUADRANGLE $$\gets$$ RECTANGLE $$\gets$$ SQUARE
  • DEVICE $$\gets$$ FILE $$\gets$$ TEXT_FILE
  • SHIP $$\gets$$ LEISURE_SHIP $$\gets$$ SAILBOAT
  • ACCOUNT $$\gets$$ SAVINGS_ACCOUNT $$\gets$$ FIXED_RATE_ACCOUNT
  • В любом из этих случаев четко идентифицируется множество объектов, описываемое родительским типом, и мы выделяем подмножество, характеризуемое некоторыми свойствами, которыми обладают не все объекты родителя. Например, текстовый файл является файлом со специальными свойствами, вытекающими из того, что элементы файла являются строками текста, что нехарактерно, например, для бинарных файлов.

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

    Некоторые из примеров, такие как RECTANGLE $$\gets$$ SQUARE, возможно, включают эффективного родителя и потому представляют случаи наследования с ограничением.

    Различные взгляды

    Наследование подтипов кажется простым, когда существует четкий критерий классификации вариантов определенного понятия. Но иногда некоторые качества начинают конкурировать между собой. Даже в, казалось бы, совсем простом случае классификации многоугольников могут возникать сомнения: следует ли использовать для классификации число сторон, создавая такие классы, как TRIANGLE, QUADRANGLE etc., или следует разделять объекты на правильные многоугольники ( EQUILATERAL_POLYGON, SQUARE и т. д.) и неправильные?

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

    Взгляд на подтипы

    Тип - это не просто множество объектов. Он характеризуется также применимыми операциями (компонентами) и их семантическими свойствами (утверждениями: предусловиями, постусловиями, инвариантами). Мы предполагаем, что компоненты и утверждения наследника совместимы с концепцией подтипа, означая, что любой экземпляр наследника должен рассматриваться также как экземпляр родителя.

    Правила, применяемые к утверждениям, поддерживают этот взгляд на подтипы:

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

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

    Необходимость скрытия потомком

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

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

    Альтернативы обсуждались при рассмотрении основополагающего принципа Открыт-Закрыт и они не кажутся привлекательными:

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

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

    Рассмотрим класс ELLIPSE. Эллипс имеет два фокуса, через которые можно провести прямую:

    (рис 6.9) Эллипс и фокусная линия

    Класс ELLIPSE может соответственно иметь компонент focus_line.

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

    (рис 6.10) Круг и его центр

    Хороший ли это пример для скрытия потомком? Должен ли класс CIRCLE сделать компонент focus_line закрытым, как здесь:

    class CIRCLE inherit
        ELLIPSE
            export {NONE} focus_line end
        ...

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

    focus_line is
            -- Линия, проходящая через два фокуса
        require
            not equal (focus_1, focus_2)
        do
            ...
        end

    (Предусловие может быть абстрактным, использующим функцию distinct_focuses ; с тем преимуществом, что класс CIRCLE может сам переопределить ее.)

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

    Приложения скрытия потомком

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

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

    Рассмотрим иерархию с корневым классом MORTGAGE ( ЗАКЛАДНАЯ ). Потомки организуются в соответствии с различными критериями, такими как фиксированная или переменная ставка, деловая или персональная, любыми другими. Для простоты будем полагать, что речь идет о таксономии - чистом случае подтипов. Класс MORTGAGE имеет процедуру redeem (выплачивать долг по закладной), управляющей выплатами по закладной в некоторый период, предшествующий сроку оплаты.

    Теперь предположим, что Конгресс в порыве великодушия (или под давлением лоббистов) ввел новую форму закладных, субсидируемых правительством, чьи преимущества одновременно предполагают запрет досрочных выплат. В иерархии классов найдется место для класса NEW_MORTGAGE ; но что делать с процедурой redeem?

    Можно было бы использовать технику предусловий, как в случае с focus_line. Но что, если банкиру никогда не приходилось иметь дело с закладными, по которым нельзя платить досрочно? Тогда, вероятно, процедура redeem не будет иметь предусловия.

    Так что использование предусловия потребует модификации класса MORTGAGE со всеми вытекающими последствиями. Предположим, однако, что в данном случае проблем с модификацией не будет, и мы добавим в класс булеву функцию redeemable и предусловие к redeem:

    require
        redeemable

    Но тем самым мы изменили интерфейс класса. Все клиенты класса и их бесчисленные потомки мгновенно стали потенциально некорректными. Все их вызовы m.redeem (...) должны быть теперь переписаны как:

    if m.redeemable then
        m.redeem (...)
    else
        ... (Кто в мире мог предвидеть это?)...
    end

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

    Отсутствие предусловия в исходной версии redeem не является ошибкой проектирования. В период создания проекта каждая закладная допускала досрочные выплаты. Мы не можем требовать предусловий для каждого компонента, иначе до конца своих дней придется каждый вызов предварять тестом if.

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

    class NEW_MORTGAGE inherit
        MORTGAGE
            export {NONE} redeem end
        ...

    Ни ошибок, ни аномалий не появится в существующем ПО. Если кто-то модифицирует класс клиента, добавив класс с новыми закладными:

    m: MORTGAGE; nm: NEW_MORTGAGE
    ...
    m := nm
    ...
    m.redeem (...)

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

    Таксономии и их ограничения

    Исключения таксономии не являются спецификой программистских примеров. В большей степени они характерны для естественных наук, где почти невозможно найти утверждение в форме "члены ABC phylum [или genus, species etc.] характеризуются свойством XYZ ", которому не предшествовало бы " большинство ", " обычно " или за которым не следовало бы " за исключением нескольких случаев ". Это справедливо на всех уровнях иерархии, даже для наиболее фундаментальных категорий, для которых, казалось бы, существуют бесспорные критерии!

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

    Отличие растений от животных

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

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

    Пища. Зеленые растения, содержащие хлорофилл, сами производят еду для себя, большинство животных питаются растениями или поедают других животных.

    Рост. Растения обычно растут от концов своих ветвей и корней и от внешних участков ствола в течение всей жизни. У животных рост обычно идет во всех частях их тела и прекращается при достижении зрелости.

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

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

    Известный пример из зоологии, ставший уже клише, иллюстрирует таксономию исключений. (Помните, однако, что это только аналогия, а не программистский пример.) Птицы летают. (Класс BIRD должен иметь процедуру fly.) Страус - птица, но страус не летает. При создании наследника - класса OSTRICH - необходимо будет указать, что эта самая большая из птиц не летает.

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

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

    В программистских терминах класс OSTRICH будет наследником BIRD, скрывая наследуемый компонент fly.

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

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

    Практика разработки ПО и аналогии природного мира свидетельствуют, что даже при самом тщательном проектировании остаются исключения таксономии. Скрытие redeem класса NEW_MORTGAGE или fly из OSTRICH не является свидетельством небрежного проектирования или недостаточного предвидения, оно свидетельствует о реальной сложности иерархии наследования.

    Такие исключения таксономии имеют прецеденты, насчитывающие столетние усилия интеллектуальных гигантов (включая Аристотеля, Линнея, Бюффона и Дарвина). Они сигнализируют о внутренних ограничениях человеческой способности познания мира. Связаны ли они с результатами, шокирующими научную мысль в двадцатом столетии - принципом неопределенности в физике, неразрешимыми проблемами в математике?

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

    Наследование реализации

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

    Брак по расчету

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

    class ARRAYED_STACK [G] inherit
        STACK [G]
            redefine change_top end
        ARRAY [G]
            rename
                count as capacity, put as array_put
            export
                {NONE} all
            end
    feature
        ... Реализация отложенных программ STACK, таких как put, count, full...
        ... и переопределение change_top в терминах операций ARRAY ...
    end

    Интересно сравнить представленную схему класса ARRAYED_STACK с классом STACK2 из предыдущих обсуждений (см. лекцию 11 курса "Основы объектно-ориентированного программирования") - реализацию стека массивом, но без использования наследования. Заметьте, устранение необходимости быть клиентом ARRAY упрощает нотацию (предыдущая версия должна была использовать вызов в форме implementation.put, теперь можно писать просто put ).

    При наследовании все компоненты ARRAY были сделаны закрытыми. Это типично при браках по расчету: все компоненты родителя, обеспечивающего спецификацию, здесь STACK, экспортируются; все компоненты родителя, обеспечивающего реализацию, здесь ARRAY, скрываются. Это вынуждает клиентов класса ARRAYED_STACK использовать соответствующие экземпляры только через компоненты стека.

    Это выглядит привлекательно, но правильно ли это?

    Наследование реализации подвергается критике. Скрытие компонентов кажется нарушением отношения "is-a".

    Это не так. У этого отношения есть разные формы. По своему поведению стек, основанный на массиве, ведет себя как стек. По внутреннему представлению он массив, и экземпляры ARRAYED_STACK отличаются от экземпляров ARRAY лишь обогащением за счет атрибута ( count ). Экземпляры, создаваемые по единому образцу, представляют достаточно строгую форму отношения "is-a". И дело не только в представлении: все компоненты ARRAY, такие как put (переименованный в array_put ), infix "@" и count (переименованный capacity ), доступны для ARRAYED_STACK, хотя и не экспортируются его клиентам. Классу они необходимы для реализации компонентов STACK.

    Так что концептуально ничего ошибочного нет в наследовании в интересах реализации. Показательно сравнение с контрпримером, изучаемым в начале этой лекции, класс CAR_OWNER свидетельство непонимания концепции наследования, класс ARRAYED_STACK задает хорошо определенную форму отношения "is-a".

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

    Как это делается без наследования

    Давайте проверим, как можно выполнить эту работу без использования наследования. Для нашего примера это было уже сделано в классе STACK2 из предыдущих лекций. Он имеет атрибут representation типа ARRAY [G] и процедуры стека, реализованные в следующем виде (утверждения опущены):

    put (x: G) is
            -- Добавляет x на вершину
        require
            ...
        do
            count := count + 1
            representation.put (count, x)
        ensure
            ...
        end

    Каждая манипуляция с представлением требует вызова компонента ARRAY с representation как цели. Платой являются потери производительности: минимальные по памяти (атрибут representation ), более серьезные по времени (связанные с representation, накладные расходы, добавляемые при вызове каждой операции).

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

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

    indexing
        description: "Объекты, имеющие доступ к массиву и его операциям"
    class
        ARRAYED [G]
    feature -- Access
        item (i: INTEGER): G is
                -- Элемент представления с индексом i
            require
                ...
            do
                Result := representation.item (i)
            ensure
                ...
            end
    feature -- Element change
        put (x: G; i: INTEGER) is
                -- Замена на x элемента с индексом i
            require
                ...
            do
                representation.put (x, i)
            ensure
                ...
            end
    feature {NONE} -- Implementation
        representation: ARRAY [G]
    end

    Компоненты item и put экспортированы. Так как ARRAYED описывает только внутренние свойства структуры данных, нет реальной необходимости в экспортируемых компонентах. Так что тот, кто не согласен с самой идей разрешения потомкам скрывать некоторые из экспортируемых компонентов, может предпочесть сделать закрытыми все компоненты ARRAYED. По умолчанию они тогда будут скрытыми и у потомков.

    При таком определении класса не вызывает споров, что классы, такие как ARRAYED_STACK или ARRAYED_LIST, становятся наследниками ARRAYED: они действительно описывают структуры на массивах. Эти классы могут теперь использовать item вместо representation.item и так далее; мы избавились от утомительного повторения.

    Но минуточку! Если наследовать от ARRAYED представляется правильным, почему же нельзя непосредственно наследовать от ARRAY? Никакой выгоды от введения еще одного слоя, надстроенного над ARRAY. Введение ARRAYED позволило убедить себя, что наследование реализации не используется, но по соображениям практики мы пошли на это, сделав систему более сложной и менее эффективной.

    На самом деле нет никаких причин для введения класса ARRAYED. Прямое наследование реализации от классов, подобных ARRAY, проще и легитимнее.

    Льготное наследование

    При льготном наследовании мы еще менее щепетильны, чем при наследовании реализации. Чистый расчет руководит нами при вступлении в брак. Мы видим класс с полезными свойствами и хотим использовать его. Здесь нет ничего предосудительного, поскольку таково назначение класса.

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

    Библиотека Base Libraries включает класс ASCII:

    indexing
        description:
        "Множество символов ASCII. %
        %Этот класс - предок всех классов, нуждающихся в его свойствах."
    class ASCII feature -- Access
        Character_set_size: INTEGER is 128; Last_ascii: INTEGER is 127
        First_printable: INTEGER is 32; Last_printable: INTEGER is 126
        Letter_layout: INTEGER is 70
        Case_diff: INTEGER is 32
            -- Lower_a - Upper_a
        ...
        Ctrl_a: INTEGER is 1; Soh: INTEGER is 1
        Ctrl_b: INTEGER is 2; Stx: INTEGER is 2
        ...
        Blank: INTEGER is 32; Sp: INTEGER is 32
        Exclamation: INTEGER is 33; Doublequote: INTEGER is 34
        ...
        ...
        Upper_a: INTEGER is 65; Upper_b: INTEGER is 66
        ...
        Lower_a: INTEGER is 97; Lower_b: INTEGER is 98
        ... и т.д. ...
    end

    Этот класс является хранилищем множества константных атрибутов (всего 142 компонента), описывающих свойства множества ASCII.

    Рассмотрим, например, лексический анализатор, ответственный за идентификацию лексем входного текста. Лексемами текста, написанного на некотором языке программирования, являются целые, идентификаторы, символы и так далее. Одному из классов системы, скажем, TOKENIZER, необходим доступ к кодам символов для их классификации на цифры, буквы и т. д. Такой класс воспользуется льготами и наследует эти коды от ASCII:

    class TOKENIZER inherit ASCII feature
        ... Программы класса могут использовать компоненты Blank, Case_diff и другие...
    end

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

    Итераторы

    Второй пример демонстрирует наследование программ общего вида, а не константных атрибутов.

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

    С первого взгляда может показаться, что итерирующие компоненты должны принадлежать классам, описывающих соответствующие структуры данных, таким как LIST или SEQUENCE.

    В упражнении У6.7 предлагается показать, что это неправильное решение.

    Предпочтительнее ввести независимую иерархию итераторов, показанную на рис. 6.11.

    (рис 6.11) Иерархия итераторов

    Класс LINEAR_ITERATOR, один из наиболее интересных классов в этом обсуждении, выглядит так:

    indexing
        description:
            "Объекты, допускающие итерирование на линейных структурах"
        names: iterators, iteration, linear_iterators, linear_iteration
    deferred class LINEAR_ITERATOR [G] inherit
        ITERATOR [G]
            redefine target end
    feature -- Access
        invariant_value: BOOLEAN is    
                -- Свойство, сопровождающее итерацию (по умолчанию: true)
            do
                Result:= True
            end
        target: LINEAR [G]
            -- Структура, к которой будут применяться компоненты итерации
        test: BOOLEAN is
            -- Булево условие выбора применимых элементов
            deferred
            end
    feature - Basic operations
        action is
            -- Действие на выбранных элементах
            deferred
            end
        do_if is
            -- Применить action в последовательности к каждому элементу
            --target, удовлетворяющему test.
            do
                from start invariant invariant_value until exhausted loop
                    if test then action end
                    forth
                end
            ensure then
                exhausted
            end
        ... И так далее: do_all, do_while, do_until и другие процедуры ...
    end

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

    class JUSTIFIER inherit
        LINEAR_ITERATOR [PARAGRAPH]
            rename
                action as justify,
                test as justifiable,
                do_all as justify_all
            end
    feature
        justify is
            do ... end
        justifiable is
                -- Подлежит ли абзац проверке?
            do
                Result := not preformated
            end
        ...
    end

    Переименование облегчает понимание. Заметьте, нет необходимости в объявлении или повторном объявлении процедуры justify_all (бывшей do_all ): будучи наследуемой, ожидаемая работа будет проделана эффективными версиями action и test.

    Процедура justify, вместо того чтобы быть описанной в классе, может наследоваться от другого родителя. В этом случае множественного наследования будет выполняться операция объединения ("join"), эффективизирующая отложенную action, наследуемую от одного родителя под именем justify (здесь переименование существенно), с эффективной justify, наследуемой от другого родителя. Реально, это и есть брак по расчету.

    Класс LINEAR_ITERATOR является замечательным примером класса поведения (behavior class), рассматривая общее поведение и оставляя открытыми специфические компоненты, так чтобы его потомки могли подключить специальные варианты.

    Формы льготного наследования

    Два примера, ASCII и LINEAR_ITERATOR, демонстрируют два главных варианта льготного наследования:

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

    Понимание льготного наследования

    Некоторые рассматривают льготное наследование как злоупотребление механизмом наследования, как некоторую форму хакерства.

    Главный вопрос, заслуживающий рассмотрения, связан не столько с наследованием, сколько с тем, как определены классы ASCII и LINEAR_ITERATOR. Как всегда, при рассмотрении проекта класса, следует спросить себя: "Действительно ли мы описали значимую абстракцию данных - множество объектов, характеризуемых абстрактными свойствами?"

    Для этих примеров ответ менее очевиден, чем для классов RECTANGLE, BANK_ACCOUNT или LINKED_LIST, но, по сути, он тот же:

  • Класс ASCII представляет абстракцию: "любой объект, имеющий доступ к свойствам множества ASCII ".
  • Класс LINEAR_ITERATOR представляет абстракцию: "любой объект, способный выполнять последовательные итерации на линейной структуре". Такой объект имеет тенденцию быть "абстрактной машиной", описанной в лекции 5.
  • Как только эти абстракции принимаются, наследственные связи не вызывают никаких проблем: экземпляру TOKENIZER необходим "доступ к свойствам множества ASCII", а экземпляр JUSTIFIER способен "выполнять последовательные итерации на линейной структуре". Фактически можно было бы классифицировать такие примеры как наследование подтипов. Что отличает льготное наследование, так это природа родителей. Эти классы являются исходными, не использующими наследование. И класс приложения может предпочесть быть их клиентом, а не наследником. Это утяжеляет подход, особенно для класса ASCII:

    charset: ASCII
    ...
    create charset

    При каждом использовании кода символа потребуется задавать целевой объект charset.Lower_a. Присоединяемый объект charset не играет никакой полезной роли. Те же комментарии справедливы и для класса LINEAR_ITERATOR. Но если классу необходимы несколько видов итерации, то тогда создание объектов-итераторов с собственными версиями action и test становится разумным.

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

    Множественные критерии и наследование видов

    Вероятно, наиболее трудная проблема наследования возникает при наличии альтернативных критериев классификации.

    Классификация при множественных критериях

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

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

  • по типу контракта: временные или постоянные работники;
  • по типу исполняемой работы: инженерная, административная, управленческая.
  • Оба эти критерия приводят к правильным классам-потомкам. При этом мы не впадаем в таксоманию, так как идентифицируемые классы, такие как TEMPORARY_EMPLOYEE по первому критерию и MANAGER по второму, действительно характеризуются специальными компонентами, не применимыми к другим категориям. Как же следует поступать?

    В первой попытке введем все варианты на одном и том же уровне (рис. 6.12).

    Для простоты на этой схеме имена классов сокращены. В реальной системе мы действуем более аккуратно и используем, как положено, полные имена, такие как PERMANENT_EMPLOYEE, ENGINEERING_EMPLOYEE и так далее.

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

    (рис 6.12) Беспорядочная классификация

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

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

    Появились два вида служащих. Заметьте, имя CONTRACT_EMPLOYEE не означает служащего, имеющего контракт, а служащего, характеризуемого контрактом (он может не иметь контракта!). Имя класса для другого вида означает "служащий, характеризуемый своей специальностью".

    То, что эти имена кажутся неестественными, отражает определенную сложность, характерную для наследования видов. При наследовании подтипов мы встречались с правилом, устанавливающим, что экземпляры наследников принадлежат непересекающимся подмножествам множества, заданного родителем. Здесь это правило неприменимо. Постоянный служащий имеет специальность и может быть инженером. Такая классификация подходит для дублирующего наследования: некоторые потомки классов, показанных на рисунке, будут иметь в качестве предков CONTRACT_EMPLOYEE и SPECIALTY_EMPLOYEE не напрямую, но через наследование от классов PERMANENT и ENGINEER. Такие классы будут дублируемыми потомками EMPLOYEE.

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

    (рис 6.13) Классификация, использующая виды

    Подходит ли нам наследование видов?

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

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

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

    Чтобы в нашем примере придерживаться единого критерия, мы могли бы принять решение, что тип работы служащего является более важным фактором, а статус контракта задать компонентом. Рассмотрим первую попытку введения в класс EMPLOYEE такого компонента:

    is_permanent: BOOLEAN

    Но такое решение накладывает серьезные ограничения. Расширяя возможности, приходим к варианту:

    Permanent: INTEGER is unique
    Temporary: INTEGER is unique
    Contractor: INTEGER is unique
    ...

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

    if is_permanent then ... else ... end

    или

    inspect
        contract_type
    when Permanent then
        ...
    when ...
        ...
    end

    Как неоднократно говорилось, разбор случаев приводит к ряду проблем при появлении новых вариантов и нарушает важные принципы непрерывности, единого выбора, открытость-закрытость и так далее. Вместо этого мы поставляем класс WORK_CONTRACT с отложенными компонентами, представляющими операции, зависящие от типа контракта, которые по-разному будут реализованы потомками. Большинству из этих компонентов будет необходим аргумент типа EMPLOYEE, представляющий служащего, к которому применяется операция, примерами операций могут быть hire (приглашение на работу) and terminate (увольнение).

    Результирующая структура показана на рис. 6.14.

    Эта схема, как вы заметили, почти идентична образцу проектирования с описателями, изучаемому ранее в этой лекции.

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

    (рис 6.14) Многокритериальная классификация с независимой иерархией, построенной для клиента

    Критерии для наследования видов

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

    Все же я нахожу наследование видов полезным при выполнении следующих трех условий:

  • Различные критерии классификации одинаково важны, так что выбор одного в качестве основного представляется спорным.
  • Многие возможные комбинации (такие как в примере: permanent supervisor, temporary engineer, permanent engineer и так далее) являются необходимыми.
  • Рассматриваемые классы настолько важны, что стоит потратить время на разработку лучшей из возможных структур наследования. Чаще всего речь идет в таких случаях о библиотечных классах повторного использования.
  • Примером приложения, удовлетворяющего этим критериям, является библиотека Base с ее структурой иерархии на верхних уровнях, описанная в последней лекции этой книги. Классы, полученные в результате этих усилий, в деталях описаны в [M 1994а]. Они построены в традиции естественных наук с применением таксономических принципов систематической классификации основных программистских структур. Верхняя часть этой иерархии выглядит так:

    (рис 6.15) Классификация, основанная на видах фундаментальных программистских структур

    Классификация на первом уровне ( BOX, COLLECTION, TRAVERSABLE ) основана на типах; уровень ниже (и многие другие, не показанные на рисунке) задают классификацию подтипов. Структура контейнера характеризуется тремя различными критериями:

  • COLLECTION определяет доступ к элементам. Класс SET позволяет определить сам факт присутствия элемента, в то время как BAG позволяет также посчитать число вхождений данного элемента. Дальнейшие уточнения включают такие абстракции доступа, как SEQUENCE (элементы доступны последовательно), STACK (элементы доступны в порядке, обратном их включению) и так далее.
  • BOX определяет представление элементов. Варианты включают конечные и бесконечные структуры. Конечные структуры могут быть ограниченными и не ограниченными. Ограниченные структуры могут быть фиксированными или изменяемого размера.
  • TRAVERSABLE определяет способы обхода структур.
  • Интересно отметить, что эта иерархия не начиналась, как иерархия видов. Начальная идея состояла в том, чтобы определить BOX, COLLECTION и TRAVERSABLE как несвязанные классы, каждый, задающий вершину своей независимой иерархии. Затем при описании реализации любой специальной структуры данных использовать множественное наследование с родителями из каждой иерархии. Например, связный список является конечным и неограниченным с последовательным доступом и линейным способом обхода.

    (рис 6.16) Построение структуры данных комбинированием абстракций путем множественного наследования

    Но затем мы осознали, что независимые семейства классов BOX, COLLECTION и TRAVERSABLE не лучший способ: им всем потребовались некоторые общие компоненты, в частности has (тест на проверку членства) и empty (тест на отсутствие элементов). Все это указывало на необходимость иметь общего родителя - CONTAINER, где эти общие свойства теперь и появляются. Следовательно, структура, изначально спроектированная как чистое множественное наследование с тремя непересекающимися иерархиями, превратилась в структуру с наследованием типов, приводящую к дублируемому наследованию.

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

    Как разрабатываются структуры наследования

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

    Специализация и абстракция

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

    Подобный комментарий, сделанный Майклом Джексоном, упоминался при рассмотрении проектирования сверху вниз.

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

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

  • точки в пространстве произвольной размерности, приводящие к наследственной структуре, где братьями класса POINT будут классы POINT_3D и так далее;
  • геометрические фигуры - другими классами структуры могут быть FIGURE, RECTANGLE, CIRCLE и так далее;
  • многоугольники - с такими классами, как QUADRANGLE (четыре вершины), TRIANGLE (три вершины) и SEGMENT (две вершины), POINT является специальным случаем, имеющим ровно одну вершину;
  • объекты, полностью определяемые двумя координатами - другими кандидатами являются комплексные числа и двумерные векторы COMPLEX и VECTOR_2D.
  • Интуитивно некоторые из этих обобщений кажутся более приемлемыми, чем другие, но невозможно со всей определенностью выбрать наилучшее. Ответ зависит от потребностей. Потому предусмотрительный и осторожный процесс, в котором абстракция создается с некоторым опозданием, чтобы точно убедиться в правильном выборе пути обобщения, может быть предпочтительнее скорых и быстрых решений, приводящих к непроверенной абстракции.

    Произвольность классификации

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

    Эта ситуация не является спецификой разработки ПО. Классификация Линнея не является универсально приемлемой или непреложной. У нее есть соперники, один из которых вместо традиционного эволюционного критерия использует другой, более индуктивный, основанный на ДНК-анализе и приводящий к совершенно другим результатам. Есть зоологи, для которых умение (неумение) птиц летать является важным таксономическим признаком, но официальная классификация с этим не соглашается.

    Индукция и дедукция

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

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

    Разнообразие абстракции

    Этот принцип атавизма - один из наиболее удивительных из всех 
    		атрибутов наследования.
    						Чарльз Дарвин

    Две формы апостериорного конструирования родителя являются общими и полезными.

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

    (рис 6.17) Абстракция

    То, что это понимание не пришло сразу, другими словами, то, что B был построен без учета A, - не является причиной отказа от наследования в этом случае. Сразу же при обнаружении необходимости A вы можете, а в большинстве случаев должны написать этот класс и адаптировать B как его наследника. Это не столь хорошо, как написать раньше A, но лучше, чем не написать вовсе.

    Факторизация возникает в случае обнаружения того, что два класса E и F фактически представляют варианты одного и того же понятия:

    (рис 6.18) Факторизация

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

    Независимость клиента

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

    (рис 6.19) Абстракция, факторизация и клиенты

    Когда B абстрагируется в A, или компоненты E факторизуются с компонентами F в D, класс X, представляющий клиента B или E (на рисунке он клиент обоих классов) в большинстве случаев не заметит никаких изменений. Включение класса в схему наследования не оказывает влияния на его клиентов, если они применяют компоненты класса на сущностях соответствующего типа. Другими словами, если X использует B и E как поставщиков по схеме:

    b1: B; e1: E
    ...
    b1.some_feature_of_B
    ...
    e1.some_feature_of_E

    то X не заметит, что B или E обрели родителей в результате абстрагирования или факторизации.

    Совершенствование уровня абстракции

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

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

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

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

    Прежде всего не следует бояться разнообразия способов использования наследования. Запрет множественного или льготного наследования не достигает никакой другой цели, кроме нанесения вреда самому себе. Механизмы должны помогать нам, используйте их правильно, но используйте их!

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

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

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

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

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

    Ключевые концепции

  • Каждое использование наследования должно отражать некоторую форму отношения "is a" между двумя категориями объектов, либо из внешней моделируемой области, либо из мира самого ПО.
  • Не используйте наследование для моделирования отношения "has"; это область клиентских отношений. (Помните о CAR_OWNER.)
  • Когда наследование применимо, часто потенциально применим и клиент. Если соответствующая точка зрения может измениться, используйте клиентское отношение. Если предвидится полиморфное использование, используйте наследование.
  • Не вводите промежуточных узлов наследования, если только они не задают хорошо определенные абстракции со своими специфическими компонентами.
  • Данная классификация наследования основана на двенадцати видах, разделенных на три общие категории: наследование модели (описывающее отношения, существующие в моделируемой области), программное наследование (описывающее отношения, существующие в самом ПО) и наследование вариаций (для адаптации класса либо в модели, либо в ПО).
  • Мощь наследования происходит из комбинации специализации типа и механизма расширения модуля. Было бы немудро использовать различные механизмы языка.
  • Наследование реализации и льготное наследование требуют особой тщательности, но представляют мощную технику, которую следует использовать на стороне поставщика.
  • Наследование видов - это тонкая техника, включающая дублирующее наследование, позволяющая классифицировать типы объектов по нескольким конкурирующим критериям. Оно полезно при разработке профессиональных библиотек. Во многих случаях простая техника описателей предпочтительнее.
  • Хотя и нет теоретического идеала, фактический процесс проектирования иерархии наследования идет в двух направлениях - от абстрактного к конкретному и наоборот.
  • Наследование является главным образом техникой поставщика.
  • Библиографические замечания

    Основным учебником по таксономии наследования является [Girod 1991]. Книга по ОО-методологии [Page-Jones 1995] - одна из немногих, где даются полезные методологические советы, включая ценные рекомендации, когда следует и когда не следует использовать наследование. Еще один полезный учебник Джона Мак-Грегора [McGregor 1992] исследует технику, названную в этой лекции наследованием видов.

    В [Breu 1995] также содержатся полезные концепции, основанные на более ограничительном подходе к наследованию, чем в рассматриваемой лекции.

    Техника, подобная "описателям" этой лекции, рассмотрена в [Gil 1994].

    При подготовке этой статьи использовались комментарии нескольких биологов, ведущих доступные в Интернете Web-ресурсы по таксономии живых существ, в частности "Древо жизни" (http://www.phylogeny.arizona.edu/tree/life.html), учтиво предоставленные профессорами Дэвидом Мэдисоном, а для птиц Майклом Лореном. Помощь профессора Эдвина Эверхама из университета Рэдфорда была весьма полезной.

    Руководства по теории классификации или систематике приведены в следующем разделе.

    Приложение: история таксономии

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

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

    Вероятно, подобно эволюционным таксономистам, хотелось бы взять понемногу от тех и от других.

    Упражнения

    У6.1 Стек, основанный на массиве

    Напишите полностью класс STACK и его потомка ARRAYED_STACK, набросок которого дан в этой лекции, используя технику "брака по расчету".

    У6.2 Метатаксономия

    Представьте себе, что введенная классификация форм наследования представляла бы собой иерархию наследования. Какие виды наследования были бы включены?

    У6.3 Стеки Ханоя

    (Это упражнение пришло из примера Филиппа Дрикса.)

    Рассмотрим отложенный класс STACK с процедурой put для вталкивания элемента на вершину с предусловием, включающим булеву функцию full (которая также может быть названа extendible ; когда вы ознакомитесь с упражнением, то заметите, что выбор имени может влиять на возможные решения).

    Задача о Ханойских башнях, известная по многим учебникам как пример рекурсивной процедуры, идет от работы Эдварда Лукаса, Париж, 1883 г.

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

    Можно ли определить класс HANOI_STACK, представляющий такие пирамиды, как наследника класса STACK? Если да, каким должен быть этот класс? Если нет, может ли HANOI_STACK как-то использовать STACK? Напишите класс полностью для различных возможных решений, обсудите все "за и против" каждого решения. Установите, какое из них предпочтительнее и объясните ваш выбор.

    У6.4 Являются ли многоугольники списками?

    Реализация нашего примера наследования класса POLYGON использовала атрибут связного списка vertices для представления числа сторон многоугольника. Следует ли вместо этого наследовать POLYGON от LINKED_LIST [POINT]?

    У6.5 Наследование функциональной вариации

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

    У6.6 Примеры классификации

    Для каждого из следующих случаев укажите, к какому виду наследования он относится:

  • SEGMENT от OPEN_FIGURE ;
  • COMPARABLE (объекты, поставляемые с отношением полного порядка), наследуемые от PART_COMPARABLE (объекты с отношением частичного порядка);
  • некоторые классы EXCEPTIONS.
  • У6.7 Кому принадлежат итераторы?

    Разумно ли компоненты итератора ( while_do и ему подобные) включать в классы, описывающие структуры данных, которые они итерируют, такие как LIST? Рассмотрите следующие аргументы:

  • простоту применения в процессе итерирования подпрограмм action и test, выбираемых приложением;
  • расширяемость: возможность добавления новых схем итерирования;
  • общность: выполнение ОО-принципов, в частности той идеи, что операции не существуют сами по себе, но связаны с некоторой абстракцией данных.
  • У6.8 Наследование типа и модуля

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

    У6.9 Наследование и полиморфизм

    Из рассмотренных видов наследования этой лекции между родителем A и наследником B, для каких на практике характерно использование полиморфного присоединения, другими словами присваивание x := y или соответствующая передача аргументов x типа A и y типа B?

    Страницы:

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

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

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

    Множественное наследование позволяет нескольким объектам выступать в роли базовых и поддерживается во многих языках (ссылка на первое издание этой книги [M1988]).

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

    Характеристики нескольких различных классов объектов

    (классы, уже хорошо!)

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

    (Нет, опять неудача.) Далее следует пример множественного наследования:

    например, пусть мы имеем класс объектов CAR, инкапсулирующий информацию об автомобиле, и класс PERSON, инкапсулирующий информацию о человеке. Мы можем использовать их для определения

    (неужели оправдаются наши наихудшие подозрения?)

    нового класса CAR-OWNER, комбинирующего атрибуты CAR и PERSON.

    (Они оправдались.) Нас приглашают рассматривать каждый объект CAR-OWNER не только как персону, но и как автомобиль. Для каждого, кто изучал наследование даже на элементарном уровне, это станет сюрпризом.

    Несомненно, вы понимаете, что второе отношение является клиентским, а не наследованием, владелец автомобиля является (is) персоной, но имеет (has) автомобиль.

    (рис 6.1) Походящая модель

    В формальной записи:

    class CAR_OWNER inherit
        PERSON
    feature
        my_car: CAR
        ...
    end

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

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

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

    (рис 6.2) Рисунок Джеффа Хокинга (голова его похожа на его же авто с открытыми дверцами)

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

    Правило: Наследование "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]! Что отсюда следует: слегка изменив точку зрения, можно представить свойство является как имеет.

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

    (рис 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.
  • Приложение: техника описателей

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

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

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

    Дадим обзор правильного использования наследования:

  • наследование подтипов (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 между соответствующими абстракциями, а в некоторых случаях вообще без адекватной абстракции.

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

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

    (рис 6.7) Классификация допустимых категорий наследования

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

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

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

    Следующие далее определения используют имя A для родительского класса и B для наследника:

    (рис 6.8) Соглашение именования при определении категорий наследования

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

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

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

    Определение: наследование подтипов

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

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

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

    ( ПОЗВОНОЧНЫЕ $$\gets$$ МЛЕКОПИТАЮЩИЕ и подобные примеры).

    Мы настаиваем, что родитель A должен быть отложенным, поскольку он описывает не полностью специфицированное множество объектов, в то время как наследник B может быть как эффективным, так и отложенным. Следующие две категории рассматривают ситуации, где A может быть эффективным классом.

    В одном из следующих разделов эта категория наследования будет рассмотрена детальнее, поскольку она не столь уж проста, как может показаться с первого взгляда.

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

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

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

    Типичным примером является: Прямоугольник $$\gets$$ Квадрат.

    Ограничением является утверждение: сторона1 = сторона2 (включается в инвариант класса Квадрат ).

    Многие математические примеры подпадают под эту категорию.

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

    Поскольку допускаются только концептуальные изменения класса A, добавляющие некоторые ограничения в класс B, то оба класса должны быть либо отложенными, либо эффективными.

    Наследование с ограничением концептуально близко к наследованию подтипов; последующее обсуждение создания подтипов (subtyping) будет относиться к обеим категориям.

    Наследование с расширением

    Определение: Наследование с Расширением

    Наследование с расширением применимо, когда B вводит компоненты, не представленные в A и не применимые к прямым экземплярам A. Класс A должен быть эффективным.

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

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

    class A feature a1: INTEGER end
    class B inherit
        A
    feature
        b1: REAL
    end

    Рассмотрим каждый экземпляр класса A как одноэлементное множество (которое можно записать как <n>, где n целое), а каждый экземпляр B - как пару, содержащую целое и вещественное (например, пару <1, -2.5> ). Множество пар MB не является подмножеством одноэлементного множества MA. Верно обратное, отношение "быть подмножеством" имеет место в обратном направлении, поскольку существует отображение один-к-одному между MA и множеством всех пар, имеющих данный второй элемент.

    Обнаружение того факта, что отношение "быть подмножеством" не выполняется, делает наследование расширением довольно подозрительным. Например, в ранней версии уважаемой ОО-библиотеки (не от ISE) класс RECTANGLE был наследником SQUARE, в отличие от изучаемого нами способа. Причина простая: класс SQUARE имеет атрибут side ; класс RECTANGLE наследует его, добавляя новый компонент other_side. Этот проект был подвергнут критике, он был пересмотрен с обращением наследования.

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

    Соответствующая схема используется и при разработке ОО-ПО. Конечно, класс SQUARE должен быть наследником RECTANGLE, а не наоборот, но можно предложить легитимные примеры. Класс MOVING_POINT (в приложениях кинематики) может наследовать от чисто графического класса POINT и добавлять компонент speed, описывающую величину и направление скорости. Другой пример, в текстовом процессоре класс CHAPTER может наследовать от DOCUMENT, добавляя специфические свойства - текущую позицию лекции в книге и процедуру ее сохранения.

    Подходящая математическая модель

    (Читатели - не математики могут пропустить этот раздел.)

    Для успокоения совести следует разрешить видимый парадокс, отмеченный выше (обнаружение того, что MB не является подмножеством MA ), так как мы хотим, чтобы некоторое отношение подмножества имело место между наследником и родителем. И это отношение реально существует, парадокс лишь показывает, что декартово произведение атрибутов не является подходящей моделью для моделирования класса. Рассмотрим класс:

    class C feature
        c1: T1
        c2: T2
        c3: T3
    end

    Мы не должны выбирать в качестве математической модели C' - множества экземпляров C - декартово произведение T'1 _ T'2 _ T'3, где знак штрих ' указывает на рекурсивное использование модели множеств, приводящее к парадоксу (наряду с другими недостатками).

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

  • A1 Функция определена для c1, c2 и c3.
  • A2 Множество VALUE (множество цели для функции) является супермножеством $$T'1 \cup T'2 \cup T'3$$.
  • A3 Значения функции для c1 лежат в T'1 и так далее.
  • Тогда, если вспомнить, что функция является специальным случаем отношения и что отношение является множеством пар (например, в ранее упоминаемом случае экземпляр класса A может быть промоделирован функцией {<a1, 25>}, а экземпляр класса B - {<a1, 1>, <b1, -2.5>} ), мы получаем ожидаемое свойство - B' является подмножеством A'. Заметьте, здесь уже элементы обоих множеств являются парами и первая функция задает все возможные отображения второго атрибута.

    Заметьте также, что принципиально важно установить свойство A1 как "Функция определена для...", но не в виде "Областью определения функции является...", что ограничивало бы область множеством {c1, c2 c3}, не позволяя потомкам добавлять свои собственные атрибуты. Как результат такого подхода, каждый программный объект моделируется неограниченным числом математических объектов.

    Это обсуждение дает только схему математической модели. С деталями использования частичных функций для моделирования кортежей и общими математическими основами можно ознакомиться в [M 1990].

    Наследование вариаций

    (Читатели - не математики, добро пожаловать!) Перейдем теперь ко второму семейству категорий - наследованию вариаций.

    Определение: Наследование вариаций типа и функций

    Наследование вариаций применяется, если B переопределяет некоторые компоненты A ; A и B являются оба либо отложенными, либо эффективными. Класс B не должен вводить никаких новых компонентов за исключением тех, что непосредственно необходимы переопределяемым компонентам. Здесь рассматриваются два случая:

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

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

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

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

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

    perpendicular: SEGMENT is
            -- Сегмент, повернутый на 90 градусов
        ...

    Затем определим наследника DOTTED_SEGMENT, дающего графическое представление пунктирными, а не непрерывными линиями. В этом классе perpendicular должен возвращать результат типа DOTTED_SEGMENT, так что необходимо переопределить тип. Этого бы не требовалось, если бы изначально результат объявлялся как like Current. Так что, будь у вас доступ к источнику и его автору, можно было бы предложить модифицировать оригинал, не нанося ущерба существующим клиентам. Но если нет возможности модифицировать оригинал или по ряду причин закрепленное объявление не подходит оригиналу (вероятно, из-за потребностей других потомков), то возможность переопределить тип может стать палочкой-выручалочкой.

    При наследовании функциональной вариации изменяются тела некоторых компонентов. Если, как обычно в таком случае, компонент уже эффективен, то это означает изменение реализации. Спецификация компонента, заданная утверждением, также может измениться. Также возможно, хотя и реже встречается, иметь функциональную вариацию между двумя отложенными классами, в этом случае будут меняться только утверждения. Это может повлечь изменения в некоторых функциях, отложенных или эффективных, связанных с утверждениями, или даже добавление новых компонентов.

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

    Отмена эффективизации

    Определение: Наследование с Отменой эффективизации

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

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

  • При множественном наследовании сливаются компоненты, наследуемые от двух различных родителей. Если один из них отложенный, а другой эффективный, то слияние произойдет автоматически при условии совпадения имен (возможно после переименования), эффективная версия будет определять реализацию. Но если обе версии эффективны, то следует провести потерю эффективизации одной версии, отдавая предпочтение другой версии.
  • Хотя абстракция соответствует потребностям, но повторно используемый класс слишком конкретен для наших целей. Отмена эффективизации позволит удалит нежеланную реализацию. Перед использованием этого решения следует рассмотреть альтернативу - реорганизовать иерархию наследования, сделав более конкретный класс наследником нового отложенного класса. По понятным причинам это не всегда возможно, например из-за отсутствия доступа к исходному коду. Отмена эффективизации может в таких случаях обеспечить полезную форму обобщения.
  • Для этой категории наследования класс B будет отложенным; A обычно эффективным, но может быть частично отложенным.

    Наследование с конкретизацией

    Перейдем теперь к третьей и последней группе категорий - программному наследованию.

    Определение: Наследование с конкретизацией

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

    Примером, используемым многократно в предыдущих лекциях, является отложенный класс TABLE, описывающий таблицы самой общей природы. Конкретизация ведет к потомкам SEQUENTIAL_ TABLE и HASH_TABLE, все еще отложенным. Заключительная конкретизация SEQUENTIAL_TABLE приводит к эффективным классам ARRAYED_TABLE, LINKED_TABLE, FILE_TABLE.

    Термин "конкретизация" (reification), введенный Георгом Лукасом, происходит от латинского слова, означающего "превращение в вещь". Он используется в спецификациях и методе разработки VDM.

    Структурное наследование

    Определение: структурное наследование

    Структурное наследование применяется, если A, отложенный класс, представляет общее структурное свойство, а B, который может быть отложенным или эффективным, представляет некоторый тип объектов, обладающих этим свойством.

    Обычно A представляет математическое свойство, которым может обладать некоторое множество объектов. Например, A может быть классом COMPARABLE, поставляемым с такими операциями, как infix "<" и infix ">=", представляющим объекты с заданным отношением полного порядка. Класс, которому необходимо отношение порядка, такой как STRING, становится наследником COMPARABLE.

    Наследование таким способом от нескольких родителей является обычным приемом. Например, класс INTEGER в библиотеке Kernel наследует от COMPARABLE и класса NUMERIC (с такими компонентами, как infix "+" и infix "*" ), задающего арифметические свойства. (Класс NUMERIC более точно представляет математическое понятие кольца.)

    В чем разница между конкретизацией и структурным наследованием? При конкретизации B представляет то же понятие, что и A, отличаясь большей степенью реализации; при структурном наследовании B представляет собственную абстракцию, для которой A задает лишь один из аспектов, такой как порядок на объектах или наличие арифметических операций.

    Валден и Нерсон заметили, что новички иногда верят, что они используют подобную форму наследования, подменяя фактически имеющее место отношение "is" вариантом схемы "car-owner" ( AIRPLANE наследуется от VENTILATION_SYSTEM ). Они указывают, что этой ошибки просто избежать благодаря абсолютному критерию, не оставляющему места для сомнений или двусмысленности:

    При схеме наследования, хотя наследуемые свойства являются вторичными, они все же являются свойствами всего объекта, описываемого классом. Если мы делаем AIRPLANE наследником COMPARABLE , то отношение порядка применимо к каждому самолету как к целому, но свойства VENTILATION_SYSTEM не таковы. Компонент stop VENTILATION_SYSTEM не прекращает полет самолета.

    Заключение в этом примере очевидно: AIRPLANE должен быть клиентом, а не наследником класса VENTILATION_SYSTEM.

    Наследование реализации

    Определение: Наследование Реализации

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

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

    Случай наследования константных атрибутов и однократных функций покрывается следующим вариантом.

    Льготное наследование

    Льготное наследование является схемой, в которой родитель представляет коллекцию полезных компонентов, предназначенных для использования его потомками.

    Определение: Льготное наследование

    Льготное наследование применяется, если A существует единственно в целях обеспечения множества логически связанных компонентов, дающих преимущества его потомкам, таким как B. Двумя общими вариантами являются:

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

    Иногда, как в примерах, которые появятся чуть позже, при льготном наследовании используется только один вариант - константы или абстрактная машина, но в других случаях, как для класса EXCEPTIONS, родительский класс предоставляет как константы (такие как коды исключений Incorrect_inspect_value ), так и подпрограммы (такие как trigger для возбуждения исключения разработчика). Так как при нашем обсуждении категории наследования рассматриваются как непересекающиеся, то льготное наследование с двумя пересекающимися вариантами рассматривается как одна категория.

    При наследовании констант как A, так и B являются эффективными. При наследовании абстрактной машины ситуация более гибкая, но B должно быть, по меньшей мере, столь же эффективно как и A.

    В деталях льготное наследование еще будет обсуждаться в данной лекции.

    Использование наследования с отложенными и эффективными классами

    В следующей таблице обобщены правила, определяющие для каждой категории, должны ли родитель и его потомок быть эффективными или отложенными классами. "Вариация" покрывает случаи вариации типа и функциональной вариации. Элементы, помеченные символом *, появляются более чем в одном входе.

    Отложенные и эффективные наследники и их родители
    ПотомокРодитель
    ОтложенныйЭффективный
    Отложенный

    Константы*

    Ограничение*

    Структура*

    Тип*

    Потеря эффективизации*

    Вариация*

    Вид

    Расширение*

    Потеря эффективизации*

    Эффективный

    Константы*

    Конкретизация

    Структура*

    Тип*

    Константы*

    Расширение*

    Реализация

    Ограничение*

    Вариация*

    Один механизм, или несколько?

    Заметьте, это обсуждение предполагает в качестве основы раннюю презентацию, определяющую смысл наследования (см. лекцию 14 курса "Основы объектно-ориентированного программирования").

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

    Такое разделение нанесло бы больше вреда, чем принесло пользы. Вот несколько доводов.

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

    Практическим следствием были бы бесполезные методологические обсуждения: предположим, вы хотите наследовать от класса итератора, такого как LINEAR_ITERATOR ; следует ли использовать наследование модуля или наследование типа? Можно приводить аргументы в защиту одного и другого решения. Вклад этого предложения в критерий качества нашего ПО и скорость его создания будут фактически нулевыми.

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

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

  • Переопределение полезно как для подтипов (вспомните RECTANGLE, переопределяющий perimeter от POLYGON ) и для расширения модуля (принцип Открыт-Закрыт требует при наследовании модуля сохранения гибкости изменений, без чего будет потеряно одно из главных преимуществ ОО-метода).
  • Переименование полезно при наследовании модуля. Полагать его неподходящим при наследовании типа (см. [Breu 1995]) представляется серьезным ограничением. При моделировании внешней системы варианты некоторого понятия могут вводить специальную терминологию, которую желательно сохранить в ПО. Класс STATE_INSTITUTIONS в географической или выборной информационной системе может иметь потомка LOUISIANA_INSTITUTIONS, отражающего особенности политической структуры штата Луизиана, поэтому вполне ожидаемо желание потомка переименовать компонент counties, задающий список округов штатов, в parishes - имя, используемое для округа в данном штате.
  • Дублируемое наследование может встретиться для любой из форм. Так как можно ожидать, что только наследование модуля сохранит полиморфную подстановку, то при наследовании типов тут же возникнет необходимость разбора случаев и предложения select со всеми недостатками при появлении новых случаев. Появляются и другие вопросы - когда разделять компоненты, а когда их дублировать.
  • При введении в язык новых механизмов они взаимодействуют друг с другом и с другими механизмами языка. Должны ли мы защитить класс от совместного наследования и модуля, и типа? Если да, то будут возмущены разработчики, использующие класс двумя возможными способами, если нет, мы откроем ящик Пандоры, грозящий появлением множества проблем - конфликтов имен, переопределений и так далее.
  • Все это ради преимуществ пуристской точки зрения - ограниченной и спорной. Нет ничего плохого в защите спорной точки зрения, но следует быть крайне осторожным в нововведениях и учитывать их последствия для пользователей языка. И снова примером может служить Эдсгар Дейкстра в исследовании goto. Он не только в деталях объяснил все недостатки этой инструкции, основываясь на теории конструирования ПО и процесса его выполнения, но и показал, как можно без труда заменить этот механизм. В данном же случае убедительные аргументы не представлены, по крайней мере, я не увидел, почему "плохо" иметь единый механизм, покрывающий как наследование модулей, так и наследование типа.

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

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

    Наследование подтипов и скрытие потомков

    Первая категория наследования из нашего списка, вероятно, единственная, с которой согласится каждый, по меньшей мере, тот, кто принимает наследование: то, что мы можем назвать чистым наследованием подтипов (типов).

    Большая часть данного обсуждения применима и к наследованию с ограничением, чье принципиальное отличие состоит в том, что от родителя не требуется быть отложенным классом.

    Определение подтипа

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

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

  • FIGURE $$\gets$$ CLOSED_FIGURE $$\gets$$ POLYGON $$\gets$$ QUADRANGLE $$\gets$$ RECTANGLE $$\gets$$ SQUARE
  • DEVICE $$\gets$$ FILE $$\gets$$ TEXT_FILE
  • SHIP $$\gets$$ LEISURE_SHIP $$\gets$$ SAILBOAT
  • ACCOUNT $$\gets$$ SAVINGS_ACCOUNT $$\gets$$ FIXED_RATE_ACCOUNT
  • В любом из этих случаев четко идентифицируется множество объектов, описываемое родительским типом, и мы выделяем подмножество, характеризуемое некоторыми свойствами, которыми обладают не все объекты родителя. Например, текстовый файл является файлом со специальными свойствами, вытекающими из того, что элементы файла являются строками текста, что нехарактерно, например, для бинарных файлов.

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

    Некоторые из примеров, такие как RECTANGLE $$\gets$$ SQUARE, возможно, включают эффективного родителя и потому представляют случаи наследования с ограничением.

    Различные взгляды

    Наследование подтипов кажется простым, когда существует четкий критерий классификации вариантов определенного понятия. Но иногда некоторые качества начинают конкурировать между собой. Даже в, казалось бы, совсем простом случае классификации многоугольников могут возникать сомнения: следует ли использовать для классификации число сторон, создавая такие классы, как TRIANGLE, QUADRANGLE etc., или следует разделять объекты на правильные многоугольники ( EQUILATERAL_POLYGON, SQUARE и т. д.) и неправильные?

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

    Взгляд на подтипы

    Тип - это не просто множество объектов. Он характеризуется также применимыми операциями (компонентами) и их семантическими свойствами (утверждениями: предусловиями, постусловиями, инвариантами). Мы предполагаем, что компоненты и утверждения наследника совместимы с концепцией подтипа, означая, что любой экземпляр наследника должен рассматриваться также как экземпляр родителя.

    Правила, применяемые к утверждениям, поддерживают этот взгляд на подтипы:

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

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

    Необходимость скрытия потомком

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

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

    Альтернативы обсуждались при рассмотрении основополагающего принципа Открыт-Закрыт и они не кажутся привлекательными:

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

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

    Рассмотрим класс ELLIPSE. Эллипс имеет два фокуса, через которые можно провести прямую:

    (рис 6.9) Эллипс и фокусная линия

    Класс ELLIPSE может соответственно иметь компонент focus_line.

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

    (рис 6.10) Круг и его центр

    Хороший ли это пример для скрытия потомком? Должен ли класс CIRCLE сделать компонент focus_line закрытым, как здесь:

    class CIRCLE inherit
        ELLIPSE
            export {NONE} focus_line end
        ...

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

    focus_line is
            -- Линия, проходящая через два фокуса
        require
            not equal (focus_1, focus_2)
        do
            ...
        end

    (Предусловие может быть абстрактным, использующим функцию distinct_focuses ; с тем преимуществом, что класс CIRCLE может сам переопределить ее.)

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

    Приложения скрытия потомком

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

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

    Рассмотрим иерархию с корневым классом MORTGAGE ( ЗАКЛАДНАЯ ). Потомки организуются в соответствии с различными критериями, такими как фиксированная или переменная ставка, деловая или персональная, любыми другими. Для простоты будем полагать, что речь идет о таксономии - чистом случае подтипов. Класс MORTGAGE имеет процедуру redeem (выплачивать долг по закладной), управляющей выплатами по закладной в некоторый период, предшествующий сроку оплаты.

    Теперь предположим, что Конгресс в порыве великодушия (или под давлением лоббистов) ввел новую форму закладных, субсидируемых правительством, чьи преимущества одновременно предполагают запрет досрочных выплат. В иерархии классов найдется место для класса NEW_MORTGAGE ; но что делать с процедурой redeem?

    Можно было бы использовать технику предусловий, как в случае с focus_line. Но что, если банкиру никогда не приходилось иметь дело с закладными, по которым нельзя платить досрочно? Тогда, вероятно, процедура redeem не будет иметь предусловия.

    Так что использование предусловия потребует модификации класса MORTGAGE со всеми вытекающими последствиями. Предположим, однако, что в данном случае проблем с модификацией не будет, и мы добавим в класс булеву функцию redeemable и предусловие к redeem:

    require
        redeemable

    Но тем самым мы изменили интерфейс класса. Все клиенты класса и их бесчисленные потомки мгновенно стали потенциально некорректными. Все их вызовы m.redeem (...) должны быть теперь переписаны как:

    if m.redeemable then
        m.redeem (...)
    else
        ... (Кто в мире мог предвидеть это?)...
    end

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

    Отсутствие предусловия в исходной версии redeem не является ошибкой проектирования. В период создания проекта каждая закладная допускала досрочные выплаты. Мы не можем требовать предусловий для каждого компонента, иначе до конца своих дней придется каждый вызов предварять тестом if.

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

    class NEW_MORTGAGE inherit
        MORTGAGE
            export {NONE} redeem end
        ...

    Ни ошибок, ни аномалий не появится в существующем ПО. Если кто-то модифицирует класс клиента, добавив класс с новыми закладными:

    m: MORTGAGE; nm: NEW_MORTGAGE
    ...
    m := nm
    ...
    m.redeem (...)

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

    Таксономии и их ограничения

    Исключения таксономии не являются спецификой программистских примеров. В большей степени они характерны для естественных наук, где почти невозможно найти утверждение в форме "члены ABC phylum [или genus, species etc.] характеризуются свойством XYZ ", которому не предшествовало бы " большинство ", " обычно " или за которым не следовало бы " за исключением нескольких случаев ". Это справедливо на всех уровнях иерархии, даже для наиболее фундаментальных категорий, для которых, казалось бы, существуют бесспорные критерии!

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

    Отличие растений от животных

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

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

    Пища. Зеленые растения, содержащие хлорофилл, сами производят еду для себя, большинство животных питаются растениями или поедают других животных.

    Рост. Растения обычно растут от концов своих ветвей и корней и от внешних участков ствола в течение всей жизни. У животных рост обычно идет во всех частях их тела и прекращается при достижении зрелости.

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

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

    Известный пример из зоологии, ставший уже клише, иллюстрирует таксономию исключений. (Помните, однако, что это только аналогия, а не программистский пример.) Птицы летают. (Класс BIRD должен иметь процедуру fly.) Страус - птица, но страус не летает. При создании наследника - класса OSTRICH - необходимо будет указать, что эта самая большая из птиц не летает.

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

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

    В программистских терминах класс OSTRICH будет наследником BIRD, скрывая наследуемый компонент fly.

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

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

    Практика разработки ПО и аналогии природного мира свидетельствуют, что даже при самом тщательном проектировании остаются исключения таксономии. Скрытие redeem класса NEW_MORTGAGE или fly из OSTRICH не является свидетельством небрежного проектирования или недостаточного предвидения, оно свидетельствует о реальной сложности иерархии наследования.

    Такие исключения таксономии имеют прецеденты, насчитывающие столетние усилия интеллектуальных гигантов (включая Аристотеля, Линнея, Бюффона и Дарвина). Они сигнализируют о внутренних ограничениях человеческой способности познания мира. Связаны ли они с результатами, шокирующими научную мысль в двадцатом столетии - принципом неопределенности в физике, неразрешимыми проблемами в математике?

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

    Наследование реализации

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

    Брак по расчету

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

    class ARRAYED_STACK [G] inherit
        STACK [G]
            redefine change_top end
        ARRAY [G]
            rename
                count as capacity, put as array_put
            export
                {NONE} all
            end
    feature
        ... Реализация отложенных программ STACK, таких как put, count, full...
        ... и переопределение change_top в терминах операций ARRAY ...
    end

    Интересно сравнить представленную схему класса ARRAYED_STACK с классом STACK2 из предыдущих обсуждений (см. лекцию 11 курса "Основы объектно-ориентированного программирования") - реализацию стека массивом, но без использования наследования. Заметьте, устранение необходимости быть клиентом ARRAY упрощает нотацию (предыдущая версия должна была использовать вызов в форме implementation.put, теперь можно писать просто put ).

    При наследовании все компоненты ARRAY были сделаны закрытыми. Это типично при браках по расчету: все компоненты родителя, обеспечивающего спецификацию, здесь STACK, экспортируются; все компоненты родителя, обеспечивающего реализацию, здесь ARRAY, скрываются. Это вынуждает клиентов класса ARRAYED_STACK использовать соответствующие экземпляры только через компоненты стека.

    Это выглядит привлекательно, но правильно ли это?

    Наследование реализации подвергается критике. Скрытие компонентов кажется нарушением отношения "is-a".

    Это не так. У этого отношения есть разные формы. По своему поведению стек, основанный на массиве, ведет себя как стек. По внутреннему представлению он массив, и экземпляры ARRAYED_STACK отличаются от экземпляров ARRAY лишь обогащением за счет атрибута ( count ). Экземпляры, создаваемые по единому образцу, представляют достаточно строгую форму отношения "is-a". И дело не только в представлении: все компоненты ARRAY, такие как put (переименованный в array_put ), infix "@" и count (переименованный capacity ), доступны для ARRAYED_STACK, хотя и не экспортируются его клиентам. Классу они необходимы для реализации компонентов STACK.

    Так что концептуально ничего ошибочного нет в наследовании в интересах реализации. Показательно сравнение с контрпримером, изучаемым в начале этой лекции, класс CAR_OWNER свидетельство непонимания концепции наследования, класс ARRAYED_STACK задает хорошо определенную форму отношения "is-a".

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

    Как это делается без наследования

    Давайте проверим, как можно выполнить эту работу без использования наследования. Для нашего примера это было уже сделано в классе STACK2 из предыдущих лекций. Он имеет атрибут representation типа ARRAY [G] и процедуры стека, реализованные в следующем виде (утверждения опущены):

    put (x: G) is
            -- Добавляет x на вершину
        require
            ...
        do
            count := count + 1
            representation.put (count, x)
        ensure
            ...
        end

    Каждая манипуляция с представлением требует вызова компонента ARRAY с representation как цели. Платой являются потери производительности: минимальные по памяти (атрибут representation ), более серьезные по времени (связанные с representation, накладные расходы, добавляемые при вызове каждой операции).

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

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

    indexing
        description: "Объекты, имеющие доступ к массиву и его операциям"
    class
        ARRAYED [G]
    feature -- Access
        item (i: INTEGER): G is
                -- Элемент представления с индексом i
            require
                ...
            do
                Result := representation.item (i)
            ensure
                ...
            end
    feature -- Element change
        put (x: G; i: INTEGER) is
                -- Замена на x элемента с индексом i
            require
                ...
            do
                representation.put (x, i)
            ensure
                ...
            end
    feature {NONE} -- Implementation
        representation: ARRAY [G]
    end

    Компоненты item и put экспортированы. Так как ARRAYED описывает только внутренние свойства структуры данных, нет реальной необходимости в экспортируемых компонентах. Так что тот, кто не согласен с самой идей разрешения потомкам скрывать некоторые из экспортируемых компонентов, может предпочесть сделать закрытыми все компоненты ARRAYED. По умолчанию они тогда будут скрытыми и у потомков.

    При таком определении класса не вызывает споров, что классы, такие как ARRAYED_STACK или ARRAYED_LIST, становятся наследниками ARRAYED: они действительно описывают структуры на массивах. Эти классы могут теперь использовать item вместо representation.item и так далее; мы избавились от утомительного повторения.

    Но минуточку! Если наследовать от ARRAYED представляется правильным, почему же нельзя непосредственно наследовать от ARRAY? Никакой выгоды от введения еще одного слоя, надстроенного над ARRAY. Введение ARRAYED позволило убедить себя, что наследование реализации не используется, но по соображениям практики мы пошли на это, сделав систему более сложной и менее эффективной.

    На самом деле нет никаких причин для введения класса ARRAYED. Прямое наследование реализации от классов, подобных ARRAY, проще и легитимнее.

    Льготное наследование

    При льготном наследовании мы еще менее щепетильны, чем при наследовании реализации. Чистый расчет руководит нами при вступлении в брак. Мы видим класс с полезными свойствами и хотим использовать его. Здесь нет ничего предосудительного, поскольку таково назначение класса.

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

    Библиотека Base Libraries включает класс ASCII:

    indexing
        description:
        "Множество символов ASCII. %
        %Этот класс - предок всех классов, нуждающихся в его свойствах."
    class ASCII feature -- Access
        Character_set_size: INTEGER is 128; Last_ascii: INTEGER is 127
        First_printable: INTEGER is 32; Last_printable: INTEGER is 126
        Letter_layout: INTEGER is 70
        Case_diff: INTEGER is 32
            -- Lower_a - Upper_a
        ...
        Ctrl_a: INTEGER is 1; Soh: INTEGER is 1
        Ctrl_b: INTEGER is 2; Stx: INTEGER is 2
        ...
        Blank: INTEGER is 32; Sp: INTEGER is 32
        Exclamation: INTEGER is 33; Doublequote: INTEGER is 34
        ...
        ...
        Upper_a: INTEGER is 65; Upper_b: INTEGER is 66
        ...
        Lower_a: INTEGER is 97; Lower_b: INTEGER is 98
        ... и т.д. ...
    end

    Этот класс является хранилищем множества константных атрибутов (всего 142 компонента), описывающих свойства множества ASCII.

    Рассмотрим, например, лексический анализатор, ответственный за идентификацию лексем входного текста. Лексемами текста, написанного на некотором языке программирования, являются целые, идентификаторы, символы и так далее. Одному из классов системы, скажем, TOKENIZER, необходим доступ к кодам символов для их классификации на цифры, буквы и т. д. Такой класс воспользуется льготами и наследует эти коды от ASCII:

    class TOKENIZER inherit ASCII feature
        ... Программы класса могут использовать компоненты Blank, Case_diff и другие...
    end

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

    Итераторы

    Второй пример демонстрирует наследование программ общего вида, а не константных атрибутов.

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

    С первого взгляда может показаться, что итерирующие компоненты должны принадлежать классам, описывающих соответствующие структуры данных, таким как LIST или SEQUENCE.

    В упражнении У6.7 предлагается показать, что это неправильное решение.

    Предпочтительнее ввести независимую иерархию итераторов, показанную на рис. 6.11.

    (рис 6.11) Иерархия итераторов

    Класс LINEAR_ITERATOR, один из наиболее интересных классов в этом обсуждении, выглядит так:

    indexing
        description:
            "Объекты, допускающие итерирование на линейных структурах"
        names: iterators, iteration, linear_iterators, linear_iteration
    deferred class LINEAR_ITERATOR [G] inherit
        ITERATOR [G]
            redefine target end
    feature -- Access
        invariant_value: BOOLEAN is    
                -- Свойство, сопровождающее итерацию (по умолчанию: true)
            do
                Result:= True
            end
        target: LINEAR [G]
            -- Структура, к которой будут применяться компоненты итерации
        test: BOOLEAN is
            -- Булево условие выбора применимых элементов
            deferred
            end
    feature - Basic operations
        action is
            -- Действие на выбранных элементах
            deferred
            end
        do_if is
            -- Применить action в последовательности к каждому элементу
            --target, удовлетворяющему test.
            do
                from start invariant invariant_value until exhausted loop
                    if test then action end
                    forth
                end
            ensure then
                exhausted
            end
        ... И так далее: do_all, do_while, do_until и другие процедуры ...
    end

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

    class JUSTIFIER inherit
        LINEAR_ITERATOR [PARAGRAPH]
            rename
                action as justify,
                test as justifiable,
                do_all as justify_all
            end
    feature
        justify is
            do ... end
        justifiable is
                -- Подлежит ли абзац проверке?
            do
                Result := not preformated
            end
        ...
    end

    Переименование облегчает понимание. Заметьте, нет необходимости в объявлении или повторном объявлении процедуры justify_all (бывшей do_all ): будучи наследуемой, ожидаемая работа будет проделана эффективными версиями action и test.

    Процедура justify, вместо того чтобы быть описанной в классе, может наследоваться от другого родителя. В этом случае множественного наследования будет выполняться операция объединения ("join"), эффективизирующая отложенную action, наследуемую от одного родителя под именем justify (здесь переименование существенно), с эффективной justify, наследуемой от другого родителя. Реально, это и есть брак по расчету.

    Класс LINEAR_ITERATOR является замечательным примером класса поведения (behavior class), рассматривая общее поведение и оставляя открытыми специфические компоненты, так чтобы его потомки могли подключить специальные варианты.

    Формы льготного наследования

    Два примера, ASCII и LINEAR_ITERATOR, демонстрируют два главных варианта льготного наследования:

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

    Понимание льготного наследования

    Некоторые рассматривают льготное наследование как злоупотребление механизмом наследования, как некоторую форму хакерства.

    Главный вопрос, заслуживающий рассмотрения, связан не столько с наследованием, сколько с тем, как определены классы ASCII и LINEAR_ITERATOR. Как всегда, при рассмотрении проекта класса, следует спросить себя: "Действительно ли мы описали значимую абстракцию данных - множество объектов, характеризуемых абстрактными свойствами?"

    Для этих примеров ответ менее очевиден, чем для классов RECTANGLE, BANK_ACCOUNT или LINKED_LIST, но, по сути, он тот же:

  • Класс ASCII представляет абстракцию: "любой объект, имеющий доступ к свойствам множества ASCII ".
  • Класс LINEAR_ITERATOR представляет абстракцию: "любой объект, способный выполнять последовательные итерации на линейной структуре". Такой объект имеет тенденцию быть "абстрактной машиной", описанной в лекции 5.
  • Как только эти абстракции принимаются, наследственные связи не вызывают никаких проблем: экземпляру TOKENIZER необходим "доступ к свойствам множества ASCII", а экземпляр JUSTIFIER способен "выполнять последовательные итерации на линейной структуре". Фактически можно было бы классифицировать такие примеры как наследование подтипов. Что отличает льготное наследование, так это природа родителей. Эти классы являются исходными, не использующими наследование. И класс приложения может предпочесть быть их клиентом, а не наследником. Это утяжеляет подход, особенно для класса ASCII:

    charset: ASCII
    ...
    create charset

    При каждом использовании кода символа потребуется задавать целевой объект charset.Lower_a. Присоединяемый объект charset не играет никакой полезной роли. Те же комментарии справедливы и для класса LINEAR_ITERATOR. Но если классу необходимы несколько видов итерации, то тогда создание объектов-итераторов с собственными версиями action и test становится разумным.

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

    Множественные критерии и наследование видов

    Вероятно, наиболее трудная проблема наследования возникает при наличии альтернативных критериев классификации.

    Классификация при множественных критериях

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

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

  • по типу контракта: временные или постоянные работники;
  • по типу исполняемой работы: инженерная, административная, управленческая.
  • Оба эти критерия приводят к правильным классам-потомкам. При этом мы не впадаем в таксоманию, так как идентифицируемые классы, такие как TEMPORARY_EMPLOYEE по первому критерию и MANAGER по второму, действительно характеризуются специальными компонентами, не применимыми к другим категориям. Как же следует поступать?

    В первой попытке введем все варианты на одном и том же уровне (рис. 6.12).

    Для простоты на этой схеме имена классов сокращены. В реальной системе мы действуем более аккуратно и используем, как положено, полные имена, такие как PERMANENT_EMPLOYEE, ENGINEERING_EMPLOYEE и так далее.

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

    (рис 6.12) Беспорядочная классификация

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

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

    Появились два вида служащих. Заметьте, имя CONTRACT_EMPLOYEE не означает служащего, имеющего контракт, а служащего, характеризуемого контрактом (он может не иметь контракта!). Имя класса для другого вида означает "служащий, характеризуемый своей специальностью".

    То, что эти имена кажутся неестественными, отражает определенную сложность, характерную для наследования видов. При наследовании подтипов мы встречались с правилом, устанавливающим, что экземпляры наследников принадлежат непересекающимся подмножествам множества, заданного родителем. Здесь это правило неприменимо. Постоянный служащий имеет специальность и может быть инженером. Такая классификация подходит для дублирующего наследования: некоторые потомки классов, показанных на рисунке, будут иметь в качестве предков CONTRACT_EMPLOYEE и SPECIALTY_EMPLOYEE не напрямую, но через наследование от классов PERMANENT и ENGINEER. Такие классы будут дублируемыми потомками EMPLOYEE.

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

    (рис 6.13) Классификация, использующая виды

    Подходит ли нам наследование видов?

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

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

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

    Чтобы в нашем примере придерживаться единого критерия, мы могли бы принять решение, что тип работы служащего является более важным фактором, а статус контракта задать компонентом. Рассмотрим первую попытку введения в класс EMPLOYEE такого компонента:

    is_permanent: BOOLEAN

    Но такое решение накладывает серьезные ограничения. Расширяя возможности, приходим к варианту:

    Permanent: INTEGER is unique
    Temporary: INTEGER is unique
    Contractor: INTEGER is unique
    ...

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

    if is_permanent then ... else ... end

    или

    inspect
        contract_type
    when Permanent then
        ...
    when ...
        ...
    end

    Как неоднократно говорилось, разбор случаев приводит к ряду проблем при появлении новых вариантов и нарушает важные принципы непрерывности, единого выбора, открытость-закрытость и так далее. Вместо этого мы поставляем класс WORK_CONTRACT с отложенными компонентами, представляющими операции, зависящие от типа контракта, которые по-разному будут реализованы потомками. Большинству из этих компонентов будет необходим аргумент типа EMPLOYEE, представляющий служащего, к которому применяется операция, примерами операций могут быть hire (приглашение на работу) and terminate (увольнение).

    Результирующая структура показана на рис. 6.14.

    Эта схема, как вы заметили, почти идентична образцу проектирования с описателями, изучаемому ранее в этой лекции.

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

    (рис 6.14) Многокритериальная классификация с независимой иерархией, построенной для клиента

    Критерии для наследования видов

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

    Все же я нахожу наследование видов полезным при выполнении следующих трех условий:

  • Различные критерии классификации одинаково важны, так что выбор одного в качестве основного представляется спорным.
  • Многие возможные комбинации (такие как в примере: permanent supervisor, temporary engineer, permanent engineer и так далее) являются необходимыми.
  • Рассматриваемые классы настолько важны, что стоит потратить время на разработку лучшей из возможных структур наследования. Чаще всего речь идет в таких случаях о библиотечных классах повторного использования.
  • Примером приложения, удовлетворяющего этим критериям, является библиотека Base с ее структурой иерархии на верхних уровнях, описанная в последней лекции этой книги. Классы, полученные в результате этих усилий, в деталях описаны в [M 1994а]. Они построены в традиции естественных наук с применением таксономических принципов систематической классификации основных программистских структур. Верхняя часть этой иерархии выглядит так:

    (рис 6.15) Классификация, основанная на видах фундаментальных программистских структур

    Классификация на первом уровне ( BOX, COLLECTION, TRAVERSABLE ) основана на типах; уровень ниже (и многие другие, не показанные на рисунке) задают классификацию подтипов. Структура контейнера характеризуется тремя различными критериями:

  • COLLECTION определяет доступ к элементам. Класс SET позволяет определить сам факт присутствия элемента, в то время как BAG позволяет также посчитать число вхождений данного элемента. Дальнейшие уточнения включают такие абстракции доступа, как SEQUENCE (элементы доступны последовательно), STACK (элементы доступны в порядке, обратном их включению) и так далее.
  • BOX определяет представление элементов. Варианты включают конечные и бесконечные структуры. Конечные структуры могут быть ограниченными и не ограниченными. Ограниченные структуры могут быть фиксированными или изменяемого размера.
  • TRAVERSABLE определяет способы обхода структур.
  • Интересно отметить, что эта иерархия не начиналась, как иерархия видов. Начальная идея состояла в том, чтобы определить BOX, COLLECTION и TRAVERSABLE как несвязанные классы, каждый, задающий вершину своей независимой иерархии. Затем при описании реализации любой специальной структуры данных использовать множественное наследование с родителями из каждой иерархии. Например, связный список является конечным и неограниченным с последовательным доступом и линейным способом обхода.

    (рис 6.16) Построение структуры данных комбинированием абстракций путем множественного наследования

    Но затем мы осознали, что независимые семейства классов BOX, COLLECTION и TRAVERSABLE не лучший способ: им всем потребовались некоторые общие компоненты, в частности has (тест на проверку членства) и empty (тест на отсутствие элементов). Все это указывало на необходимость иметь общего родителя - CONTAINER, где эти общие свойства теперь и появляются. Следовательно, структура, изначально спроектированная как чистое множественное наследование с тремя непересекающимися иерархиями, превратилась в структуру с наследованием типов, приводящую к дублируемому наследованию.

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

    Как разрабатываются структуры наследования

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

    Специализация и абстракция

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

    Подобный комментарий, сделанный Майклом Джексоном, упоминался при рассмотрении проектирования сверху вниз.

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

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

  • точки в пространстве произвольной размерности, приводящие к наследственной структуре, где братьями класса POINT будут классы POINT_3D и так далее;
  • геометрические фигуры - другими классами структуры могут быть FIGURE, RECTANGLE, CIRCLE и так далее;
  • многоугольники - с такими классами, как QUADRANGLE (четыре вершины), TRIANGLE (три вершины) и SEGMENT (две вершины), POINT является специальным случаем, имеющим ровно одну вершину;
  • объекты, полностью определяемые двумя координатами - другими кандидатами являются комплексные числа и двумерные векторы COMPLEX и VECTOR_2D.
  • Интуитивно некоторые из этих обобщений кажутся более приемлемыми, чем другие, но невозможно со всей определенностью выбрать наилучшее. Ответ зависит от потребностей. Потому предусмотрительный и осторожный процесс, в котором абстракция создается с некоторым опозданием, чтобы точно убедиться в правильном выборе пути обобщения, может быть предпочтительнее скорых и быстрых решений, приводящих к непроверенной абстракции.

    Произвольность классификации

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

    Эта ситуация не является спецификой разработки ПО. Классификация Линнея не является универсально приемлемой или непреложной. У нее есть соперники, один из которых вместо традиционного эволюционного критерия использует другой, более индуктивный, основанный на ДНК-анализе и приводящий к совершенно другим результатам. Есть зоологи, для которых умение (неумение) птиц летать является важным таксономическим признаком, но официальная классификация с этим не соглашается.

    Индукция и дедукция

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

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

    Разнообразие абстракции

    Этот принцип атавизма - один из наиболее удивительных из всех 
    		атрибутов наследования.
    						Чарльз Дарвин

    Две формы апостериорного конструирования родителя являются общими и полезными.

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

    (рис 6.17) Абстракция

    То, что это понимание не пришло сразу, другими словами, то, что B был построен без учета A, - не является причиной отказа от наследования в этом случае. Сразу же при обнаружении необходимости A вы можете, а в большинстве случаев должны написать этот класс и адаптировать B как его наследника. Это не столь хорошо, как написать раньше A, но лучше, чем не написать вовсе.

    Факторизация возникает в случае обнаружения того, что два класса E и F фактически представляют варианты одного и того же понятия:

    (рис 6.18) Факторизация

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

    Независимость клиента

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

    (рис 6.19) Абстракция, факторизация и клиенты

    Когда B абстрагируется в A, или компоненты E факторизуются с компонентами F в D, класс X, представляющий клиента B или E (на рисунке он клиент обоих классов) в большинстве случаев не заметит никаких изменений. Включение класса в схему наследования не оказывает влияния на его клиентов, если они применяют компоненты класса на сущностях соответствующего типа. Другими словами, если X использует B и E как поставщиков по схеме:

    b1: B; e1: E
    ...
    b1.some_feature_of_B
    ...
    e1.some_feature_of_E

    то X не заметит, что B или E обрели родителей в результате абстрагирования или факторизации.

    Совершенствование уровня абстракции

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

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

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

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

    Прежде всего не следует бояться разнообразия способов использования наследования. Запрет множественного или льготного наследования не достигает никакой другой цели, кроме нанесения вреда самому себе. Механизмы должны помогать нам, используйте их правильно, но используйте их!

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

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

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

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

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

    Ключевые концепции

  • Каждое использование наследования должно отражать некоторую форму отношения "is a" между двумя категориями объектов, либо из внешней моделируемой области, либо из мира самого ПО.
  • Не используйте наследование для моделирования отношения "has"; это область клиентских отношений. (Помните о CAR_OWNER.)
  • Когда наследование применимо, часто потенциально применим и клиент. Если соответствующая точка зрения может измениться, используйте клиентское отношение. Если предвидится полиморфное использование, используйте наследование.
  • Не вводите промежуточных узлов наследования, если только они не задают хорошо определенные абстракции со своими специфическими компонентами.
  • Данная классификация наследования основана на двенадцати видах, разделенных на три общие категории: наследование модели (описывающее отношения, существующие в моделируемой области), программное наследование (описывающее отношения, существующие в самом ПО) и наследование вариаций (для адаптации класса либо в модели, либо в ПО).
  • Мощь наследования происходит из комбинации специализации типа и механизма расширения модуля. Было бы немудро использовать различные механизмы языка.
  • Наследование реализации и льготное наследование требуют особой тщательности, но представляют мощную технику, которую следует использовать на стороне поставщика.
  • Наследование видов - это тонкая техника, включающая дублирующее наследование, позволяющая классифицировать типы объектов по нескольким конкурирующим критериям. Оно полезно при разработке профессиональных библиотек. Во многих случаях простая техника описателей предпочтительнее.
  • Хотя и нет теоретического идеала, фактический процесс проектирования иерархии наследования идет в двух направлениях - от абстрактного к конкретному и наоборот.
  • Наследование является главным образом техникой поставщика.
  • Библиографические замечания

    Основным учебником по таксономии наследования является [Girod 1991]. Книга по ОО-методологии [Page-Jones 1995] - одна из немногих, где даются полезные методологические советы, включая ценные рекомендации, когда следует и когда не следует использовать наследование. Еще один полезный учебник Джона Мак-Грегора [McGregor 1992] исследует технику, названную в этой лекции наследованием видов.

    В [Breu 1995] также содержатся полезные концепции, основанные на более ограничительном подходе к наследованию, чем в рассматриваемой лекции.

    Техника, подобная "описателям" этой лекции, рассмотрена в [Gil 1994].

    При подготовке этой статьи использовались комментарии нескольких биологов, ведущих доступные в Интернете Web-ресурсы по таксономии живых существ, в частности "Древо жизни" (http://www.phylogeny.arizona.edu/tree/life.html), учтиво предоставленные профессорами Дэвидом Мэдисоном, а для птиц Майклом Лореном. Помощь профессора Эдвина Эверхама из университета Рэдфорда была весьма полезной.

    Руководства по теории классификации или систематике приведены в следующем разделе.

    Приложение: история таксономии

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

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

    Вероятно, подобно эволюционным таксономистам, хотелось бы взять понемногу от тех и от других.

    Упражнения

    У6.1 Стек, основанный на массиве

    Напишите полностью класс STACK и его потомка ARRAYED_STACK, набросок которого дан в этой лекции, используя технику "брака по расчету".

    У6.2 Метатаксономия

    Представьте себе, что введенная классификация форм наследования представляла бы собой иерархию наследования. Какие виды наследования были бы включены?

    У6.3 Стеки Ханоя

    (Это упражнение пришло из примера Филиппа Дрикса.)

    Рассмотрим отложенный класс STACK с процедурой put для вталкивания элемента на вершину с предусловием, включающим булеву функцию full (которая также может быть названа extendible ; когда вы ознакомитесь с упражнением, то заметите, что выбор имени может влиять на возможные решения).

    Задача о Ханойских башнях, известная по многим учебникам как пример рекурсивной процедуры, идет от работы Эдварда Лукаса, Париж, 1883 г.

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

    Можно ли определить класс HANOI_STACK, представляющий такие пирамиды, как наследника класса STACK? Если да, каким должен быть этот класс? Если нет, может ли HANOI_STACK как-то использовать STACK? Напишите класс полностью для различных возможных решений, обсудите все "за и против" каждого решения. Установите, какое из них предпочтительнее и объясните ваш выбор.

    У6.4 Являются ли многоугольники списками?

    Реализация нашего примера наследования класса POLYGON использовала атрибут связного списка vertices для представления числа сторон многоугольника. Следует ли вместо этого наследовать POLYGON от LINKED_LIST [POINT]?

    У6.5 Наследование функциональной вариации

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

    У6.6 Примеры классификации

    Для каждого из следующих случаев укажите, к какому виду наследования он относится:

  • SEGMENT от OPEN_FIGURE ;
  • COMPARABLE (объекты, поставляемые с отношением полного порядка), наследуемые от PART_COMPARABLE (объекты с отношением частичного порядка);
  • некоторые классы EXCEPTIONS.
  • У6.7 Кому принадлежат итераторы?

    Разумно ли компоненты итератора ( while_do и ему подобные) включать в классы, описывающие структуры данных, которые они итерируют, такие как LIST? Рассмотрите следующие аргументы:

  • простоту применения в процессе итерирования подпрограмм action и test, выбираемых приложением;
  • расширяемость: возможность добавления новых схем итерирования;
  • общность: выполнение ОО-принципов, в частности той идеи, что операции не существуют сами по себе, но связаны с некоторой абстракцией данных.
  • У6.8 Наследование типа и модуля

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

    У6.9 Наследование и полиморфизм

    Из рассмотренных видов наследования этой лекции между родителем A и наследником B, для каких на практике характерно использование полиморфного присоединения, другими словами присваивание x := y или соответствующая передача аргументов x типа A и y типа B?

    Вернуться к учебному плану