Для выработки методологического принципа часто полезно - как показано во многих обсуждениях этой книги - вначале понять, как не следует делать вещи. Понимание того, "что такое плохо", позволяет осознать, "что такое хорошо". Если постоянно тепло, то грушевое дерево не зацветет, ему необходима встряска зимним морозом - тогда оно расцветет весной.
Вот и встряска для нас, любезно предоставленная широко известным во всем мире вузовским учебником, выдержавшим 4 издания, по которому
Множественное наследование позволяет нескольким объектам выступать в роли базовых и поддерживается во многих языках (ссылка на первое издание этой книги [M1988]).
Помимо неудачного использования "объектов", вместо классов начало кажется весьма подозрительным. Цитата продолжается:
Характеристики нескольких различных классов объектов
(классы, уже хорошо!)
могут комбинироваться, создавая новый объект.
(Нет, опять неудача.) Далее следует пример
например, пусть мы имеем класс объектов
CAR , инкапсулирующий информацию об автомобиле, и класс PERSON, инкапсулирующий информацию о человеке. Мы можем использовать их для определения
(неужели оправдаются наши наихудшие подозрения?)
нового класса
CAR -OWNER, комбинирующего атрибутыCAR и PERSON.
(Они оправдались.) Нас приглашают рассматривать каждый объект не только как персону, но и как автомобиль. Для каждого, кто изучал наследование даже на элементарном уровне, это станет сюрпризом.
Несомненно, вы понимаете, что второе отношение является клиентским, а не наследованием, владелец автомобиля является (is) персоной, но имеет (has) автомобиль.
(рис 6.1) Походящая модельВ формальной записи:
class CAR_OWNER inherit
PERSON
feature
my_car: CAR
...
end
В цитируемом тексте обе связи используют
Адаптация через наследование имеет тенденцию приводить к избыточной функциональности, что может делать компоненты неэффективными и громоздкими.
И в самом деле, громоздкими - подумаем о владельце машины с крышей, мотором, карбюратором, не говоря уже о четырех колесах и запаске. Эта картина возникла, возможно, под влиянием образчика австралийского юмора, где владелец машины выглядит так, как если бы он являлся своим автомобилем.
(рис 6.2) Рисунок Джеффа Хокинга (голова его похожа на его же авто с открытыми дверцами)Наследование не является тривиальной концепцией, так что мы можем забыть и простить автора процитированного отрывка, но сам пример имеет важную практическую пользу - он помог нам стать немного умнее и напомнил базисное правило наследования:
Правило: Наследование "Is-a" (является) Не делайте класс |
Другими словами, мы должны быть способными убеждать, что каждый B is an A (отсюда имя: "is-a").
Вопреки первому впечатлению, это слабое, а не строгое правило, и вот почему:
B всегда является A. В большинстве случаев мы оставляем пространство для дискуссии. Верно ли, что каждый "сберегательный счет" (savings account) является "SAVINGS_ ACCOUNT наследником BANK_ACCOUNT или поместить его где-либо еще в структуре наследования. Разумные люди могут все же не согласиться с результатом. Это нестрашно, важно лишь, чтобы был случай, для которого ваши аргументы способны устоять. В нашем контрпримере: нет ситуации, при которой аргументы в пользу того, что CAR_OWNER является CAR , могли бы устоять.Эти наблюдения показывают как полезность, так и ограниченность правила "Is-a". Оно полезно как отрицательное правило, позволяя обнаружить и отвергнуть неподходящее использование наследования. Но как положительное правило оно недостаточно - не все, что проходит тест, заданный правилом, является подходящим случаем наследования.
Основное правило выбора между двумя возможными межмодульными отношениями - клиентом и наследованием - обманчиво просто: клиент имеет, наследование является. Почему же тогда выбор столь непрост?
Причина в том, что иметь не всегда означает быть, но во многих случаях быть означает иметь.
Нет, это не дешевая попытка экзистенциалистской философии - это отражение трудностей системного моделирования. Иллюстрацией первой половины высказывания опять-таки может служить наш пример: владелец автомобиля имеет машину, но нет никаких причин утверждать, что он является машиной.
Что можно сказать об обратной ситуации? Рассмотрим простое предложение о двух объектах из обычной программистской жизни:
Каждый инженер-программист является инженером. [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_2 новое значение должно быть типа ENGINEER или совместимого с ним; для класса SOFTWARE_ENGINEER_3 оно может быть любого типа, совместимого с VOCATION ( Профессия ). Такая программа способна моделировать инженера-программиста, который после многих лет притязаний стать настоящим инженером, наконец, покончил с этой составляющей своей личности и решил стать поэтом или сантехником. ("Не надо оваций. Графа Монте-Кристо из меня не вышло. Придется переквалифицироваться в управдомы".)
Это приводит к нашему первому критерию:
Правило изменений Не используйте наследование для описания отношения, воспринимаемого как " является ", если компоненты соответствующего объекта могут изменять тип в период выполнения. |
Используйте наследование только при условии, что отношение между объектами постоянно. В других случаях используйте отношение клиента.
По настоящему интересный случай имеет место для Это также означает, что, хотя решение использует клиента как первичное отношение, но на практике в своей окончательной форме оно часто использует дополнительно |
Займемся теперь критерием, требующим наследования и исключающим клиента. Этот критерий прост: он основан на полиморфизме. При изучении наследования мы видели, что для объявления в форме:
x: C
x обозначает в период выполнения (предполагая, что класс C не является развернутым) полиморфную ссылку. Другими словами, x может быть присоединен как к прямому экземпляру C, так и к экземпляру потомков C. Это свойство представляет ключевой вклад в мощность и гибкость ОО-метода, особенно из-за следствий - возможности определения полиморфных структур данных, подобных LIST [C], которые могут содержать экземпляры любого из потомков C.
В нашем примере это означает, что, выбрав решение SOFTWARE_ENGINEER_1 - форму, в которой класс является наследником ENGINEER, клиент может объявить сущность:
eng: ENGINEER
Эта сущность в период выполнения может быть присоединена к объекту типа SOFTWARE_ENGINEER_1. Можно иметь список инженеров, базу данных, включающую инженеров-механиков, инженеров-химиков наряду с программистами.
Методологическое напоминание: использование слов, не относящихся к программе, облегчает понимание концепций, но это нужно делать с осторожностью, особенно для антропологических примеров. Объекты нашего интереса являются программными объектами, поэтому, когда мы говорим "a |
Такие полиморфные эффекты требуют наследования: в случае SOFTWARE_ENGINEER_2 или SOFTWARE_ENGINEER_3 сущности или структуры данных типа ENGINEER не могут непосредственно означать объекты "
Обобщая это наблюдение, характерное не только для этого примера, приходим к правилу, дополняющему правило изменений:
Правило полиморфизма Наследование подходит для описания отношения, воспринимаемого как " является ", если для сущностей может возникнуть потребность присоединения к объектам различных типов. |
Хотя оно и не вводит новых концепций, следующее правило удобно как итог обсуждения критериев, высказывающихся за и против наследования.
Выбор между клиентом и наследованием При решении, как выразить зависимость между классами B изначально имеет компонент типа A, но этот компонент в период выполнения может нуждаться в замене объектом другого типа, сделайте B клиентом A.A обозначали объекты типа B или в полиморфных структурах, содержащих объекты типа A, некоторые могли быть типа B, сделайте B наследником A. |
Приведем пример, использующий предшествующее правило. Он приводит к широко применимому образцу проектирования - описателям (handles).
Первый проект библиотеки для платформенно-независимой графики столкнулся с общей проблемой, как учитывать зависимость от платформы. Первое решение использовало
class WINDOW inherit
GENERAL_WINDOW
PLATFORM_WINDOW
feature
...
end
Класс GENERAL_WINDOW и ему подобные, такие как GENERAL_BUTTON, являются отложенными: они выражают все, что может быть сказано о соответствующих графических объектах и применимых операциях без ссылки на особенности графической платформы. Классы, такие как PLATFORM_WINDOW, обеспечивают связь с графической платформой, такой как Windows, OS/2 Presentation-Manager или Unix WEL или MEL ).
Класс, такой как WINDOW, будет комбинировать свойства родителей, реализуя отложенные компоненты GENERAL_WINDOW механизмами, обеспечиваемыми PLATFORM_WINDOW.
Класс PLATFORM_WINDOW (как и другие подобные классы) должен присутствовать в нескольких вариантах - по одному на каждую платформу. Эти идентично именуемые классы будут храниться в различных каталогах; инструментарий
Это решение работает, но его недостаток в том, что понятие WINDOW становится тесно связанным с выбранной платформой. Перефразируя недавний комментарий о наследовании, можно сказать: окно, став однажды окном , всегда им и останется. Это не слишком печально, поскольку трудно вообразить, что однажды, достигнув почтенного возраста, окно Unix вдруг решит стать окном OS/2. Картина становится менее абсурдной при расширении определения платформы - при включении форматов, таких как
Попытаемся выразить тесную связь между GUI-объектами и поддерживающим инструментарием, используя вместо наследования WINDOW и GENERAL_WINDOW, но зависимость от платформы будет представлена клиентской связью с классом TOOLKIT, представляющим необходимый инструментарий. Как это выглядит, показано на рис. 6.6:
(рис 6.6) Комбинация отношений наследования и клиентаИнтересный аспект этого решения в том, что понятие инструментария (toolkit) становится полноценной абстракцией, представляющей отложенный класс TOOLKIT. Каждый специфический инструментарий, такой как или 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 является отложенным, но реализуется его различными потомками, такими как .
Заметьте, было бы неверным, глядя на этот пример, придти к заключению: "Ага! Вот ситуация, при которой наследование было избыточным, и данная версия призвана избежать его". Начальная версия вовсе не была ошибочной, она работает довольно хорошо, но менее гибкая, чем вторая. И в основе второй версии лежит наследование, TOOLKIT, handle и динамического связывания компонентов, таких как window_display, все бы это не работало. Вовсе не отвергая наследование, эта техника демонстрирует его более сложную форму.
Техника мы применяли ее к библиотеке баз данных Store, где понятие платформы связывается с основанными на SQL различными интерфейсами реляционных баз данных, таких как Oracle, Ingres,
Для каждой из категорий наследования, вводимых в этой лекции, наследник не тривиален - он либо переобъявляет (переопределяет или реализует) некоторые наследуемые компоненты, либо вводит собственные компоненты, либо делает добавления в инвариант класса. Конечно, он может делать все это одновременно. Результатом является следующее правило, фактически являющееся следствием правила Наследования, которое появится в этой лекции чуть позднее:
Правило Таксомании (ограничения таксомании) Каждый наследник обязан ввести новый компонент, или переобъявить наследуемый компонент, или добавить предложение в инвариант класса. |
Это правило призвано бороться с человеческой слабостью, свойственной новичкам, овладевшим ОО-методом, - с энтузиазмом они стараются применить таксономическое деление (отсюда и имя правила, как сокращение "мания
Как часто бывает в таких случаях, вернуться к правильному видению - и возвратить новичков на грешную землю - помогает обращение к АТД. Класс является реализацией АТД, частичной или полной. Различные классы, в частности родитель и его наследники, должны описывать различные АТД. Поскольку АТД полностью характеризуется применимыми компонентами и их свойствами, охватываемые утверждениями класса, новый класс должен изменять наследуемые компоненты, вводить новые компоненты и утверждения. Так как
Иногда можно найти оправдание случаю таксомании: не приносящий ничего нового класс вводится на том основании, что наследник описывает важный частный случай, а пока подстилается соломка, предполагая в будущем возможность внесения изменений. Это может быть особенно разумным, если такой класс существует в естественной иерархии, принятой в данной проблемной области. Но всегда к введению таких классов следует подходить с осторожностью, всячески сопротивляясь появлению классов без новых компонентов.
Вот один пример. Предположим, некоторая система или библиотека включает класс PERSON, и вы рассматриваете целесообразность введения его потомков - MALE и FEMALE. Оправдано ли это? Следует все тщательно взвесить. Система PERSON и
female: BOOLEAN
или
Female: INTEGER is unique Male: INTEGER is unique
Однако если есть шанс, что специфические свойства персон разного пола могут проявиться позднее, то, возможно, предпочтительнее ввести эти классы заранее.
О чем следует помнить, так это о принципе Единого Выбора. Мы научились не доверять явному перечислению вариантов, реализуемых константами , из опасения обнаружить в нашем ПО куски кода с условиями выбора в форме:
if female then
...
else
...
или инструкциями. В данном случае, однако, не стоит особенно беспокоиться:
if менее эффективен, чем основанный на this_person.some_operation, где MALE и FEMALE по-разному определяют some_operation. Но тогда, если необходимо разделять людей по полу, мы нарушаем предпосылки данного обсуждения - отсутствие специфических свойств. Если такие свойства существуют, наследование оправдано.Последний комментарий сигнализирует о реальных трудностях. Простые случаи таксомании, когда без необходимости добавляются узлы в структуру наследования, диагностируются довольно просто (достаточно заметить отсутствие специфических свойств). Но что, если варианты должны иметь специфические свойства, в результате чего классификация конфликтует с другими критериями? Система FEMALE_EMPLOYEE, если специфические свойства пола сотрудника выделяют этот класс, подобно тому как другие свойства выделяют классы постоянных и временных служащих. Но тогда речь больше не идет о таксомании - возникает другая общая и тонкая проблема многокритериальной классификации (multi-criteria classification), чье возможное решение обсуждается позже в этой лекции.
Мощь наследования - это следствие его универсальности. Правда и то, что временами оно наносит вред, заставляя многих авторов вводить ограничения на механизм. Понимая эти опасения, а иногда и разделяя их, отбросим случайные сомнения и страхи и научимся радоваться наследованию во всех его законных вариантах, к исследованию которых мы теперь и переходим.
Дадим обзор правильного использования наследования:
Некоторые из этих категорий (подтипы, вид, реализация, конкретизация, льготы) приводят к специфическим проблемам, обсуждаемым в отдельных разделах.
Относительно широкое рассмотрение наследования, предпринятое в этой книге, не означает, что "подходит все". Мы принимаем и фактически поддерживаем только некоторые формы наследования, часть из которых одобряется не всеми авторами. Конечно, есть много способв неверного использования наследования, вроде CAR_OWNER. Так что случаи наследования строго ограничены:
Правило Наследования Каждое использование наследования должно принадлежать одной из допустимых категорий. |
Это правило утверждает, что все типы наследования известны и что, если встречается ситуация, не покрываемая этими типами, то не следует применять наследование.
Под допустимыми категориями понимаются категории, рассматриваемые в этом разделе. И я надеюсь, что все имеющие смысл ситуации полностью покрываются этим рассмотрением. Но
Мы уже видели следствие правила Наследования - правило Таксомании, устанавливающее необходимость введения собственного вклада для класса наследника. Это непосредственно следует из того, что каждая легитимная форма наследования, детализируемая ниже, требует от наследника выполнения по крайней мере одной из ранее перечисленных операций. |
Правило Наследования не запрещает наследственные связи, принадлежащие более чем к одной категории. Однако такая практика не рекомендуется.
Правило Упрощения Наследования Следует предпочитать наследование, принадлежащее ровно одной допустимой категории. |
Это не абсолютное правило; оно относится к рекомендательным положительным правилам. Оно вводится в интересах простоты и ясности: всякий раз, когда вводится наследственная связь межу двумя классами, неявно применяются методологические принципы, в особенности при решении вопроса выбора одного из применимых вариантов. Простота структуры уменьшает вероятность ошибки проектирования или создания хаоса, усложняющего использование и сопровождение.
Конечно, возможна ситуация, при которой одна наследственная связь служит двум различным целям в нашей классификации. Но такие случаи составляют меньшинство.
К сожалению, я не знаю простого критерия, недвусмысленно говорящего о корректности |
Прежде чем рассмотреть правильные случаи, еще раз поговорим об ошибках. Ошибаться - в природе человека, нельзя надеяться на полноту классификации возможных ошибок, но несколько общих ошибок идентифицируются просто.
Первая типичная ошибка связана с путаницей отношений " has " и " is ". Класс CAR_OWNER служит примером - экстремальным, но не уникальным. Мне доводилось слышать и видеть и другие подобные примеры, такие как APPLE_PIE, наследуемый от APPLE и от PIE, или (упоминаемый Adele Goldberg) ROSE_TREE, наследуемый от ROSE и от TREE.
Другим типичным примером является таксомания, в котором простое булево свойство, такое как пол персоны (или свойство с несколькими фиксированными значениями, такое как цвет светофора), используется как критерий наследования, хотя нет важных вариантов компонентов, зависящих от свойства.
Третьей типичной ошибкой является наследование по расчету (convenience inheritance), при котором разработчик видит некоторые полезные компоненты класса и создает наследника просто для того, чтобы использовать эти компоненты. Заметьте, использование "наследования реализации" или "наследование компонентов класса" являются допустимыми формами наследования, изучаемыми позже в этой лекции. Ошибка в том, что класс используется как родитель без подходящего отношения is-a между соответствующими абстракциями, а в некоторых случаях вообще без адекватной абстракции.
С этого момента речь пойдет о правильном использовании наследования. Список включает двенадцать различных категорий, для удобства сгруппированных в три семейства:
(рис 6.7) Классификация допустимых категорий наследованияКлассификация основана на том наблюдении, что любая программная система отражает как внешнюю модель, так и связь с реалиями в области программных приложений. В связи с этим будем различать:
Эти три общие категории облегчают понимание, но наиболее важные свойства задаются терминальными категориями.
Так как классификация сама по себе является таксономией, можно из любопытства задаться вопросом, как применить к ней самой идентифицируемые категории. Это является темой упражнения У6.2. |
Следующие далее определения используют имя A для B для наследника:
(рис 6.8) Соглашение именования при определении категорий наследованияКаждое из определений будет устанавливать, в каких случаях A и B могут быть отложенными, а когда - эффективными. Обсуждение завершается таблицей, содержащей сводку применимых категорий для каждой комбинации отложенных и эффективных классов.
Начнем с наиболее очевидной формы модельного наследования. При моделировании внешней системы часто возникает ситуация, при которой категория внешних объектов естественно разделяется на непересекающиеся подкатегории. Например, замкнутые фигуры можно разделить на многоугольники и эллипсы. Вот формальное определение:
Определение: наследование подтипов Наследование |
A' может быть множеством замкнутых фигур, B' - множеством многоугольников, A и B - соответствующие классы. В большинстве практических случаев "внешняя система" не принадлежит миру программ, например, определяет некоторые аспекты деятельности компании (внешними объектами являются специальные и
Наследование
( ПОЗВОНОЧНЫЕ $$\gets$$ МЛЕКОПИТАЮЩИЕ и подобные примеры).
Мы настаиваем, что родитель A должен быть отложенным, поскольку он описывает не полностью специфицированное множество объектов, в то время как наследник B может быть как эффективным, так и отложенным. Следующие две категории рассматривают ситуации, где A может быть эффективным классом.
В одном из следующих разделов эта категория наследования будет рассмотрена детальнее, поскольку она не столь уж проста, как может показаться с первого взгляда.
Определение: Наследование c Ограничением Наследование c ограничением применимо, если экземпляры |
Типичным примером является: Прямоугольник $$\gets$$ Квадрат.
Ограничением является утверждение: сторона1 = сторона2 (включается в инвариант класса Квадрат ).
Многие математические примеры подпадают под эту категорию.
Последняя часть определения позволяет избежать смешения этой формы наследования с другими, такими как наследование с расширением, допускающим появление полностью новых компонентов у наследника. Здесь в интересах простоты предпочтительно ограничивать новые компоненты теми, что непосредственно следуют из добавленного ограничения. Например, у класса CIRCLE может появиться новый компонент radius, отсутствующий у ELLIPSE.
Поскольку допускаются только концептуальные изменения класса A, добавляющие некоторые ограничения в класс B, то оба класса должны быть либо отложенными, либо эффективными.
Наследование с ограничением концептуально близко к наследованию
Определение: Наследование с Расширением Наследование с расширением применимо, когда |
Присутствие обоих вариантов - расширения и сужения (ограничения) - является одним из парадоксов наследования. Как отмечалось при обсуждении наследования, расширение применяется к компонентам, в то время как ограничение (понимаемое как специализация) применяется к экземплярам. Но это не устраняет парадокс.
Проблема в том, что добавляемые компоненты обычно включают атрибуты. Так что при наивной интерпретации типа (заданного классом) как множества его экземпляров отношение между классом и наследником (рассматриваемых как множества) "быть подмножеством" становится полностью ошибочным. Рассмотрим пример:
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 был наследником , в отличие от изучаемого нами способа. Причина простая: класс имеет атрибут ; класс RECTANGLE наследует его, добавляя новый компонент other_side. Этот проект был подвергнут критике, он был пересмотрен с обращением наследования.
Но не следует исключать наследование с расширением как общую категорию. У нее есть эквивалент в математике, где специализация некоторого понятия происходит путем добавления новых операций. Такое происходит довольно часто и считается необходимым. Типичным примером является понятие кольца, представляющее специализацию понятия группы. В группе задана некоторая операция, назовем ее +, обладающая рядом свойств. Кольцо является группой, потому имеет ту же операцию + с теми же свойствами. Но в кольцо добавляется новая операция, скажем, *, со своими собственными свойствами. По сути это не отличается от введения нового
Соответствующая схема используется и при разработке ОО-ПО. Конечно, класс должен быть наследником RECTANGLE, а не наоборот, но можно предложить легитимные примеры. Класс MOVING_POINT (в приложениях кинематики) может наследовать от чисто графического класса POINT и добавлять компонент speed, описывающую величину и направление скорости. Другой пример, в может наследовать от DOCUMENT, добавляя специфические свойства - текущую позицию лекции в книге и процедуру ее сохранения.
(Читатели - не математики могут пропустить этот раздел.)
Для успокоения совести следует разрешить видимый парадокс, отмеченный выше (обнаружение того, что MB не является подмножеством MA ), так как мы хотим, чтобы некоторое отношение подмножества имело место между наследником и родителем. И это отношение реально существует, парадокс лишь показывает, что
class C feature
c1: T1
c2: T2
c3: T3
end
Мы не должны выбирать в качестве математической модели C' - множества экземпляров C -
Вместо этого, следует рассматривать любой экземпляр как частичную функцию, отображающую множество возможных имен атрибутов ATTRIBUTE в множество возможных значений VALUE, со следующими свойствами:
c1, c2 и c3.VALUE (множество цели для функции) является супермножеством $$T'1 \cup T'2 \cup T'3$$.c1 лежат в T'1 и так далее.Тогда, если вспомнить, что функция является специальным случаем отношения и что отношение является множеством пар (например, в ранее упоминаемом случае экземпляр класса A может быть промоделирован функцией {<a1, 25>}, а экземпляр класса B - {<a1, 1>, <b1, -2.5>} ), мы получаем ожидаемое свойство - B' является подмножеством A'. Заметьте, здесь уже элементы обоих множеств являются парами и первая функция задает все возможные отображения второго атрибута.
Заметьте также, что принципиально важно установить свойство |
Это обсуждение дает только схему математической модели. С деталями использования частичных функций для моделирования
(Читатели - не математики, добро пожаловать!) Перейдем теперь ко второму семейству категорий - наследованию вариаций.
Определение: Наследование вариаций типа и функций Наследование вариаций применяется, если B переопределяет некоторые компоненты |
Наследование вариаций применимо, когда существующий класс A, задающий некоторую абстракцию, полезен сам по себе, но обнаруживается необходимость представления подобной, хотя и не идентичной абстракции, имеющей те же компоненты, но с отличиями в сигнатуре и реализации.
Определение требует, чтобы оба класса были эффективными (общий случай) или оба отложенными. Оно не рассматривает эффективизацию компонентов, когда речь идет о переходе от абстрактной формы к конкретной. Тесно связанной является рассматриваемая далее категория "отмена эффективизации", в которой некоторые эффективные компоненты становятся отложенными.
Из определения следует, что наследник не должен вводить новых компонентов за исключением непосредственно необходимых для переопределения. Этим проводится граница между наследованием расширением и наследованием вариаций.
При вариациях типа можно изменять только сигнатуры некоторых компонентов (число и типы аргументов и результата). Эта форма наследования подозрительна и часто является признаком таксомании. В законных случаях, однако, это может быть подготовкой для наследования расширением или реализацией. Примером наследования вариации типа могут быть наследники MALE_EMPLOYEE и FEMALE_EMPLOYEE.
Наследование вариации типа не является необходимым, когда начальная сигнатура использует закрепленные (like...) объявления. Например, в классе интерактивного пакета для рисования можно ввести функцию:
perpendicular: SEGMENT is
-- Сегмент, повернутый на 90 градусов
...
Затем определим наследника DOTTED_SEGMENT, дающего графическое представление пунктирными, а не непрерывными линиями. В этом классе perpendicular должен возвращать результат типа DOTTED_SEGMENT, так что необходимо переопределить тип. Этого бы не требовалось, если бы изначально результат объявлялся как like Current. Так что, будь у вас доступ к источнику и его автору, можно было бы предложить модифицировать оригинал, не нанося ущерба существующим клиентам. Но если нет возможности модифицировать оригинал или по ряду причин закрепленное объявление не подходит оригиналу (вероятно, из-за потребностей других потомков), то возможность переопределить тип может стать палочкой-выручалочкой.
При наследовании функциональной вариации изменяются тела некоторых компонентов. Если, как обычно в таком случае, компонент уже эффективен, то это означает изменение реализации. Спецификация компонента, заданная утверждением, также может измениться. Также возможно, хотя и реже встречается, иметь функциональную вариацию между двумя отложенными классами, в этом случае будут меняться только утверждения. Это может повлечь изменения в некоторых функциях, отложенных или эффективных, связанных с утверждениями, или даже добавление новых компонентов.
Наследование функциональной вариации является прямым приложением принципа Открыт-Закрыт: мы хотим адаптировать существующий класс, не затрагивая оригинал (к коду которого мы можем и не иметь доступа) и его клиентов. Это может стать предметом злоупотреблений, некоей формой хакерства, перекручивая существующий класс, приспосабливая его для других целей. Во всяком случае это будет организованное хакерство, позволяющее избежать угроз модификации существующего ПО. Но если есть доступ к оригиналу, то предпочтительной может оказаться реорганизация
Определение: Наследование с Отменой эффективизации Наследование с отменой эффективизации применимо, если |
Отмена эффективизации не является общим приемом и не должна им быть. Основная идея этого способа противоречит общему направлению, так как обычно ожидается конкретизация потомка B своего более абстрактного родителя A (как это имеет место в следующей рассматриваемой категории, для которой A является отложенным, а B эффективным или, по крайней мере, менее отложенным). По этой причине новичкам следует избегать отмены эффективизации. Но она может быть законной в двух случаях:
Для этой категории наследования класс B будет отложенным; A обычно эффективным, но может быть частично отложенным.
Перейдем теперь к третьей и последней группе категорий - программному наследованию.
Определение: Наследование с конкретизацией Наследование с конкретизацией применимо, если |
Примером, используемым многократно в предыдущих лекциях, является отложенный класс TABLE, описывающий таблицы самой общей природы. Конкретизация ведет к потомкам SEQUENTIAL_ TABLE и HASH_TABLE, все еще отложенным. Заключительная конкретизация SEQUENTIAL_TABLE приводит к эффективным классам ARRAYED_TABLE, LINKED_TABLE, FILE_TABLE.
Термин "конкретизация" (reification), введенный Георгом Лукасом, происходит от латинского слова, означающего "превращение в вещь". Он используется в спецификациях и методе разработки |
Определение: структурное наследование Структурное наследование применяется, если |
Обычно A представляет математическое свойство, которым может обладать некоторое множество объектов. Например, A может быть классом , поставляемым с такими операциями, как и , представляющим объекты с заданным отношением полного порядка. Класс, которому необходимо STRING, становится наследником .
Наследование таким способом от нескольких родителей является обычным приемом. Например, класс INTEGER в библиотеке Kernel наследует от и класса (с такими компонентами, как и ), задающего арифметические свойства. (Класс более точно представляет математическое понятие кольца.)
В чем разница между конкретизацией и структурным наследованием? При конкретизации B представляет то же понятие, что и A, отличаясь большей степенью реализации; при структурном наследовании B представляет собственную абстракцию, для которой A задает лишь один из аспектов, такой как порядок на объектах или наличие арифметических операций.
Валден и Нерсон заметили, что новички иногда верят, что они используют подобную форму наследования, подменяя фактически имеющее место отношение "is" вариантом схемы "AIRPLANE наследуется от VENTILATION_SYSTEM ). Они указывают, что этой ошибки просто избежать благодаря абсолютному критерию, не оставляющему места для сомнений или двусмысленности:
При схеме наследования, хотя наследуемые свойства являются вторичными, они все же являются свойствами всего объекта, описываемого классом. Если мы делаем |
Заключение в этом примере очевидно: AIRPLANE должен быть клиентом, а не наследником класса VENTILATION_SYSTEM.
Определение: Наследование Реализации Наследование реализации применяется, если |
Наследование реализации в деталях обсуждается позднее в этой лекции. Общей ситуацией является "брак по расчету", основанный на
Случай наследования константных атрибутов и однократных функций покрывается следующим вариантом.
Льготное наследование является схемой, в которой родитель представляет коллекцию полезных компонентов, предназначенных для использования его потомками.
Определение: Льготное наследование Льготное наследование применяется, если A все являются константами или однократными функциями, описывающими разделяемые объекты.A являются подпрограммами, рассматриваемыми как операции |
Примером льготного наследования может служить класс EXCEPTIONS, класс, предоставляющий множество утилит, обеспечивающих доступ к механизму обработки исключений.
Иногда, как в примерах, которые появятся чуть позже, при льготном наследовании используется только один вариант - константы или абстрактная машина, но в других случаях, как для класса EXCEPTIONS, Incorrect_inspect_value ), так и подпрограммы (такие как для возбуждения исключения разработчика). Так как при нашем обсуждении категории наследования рассматриваются как непересекающиеся, то льготное наследование с двумя пересекающимися вариантами рассматривается как одна категория.
При наследовании констант как A, так и B являются эффективными. При наследовании B должно быть, по меньшей мере, столь же эффективно как и A.
В деталях льготное наследование еще будет обсуждаться в данной лекции.
В следующей таблице обобщены правила, определяющие для каждой категории, должны ли родитель и его потомок быть эффективными или отложенными классами. "Вариация" покрывает случаи вариации типа и функциональной вариации. Элементы, помеченные символом *, появляются более чем в одном входе.
| Потомок | Родитель | |
|---|---|---|
| Отложенный | Эффективный | |
| Отложенный | Константы* Ограничение* Структура* Тип* Потеря эффективизации* Вариация* Вид |
Расширение* Потеря эффективизации* |
| Эффективный | Константы* Конкретизация Структура* Тип* |
Константы* Расширение* Реализация Ограничение* Вариация* |
Заметьте, это обсуждение предполагает в качестве основы раннюю презентацию, определяющую смысл наследования (см. лекцию 14 курса "Основы объектно-ориентированного программирования").
Разнообразие использования наследования, чему свидетельством является предшествующее рассмотрение, может создавать впечатление, что должны существовать разнообразные механизмы языка, покрывающие основополагающие понятия. В частности, ряд авторов предлагают разделение наследование модуля, в особенности как средства повторного использования существующих компонентов в новом модуле, и наследования типа, в частности механизма классификации типов.
Такое разделение нанесло бы больше вреда, чем принесло пользы. Вот несколько доводов.
Во-первых, сведение наследования к двум категориям не отражает всего разнообразия, вводимого нашей классификацией. Так как никто не будет отстаивать введение десяти различных языковых механизмов, то введение двух механизмов приводило бы к ограниченному результату.
Практическим следствием были бы бесполезные методологические обсуждения: предположим, вы хотите наследовать от класса LINEAR_ITERATOR ; следует ли использовать наследование модуля или наследование типа? Можно приводить аргументы в защиту одного и другого решения. Вклад этого предложения в критерий качества нашего ПО и скорость его создания будут фактически нулевыми.
В упражнении У6.8 требуется проанализировать наши категории, отнеся их либо к наследованию модуля, либо к наследованию типа. |
Интересно задуматься и о тех последствиях в усложнении языка, к которым привело бы такое разделение. Наследование сопровождается несколькими вспомогательными механизмами, большинство из которых необходимо обоим видам:
RECTANGLE, переопределяющий perimeter от POLYGON ) и для расширения модуля (принцип Открыт-Закрыт требует при наследовании модуля сохранения гибкости изменений, без чего будет потеряно одно из главных преимуществ ОО-метода).STATE_INSTITUTIONS в географической или выборной информационной системе может иметь потомка LOUISIANA_INSTITUTIONS, отражающего особенности политической структуры штата Луизиана, поэтому вполне ожидаемо желание потомка переименовать компонент counties, задающий список округов штатов, в parishes - имя, используемое для округа в данном штате.select со всеми недостатками при появлении новых случаев. Появляются и другие вопросы - когда разделять компоненты, а когда их дублировать.Все это ради преимуществ пуристской точки зрения - ограниченной и спорной. Нет ничего плохого в защите спорной точки зрения, но следует быть крайне осторожным в нововведениях и учитывать их последствия для . Он не только в деталях объяснил все недостатки этой инструкции, основываясь на теории конструирования ПО и процесса его выполнения, но и показал, как можно без труда заменить этот механизм. В данном же случае убедительные аргументы не представлены, по крайней мере, я не увидел, почему "плохо" иметь единый механизм, покрывающий как наследование модулей, так и наследование типа.
Помимо общих возражений, основанных на предвзятых идеях о том, каково должно быть наследование, приводится лишь один серьезный аргумент против единого механизма наследования - сложность статической проверки типов, возникающая при этом подходе. Эта проблема полностью анализировалась в лекции 17 курса "Основы объектно-ориентированного программирования", бремя ее решения возлагается на компиляторы, которые признают это бремя разумным, если этим существенно облегчается жизнь разработчиков.
Как покажет в конечном итоге это обсуждение, возможность иметь единый механизм для наследования модуля и типа является результатом важнейшего решения ОО-конструирования ПО: унификации концепций модуля и типа в едином понятии класса. Если мы принимаем классы как модули и как типы, то следует принять и наследование, аккумулирующее модули и типы.
Первая категория наследования из нашего списка, вероятно, единственная, с которой согласится каждый, по меньшей мере, тот, кто принимает наследование: то, что мы можем назвать чистым наследованием
Большая часть данного обсуждения применима и к наследованию с ограничением, чье принципиальное отличие состоит в том, что от родителя не требуется быть отложенным классом.
Как указывалось во введении в наследование, мощь этой идеи частично происходит от интеграции механизма модуля с механизмом типов: определения нового типа как специального случая существующего типа, определение нового модуля как расширения уже существующего модуля. Большинство из спорных вопросов о наследовании идут от осознания конфликтов между этими двумя точками зрения. При наследовании
Наследование
FIGURE $$\gets$$ CLOSED_FIGURE $$\gets$$ POLYGON $$\gets$$ QUADRANGLE $$\gets$$ RECTANGLE $$\gets$$ SQUARE DEVICE $$\gets$$ FILE $$\gets$$ TEXT_FILESHIP $$\gets$$ LEISURE_SHIP $$\gets$$ SAILBOATACCOUNT $$\gets$$ SAVINGS_ACCOUNT $$\gets$$ FIXED_RATE_ACCOUNTВ любом из этих случаев четко идентифицируется множество объектов, описываемое родительским типом, и мы выделяем подмножество, характеризуемое некоторыми свойствами, которыми обладают не все объекты родителя. Например, текстовый файл является файлом со специальными свойствами, вытекающими из того, что элементы файла являются строками текста, что нехарактерно, например, для
Общее правило при наследовании
Некоторые из примеров, такие как |
Наследование , QUADRANGLE etc., или следует разделять объекты на правильные многоугольники ( EQUILATERAL_POLYGON, и т. д.) и неправильные?
Несколько стратегий доступно для
Тип - это не просто множество объектов. Он характеризуется также применимыми операциями (компонентами) и их семантическими свойствами (утверждениями:
Правила, применяемые к утверждениям, поддерживают этот взгляд на подтипы:
Для компонентов ситуация более тонкая. С точки зрения на подтип требуется, чтобы все операции, применимые к экземплярам родителя, должны быть применимы к экземплярам наследника. Внутренне это всегда верно: даже для класса 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 курса "Основы объектно-ориентированного программирования" при обсуждении
Исключения [или genus, species etc.] характеризуются свойством XYZ ", которому не предшествовало бы " большинство ", " обычно " или за которым не следовало бы " за исключением нескольких случаев ". Это справедливо на всех уровнях иерархии, даже для наиболее фундаментальных категорий, для которых, казалось бы, существуют бесспорные критерии!
Если вы думаете, например, что просто отличать животных от растений, то ознакомьтесь с текстом, взятым из популярного учебника (курсив добавлен):
Отличие растений от животных Есть несколько общих факторов, позволяющих отличать растения от животных, хотя есть многочисленные исключения. Перемещение. Большинство животных свободно передвигаются, в то время как редкие растения могут перемещаться в окружающем их пространстве. Большинство растений имеют корни в почве или прикреплены к скалам, деревьям или другим материалам. Пища. Зеленые растения, содержащие хлорофилл, сами производят еду для себя, большинство животных питаются растениями или поедают других животных. Рост. Растения обычно растут от концов своих ветвей и корней и от внешних участков ствола в течение всей жизни. У животных рост обычно идет во всех частях их тела и прекращается при достижении зрелости. Химическая регуляция. Для растений и для животных общим является наличие гормонов и других химикалиев, регулирующих |
Те же комментарии применимы к другим областям изучения. Это в полной степени относится и к области человеческой культуры - классификация естественных языков внесла свой вклад в разработку систематической
Известный пример из зоологии, ставший уже клише, иллюстрирует таксономию исключений. (Помните, однако, что это только аналогия, а не программистский пример.) Птицы летают. (Класс BIRD должен иметь процедуру .) Страус - птица, но страус не летает. При создании наследника - класса OSTRICH - необходимо будет указать, что эта самая большая из птиц не летает.
При классификации птиц можно было бы попытаться разделить их на две категории - летающие и не летающие. Но это конфликтовало бы с другими возможными и более важными критериями, применяемыми в классификации, ставшей стандартной.
Пример со страусом ( OSTRICH ) имеет интересный поворот. Хотя, к сожалению, они и не сознают этого, страусы фактически должны летать. Молодые поколения теряют этот наследственный навык из-за случайности эволюционной истории. Анатомически страусы являются самой совершенной аэродинамической машиной среди всех птиц. Это свойство, немного осложняя работу профессионального таксономиста, (хотя ее может облегчить его коллега - профессиональный таксидермист) не помешает классифицировать страусов в иерархии птиц.
В программистских терминах класс OSTRICH будет наследником BIRD, скрывая наследуемый компонент .
Все наши усилия (по классификации) кажутся беспомощными на фоне множественности отношений живых существ, окружающих нас. Эта битва Человека и Природы во всей ее бесконечности описана величайшим ботаником Гете. Одно можно сказать с уверенностью, Человек в ней всегда будет побежден. Анри Бэйлон
Практика разработки ПО и аналогии природного мира свидетельствуют, что даже при самом тщательном проектировании остаются исключения redeem класса NEW_MORTGAGE или из 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 ), и count (переименованный ), доступны для ARRAYED_STACK, хотя и не экспортируются его клиентам. Классу они необходимы для реализации компонентов STACK.
Так что концептуально ничего ошибочного нет в наследовании в интересах реализации. Показательно сравнение с контрпримером, изучаемым в начале этой лекции, класс CAR_OWNER свидетельство непонимания концепции наследования, класс ARRAYED_STACK задает хорошо определенную форму отношения "is-a".
Один недостаток здесь есть: механизм наследования, позволяющий ограничить доступность экспорта наследуемых компонентов (другими словами, разрешение предложения export ), делает более трудной статическую проверку типов. Но это трудности разработчиков компиляторов, а не разработчиков прикладного ПО.
Давайте проверим, как можно выполнить эту работу без использования наследования. Для нашего примера это было уже сделано в классе STACK2 из предыдущих лекций. Он имеет атрибут типа ARRAY [G] и процедуры стека, реализованные в следующем виде (утверждения опущены):
put (x: G) is
-- Добавляет x на вершину
require
...
do
count := count + 1
representation.put (count, x)
ensure
...
end
Каждая манипуляция с представлением требует вызова компонента ARRAY с как цели. Платой являются потери производительности: минимальные по памяти (атрибут ), более серьезные по времени (связанные с , накладные расходы, добавляемые при вызове каждой операции).
Предположим, что проблемы эффективности можно игнорировать. Остается еще одна утомительная необходимость выписывать перед каждой операцией префикс " ". Это придется делать для всех классов, реализующих различные структуры данных - стеки, списки, очереди, все, что реализуется массивами.
ОО-разработчики ненавидят утомительные, повторяющиеся операции. "Встроенное повторение" - вот наш девиз. Если некий образец повторно встречается в множестве классов, естественной и здоровой реакцией является попытка понять общую абстракцию и встроить ее в класс. Абстракция здесь нечто подобное "структуре данных, имеющей доступ к массиву и его операциям".
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
Компоненты |
При таком определении класса не вызывает споров, что классы, такие как ARRAYED_STACK или ARRAYED_LIST, становятся наследниками ARRAYED: они действительно описывают структуры на массивах. Эти классы могут теперь использовать 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 компонента), описывающих
Рассмотрим, например, лексический анализатор, ответственный за идентификацию лексем входного текста. TOKENIZER, необходим доступ к кодам символов для их классификации на цифры, буквы и т. д. Такой класс воспользуется льготами и наследует эти коды от ASCII:
class TOKENIZER inherit ASCII feature
... Программы класса могут использовать компоненты Blank, Case_diff и другие...
end
К классам, подобным ASCII, относятся иногда неодобрительно, но прежде чем перейти к методологической дискуссии, взглянем на еще один пример льготного наследования.
Второй пример демонстрирует наследование программ общего вида, а не константных атрибутов.
Предположим, мы хотим обеспечить общий механизм, позволяющий просмотр всех элементов (итерирование) некоторой структуры данных, например линейных структур, таких как списки. "Итерирование" означает выполнение некоторой процедуры, скажем, action, на элементах этой структуры, просматриваемых в последовательном порядке. Нам хочется обеспечить несколько различных механизмов итерирования, включающих применение action ко всем элементам, удовлетворяющим условию, заданному test, ко всем элементам до появления первого, удовлетворяющего test, или до первого, не удовлетворяющего этой функции. Ну и так далее, вариантов можно придумать много. Система, использующая этот механизм, должна быть способна применять его к произвольным компонентам action и test.
С первого взгляда может показаться, что итерирующие компоненты должны принадлежать классам, описывающих соответствующие структуры данных, таким как LIST или SEQUENCE.
В упражнении У6.7 предлагается показать, что это неправильное решение.
Предпочтительнее ввести независимую иерархию
(рис 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, вместо того чтобы быть описанной в классе, может наследоваться от другого родителя. В этом случае action, наследуемую от одного родителя под именем justify (здесь переименование существенно), с эффективной justify, наследуемой от другого родителя. Реально, это и есть брак по расчету.
Класс LINEAR_ITERATOR является замечательным примером класса поведения (behavior class), рассматривая общее поведение и оставляя открытыми специфические компоненты, так чтобы его потомки могли подключить специальные варианты.
Два примера, ASCII и LINEAR_ITERATOR, демонстрируют два главных варианта льготного наследования:
Как отмечалось ранее, возможна комбинация этих вариантов в единой наследственной связи. Вот почему льготное наследование задается одной категорией, а не двумя.
Некоторые рассматривают льготное наследование как злоупотребление механизмом наследования, как некоторую форму хакерства.
Главный вопрос, заслуживающий рассмотрения, связан не столько с наследованием, сколько с тем, как определены классы ASCII и LINEAR_ITERATOR. Как всегда, при рассмотрении проекта класса, следует спросить себя: "Действительно ли мы описали значимую абстракцию данных - множество объектов, характеризуемых
Для этих примеров ответ менее очевиден, чем для классов RECTANGLE, BANK_ACCOUNT или LINKED_LIST, но, по сути, он тот же:
ASCII представляет абстракцию: "любой объект, имеющий доступ к ASCII ".LINEAR_ITERATOR представляет абстракцию: "любой объект, способный выполнять последовательные итерации на линейной структуре". Такой объект имеет тенденцию быть "Как только эти абстракции принимаются, наследственные связи не вызывают никаких проблем: экземпляру TOKENIZER необходим "доступ к
charset: ASCII ... create charset
При каждом использовании кода символа потребуется задавать целевой объект charset.Lower_a. Присоединяемый объект charset не играет никакой полезной роли. Те же комментарии справедливы и для класса LINEAR_ITERATOR. Но если классу необходимы несколько видов итерации, то тогда создание объектов-action и test становится разумным.
Коль скоро мы хотим иметь объекты-
Вероятно, наиболее трудная проблема наследования возникает при наличии альтернативных критериев классификации.
Традиционная классификация в естественных науках использует единственный критерий (возможно, объединяющий несколько качеств) на каждом уровне: позвоночные или беспозвоночные, ветви, обновляемые один или несколько раз в год, и тому подобное. Результатом будет то, что называется иерархией единственного наследования, чье главное преимущество - простота классификации. Конечно, возникают проблемы, поскольку природа определенно не пользуется единственным критерием. Это очевидно для всякого, кто когда-либо пытался провести классификацию, вооружившись книгой по ботанике с традиционной классификацией Линнея.
При разработке ПО, где единый критерий кажется ограничительным, мы можем использовать все приемы множественного и особенно дублирующего наследования, которыми мы овладели при изучении предыдущих лекций. Рассмотрим, например, класс EMPLOYEE в системе
Оба эти критерия приводят к правильным классам-потомкам. При этом мы не впадаем в таксоманию, так как идентифицируемые классы, такие как TEMPORARY_EMPLOYEE по первому критерию и MANAGER по второму, действительно характеризуются специальными компонентами, не применимыми к другим категориям. Как же следует поступать?
В первой попытке введем все варианты на одном и том же уровне (рис. 6.12).
Для простоты на этой схеме имена классов сокращены. В реальной системе мы действуем более аккуратно и используем, как положено, полные имена, такие как |
Получившуюся
(рис 6.12) Беспорядочная классификация
Помня об идеях использования наследования для классификации, следует ввести промежуточный уровень, описывающий конкурирующие критерии классификации (рис. 6.13).
Появились два вида служащих. Заметьте, имя CONTRACT_EMPLOYEE не означает служащего, имеющего контракт, а служащего, характеризуемого контрактом (он может не иметь контракта!). Имя класса для другого вида означает "служащий, характеризуемый своей специальностью".
То, что эти имена кажутся неестественными, отражает определенную сложность, характерную для наследования видов. При наследовании CONTRACT_EMPLOYEE и SPECIALTY_EMPLOYEE не напрямую, но через наследование от классов и 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.
Эта схема, как вы заметили, почти идентична образцу проектирования с описателями, изучаемому ранее в этой лекции.
Такая техника может использоваться вместо наследования видов. Это усложняет структуру из-за введения независимой иерархии, нового атрибута (здесь ) и соответствующего
(рис 6.14) Многокритериальная классификация с независимой иерархией, построенной для клиента
Нет ничего необычного в рассмотрении наследования видов на ранних этапах анализа проблемной области, когда обсуждаются фундаментальные концепции и рассматриваются несколько равно привлекательных критериев классификации. В дальнейших исследованиях часто оказывается, что один из критериев начинает доминировать, выступая в качестве основы построения иерархической структуры. Тогда, как показывает наше обсуждение, следует отказаться от
Все же я нахожу наследование видов полезным при выполнении следующих трех условий:
Примером приложения, удовлетворяющего этим критериям, является библиотека Base с ее структурой иерархии на верхних уровнях, описанная в последней лекции этой книги. Классы, полученные в результате этих усилий, в деталях описаны в [M 1994а]. Они построены в традиции естественных наук с применением таксономических принципов систематической классификации основных программистских структур. Верхняя часть этой иерархии выглядит так:
(рис 6.15) Классификация, основанная на видах фундаментальных программистских структурКлассификация на первом уровне ( BOX, COLLECTION, ) основана на типах; уровень ниже (и многие другие, не показанные на рисунке) задают классификацию
COLLECTION определяет доступ к элементам. Класс SET позволяет определить сам факт присутствия элемента, в то время как BAG позволяет также посчитать число вхождений данного элемента. Дальнейшие уточнения включают такие абстракции доступа, как SEQUENCE (элементы доступны последовательно), STACK (элементы доступны в порядке, обратном их включению) и так далее.BOX определяет представление элементов. Варианты включают конечные и бесконечные структуры. Конечные структуры могут быть ограниченными и не ограниченными. Ограниченные структуры могут быть фиксированными или изменяемого размера.TRAVERSABLE определяет способы обхода структур.Интересно отметить, что эта иерархия не начиналась, как иерархия видов. Начальная идея состояла в том, чтобы определить BOX, COLLECTION и как несвязанные классы, каждый, задающий вершину своей независимой иерархии. Затем при описании реализации любой специальной структуры данных использовать
(рис 6.16) Построение структуры данных комбинированием абстракций путем множественного наследованияНо затем мы осознали, что независимые семейства классов BOX, COLLECTION и не лучший способ: им всем потребовались некоторые общие компоненты, в частности 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 обрели родителей в результате
В группе разработчиков, правильно применяющих ОО-метод, это регулярное совершенствование уровня абстракции ПО и, как следствие, его качества, заметно ощущается членами команды и служит источником постоянной мотивации.
Наследование никогда не перестанет удивлять нас своей мощью и универсальностью. В этой лекции мы пытались как можно лучше описать, что реально означает наследование и как можно его использовать для получения всех преимуществ. Перечислю несколько центральных выводов.
Прежде всего не следует бояться разнообразия способов использования наследования. Запрет множественного или льготного наследования не достигает никакой другой цели, кроме нанесения вреда самому себе. Механизмы должны помогать нам, используйте их правильно, но используйте их!
Наследование является, как правило, техникой поставщика. Это мощное оружие нашего арсенала в сражениях с противниками (в частности сложностью - безжалостным врагом разработчика).
Конечно, все ПО проектируется для клиентов, клиентские потребности управляют процессом. Множество классов хорошо тогда, когда оно предоставляет замечательные службы клиентам: интерфейсы и ассоциированные с ними реализации, являющиеся полными, свободными от неприятных сюрпризов (таких как неожиданные потери производительности), просты в использовании, легки в обучении, запоминании, допускают расширяемость. Для достижения этих целей проектировщик свободен в использовании наследования и других ОО-методов любым способом, который ему нравится. Победителей не судят.
Напомню также, при проектировании структуры наследования целью является конструирование ПО, а не философия. Редко существует единственное, или лучшее решение. Для нас "лучшее" означает лучшее для целей некоторого класса клиентского приложения. Это особенно верно, когда мы уходим от таких областей как математика или компьютерные науки, где существуют широко применимые теории, и попадаем в мир деловых приложений.
В известной степени в этом есть некоторый комфорт. Натуралист, классифицирующий растения и животных, должен мыслить в абсолютных категориях. В программном мире эквивалентом является создание библиотек общецелевого назначения (фундаментальные структуры данных, графика, базы данных). Большей же частью ваши цели скромны, вам необходимо спроектировать хорошую иерархию, ту, которая удовлетворяет потребностям клиентов.
Заключительный урок этой лекции обобщает комментарий, сделанный при обсуждении льготного наследования: принципиальная трудность построения структуры классов не в наследовании, она в поиске абстракций. Если правильные абстракции идентифицированы, то структура наследования следует из них. Для поиска абстракций следует руководствоваться тем, чем мы руководствуемся на протяжении всей книги, - абстрактными типами данных.
CAR_OWNER.)Основным учебником по
В [Breu 1995] также содержатся полезные концепции, основанные на более ограничительном подходе к наследованию, чем в рассматриваемой лекции.
Техника, подобная "
При подготовке этой статьи использовались комментарии нескольких биологов, ведущих доступные в Интернете Web-ресурсы по
Руководства по теории классификации или систематике приведены в следующем разделе.
Данное приложение является дополнительным материалом и не используется в остальной части курса. Изучение усилий по
Мы прислушиваемся к мнениям биологов, звучащих с двух противоположных сторон: одних, отстаивающих априорную форму классификации, сверху вниз, дедуктивную и основанную на "естественном" порядке вещей, идущую к нам от кладистов, начиная с Линнея, других, отстаивающих эмпирическую, индуктивную точки зрения фенетиков снизу вверх, советующих нам наблюдать и собирать...
Вероятно, подобно эволюционным таксономистам, хотелось бы взять понемногу от тех и от других.
Напишите полностью класс STACK и его потомка ARRAYED_STACK, набросок которого дан в этой лекции, используя технику "брака по расчету".
Представьте себе, что введенная классификация форм наследования представляла бы собой
(Это упражнение пришло из примера Филиппа Дрикса.)
Рассмотрим отложенный класс STACK с процедурой put для вталкивания элемента на вершину с full (которая также может быть названа extendible ; когда вы ознакомитесь с упражнением, то заметите, что выбор имени может влиять на возможные решения).
Задача о Ханойских башнях, известная по многим учебникам как пример рекурсивной процедуры, идет от работы Эдварда Лукаса, Париж, 1883 г.
Рассмотрим теперь известную задачу о Ханойских башнях, где нужно перенести пирамиду (башню), составленную из отдельных дисков разного размера, с одного стержня на другой, используя третий, соблюдая правило: диск может быть переложен только на диск большего размера.
Можно ли определить класс HANOI_STACK, представляющий такие STACK? Если да, каким должен быть этот класс? Если нет, может ли HANOI_STACK как-то использовать STACK? Напишите класс полностью для различных возможных решений, обсудите все "за и против" каждого решения. Установите, какое из них предпочтительнее и объясните ваш выбор.
Реализация нашего примера наследования класса использовала атрибут связного списка vertices для представления числа сторон многоугольника. Следует ли вместо этого наследовать от LINKED_LIST [POINT]?
Приведите один или несколько примеров функциональной вариации. Для каждого из них обсудите, дают ли они законный образец принципа Открыт-Закрыт или являются примерами того, что называется "организованным хакерством".
Для каждого из следующих случаев укажите, к какому виду наследования он относится:
SEGMENT от OPEN_FIGURE ;COMPARABLE (объекты, поставляемые с отношением полного порядка), наследуемые от PART_COMPARABLE (объекты с отношением частичного порядка);EXCEPTIONS.Разумно ли компоненты while_do и ему подобные) включать в классы, описывающие структуры данных, которые они итерируют, такие как LIST? Рассмотрите следующие аргументы:
action и test, выбираемых приложением;Предположим, мы разрабатываем язык с двумя типами наследования: расширением модуля и подтипами. К какому из этих типов следует отнести категории наследования, идентифицируемые в этой лекции?
Из рассмотренных видов наследования этой лекции между родителем A и наследником B, для каких на практике характерно использование полиморфного присоединения, другими словами присваивание x := y или соответствующая передача аргументов x типа A и y типа B?
Для выработки методологического принципа часто полезно - как показано во многих обсуждениях этой книги - вначале понять, как не следует делать вещи. Понимание того, "что такое плохо", позволяет осознать, "что такое хорошо". Если постоянно тепло, то грушевое дерево не зацветет, ему необходима встряска зимним морозом - тогда оно расцветет весной.
Вот и встряска для нас, любезно предоставленная широко известным во всем мире вузовским учебником, выдержавшим 4 издания, по которому
Множественное наследование позволяет нескольким объектам выступать в роли базовых и поддерживается во многих языках (ссылка на первое издание этой книги [M1988]).
Помимо неудачного использования "объектов", вместо классов начало кажется весьма подозрительным. Цитата продолжается:
Характеристики нескольких различных классов объектов
(классы, уже хорошо!)
могут комбинироваться, создавая новый объект.
(Нет, опять неудача.) Далее следует пример
например, пусть мы имеем класс объектов
CAR , инкапсулирующий информацию об автомобиле, и класс PERSON, инкапсулирующий информацию о человеке. Мы можем использовать их для определения
(неужели оправдаются наши наихудшие подозрения?)
нового класса
CAR -OWNER, комбинирующего атрибутыCAR и PERSON.
(Они оправдались.) Нас приглашают рассматривать каждый объект не только как персону, но и как автомобиль. Для каждого, кто изучал наследование даже на элементарном уровне, это станет сюрпризом.
Несомненно, вы понимаете, что второе отношение является клиентским, а не наследованием, владелец автомобиля является (is) персоной, но имеет (has) автомобиль.
(рис 6.1) Походящая модельВ формальной записи:
class CAR_OWNER inherit
PERSON
feature
my_car: CAR
...
end
В цитируемом тексте обе связи используют
Адаптация через наследование имеет тенденцию приводить к избыточной функциональности, что может делать компоненты неэффективными и громоздкими.
И в самом деле, громоздкими - подумаем о владельце машины с крышей, мотором, карбюратором, не говоря уже о четырех колесах и запаске. Эта картина возникла, возможно, под влиянием образчика австралийского юмора, где владелец машины выглядит так, как если бы он являлся своим автомобилем.
(рис 6.2) Рисунок Джеффа Хокинга (голова его похожа на его же авто с открытыми дверцами)Наследование не является тривиальной концепцией, так что мы можем забыть и простить автора процитированного отрывка, но сам пример имеет важную практическую пользу - он помог нам стать немного умнее и напомнил базисное правило наследования:
Правило: Наследование "Is-a" (является) Не делайте класс |
Другими словами, мы должны быть способными убеждать, что каждый B is an A (отсюда имя: "is-a").
Вопреки первому впечатлению, это слабое, а не строгое правило, и вот почему:
B всегда является A. В большинстве случаев мы оставляем пространство для дискуссии. Верно ли, что каждый "сберегательный счет" (savings account) является "SAVINGS_ ACCOUNT наследником BANK_ACCOUNT или поместить его где-либо еще в структуре наследования. Разумные люди могут все же не согласиться с результатом. Это нестрашно, важно лишь, чтобы был случай, для которого ваши аргументы способны устоять. В нашем контрпримере: нет ситуации, при которой аргументы в пользу того, что CAR_OWNER является CAR , могли бы устоять.Эти наблюдения показывают как полезность, так и ограниченность правила "Is-a". Оно полезно как отрицательное правило, позволяя обнаружить и отвергнуть неподходящее использование наследования. Но как положительное правило оно недостаточно - не все, что проходит тест, заданный правилом, является подходящим случаем наследования.
Основное правило выбора между двумя возможными межмодульными отношениями - клиентом и наследованием - обманчиво просто: клиент имеет, наследование является. Почему же тогда выбор столь непрост?
Причина в том, что иметь не всегда означает быть, но во многих случаях быть означает иметь.
Нет, это не дешевая попытка экзистенциалистской философии - это отражение трудностей системного моделирования. Иллюстрацией первой половины высказывания опять-таки может служить наш пример: владелец автомобиля имеет машину, но нет никаких причин утверждать, что он является машиной.
Что можно сказать об обратной ситуации? Рассмотрим простое предложение о двух объектах из обычной программистской жизни:
Каждый инженер-программист является инженером. [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_2 новое значение должно быть типа ENGINEER или совместимого с ним; для класса SOFTWARE_ENGINEER_3 оно может быть любого типа, совместимого с VOCATION ( Профессия ). Такая программа способна моделировать инженера-программиста, который после многих лет притязаний стать настоящим инженером, наконец, покончил с этой составляющей своей личности и решил стать поэтом или сантехником. ("Не надо оваций. Графа Монте-Кристо из меня не вышло. Придется переквалифицироваться в управдомы".)
Это приводит к нашему первому критерию:
Правило изменений Не используйте наследование для описания отношения, воспринимаемого как " является ", если компоненты соответствующего объекта могут изменять тип в период выполнения. |
Используйте наследование только при условии, что отношение между объектами постоянно. В других случаях используйте отношение клиента.
По настоящему интересный случай имеет место для Это также означает, что, хотя решение использует клиента как первичное отношение, но на практике в своей окончательной форме оно часто использует дополнительно |
Займемся теперь критерием, требующим наследования и исключающим клиента. Этот критерий прост: он основан на полиморфизме. При изучении наследования мы видели, что для объявления в форме:
x: C
x обозначает в период выполнения (предполагая, что класс C не является развернутым) полиморфную ссылку. Другими словами, x может быть присоединен как к прямому экземпляру C, так и к экземпляру потомков C. Это свойство представляет ключевой вклад в мощность и гибкость ОО-метода, особенно из-за следствий - возможности определения полиморфных структур данных, подобных LIST [C], которые могут содержать экземпляры любого из потомков C.
В нашем примере это означает, что, выбрав решение SOFTWARE_ENGINEER_1 - форму, в которой класс является наследником ENGINEER, клиент может объявить сущность:
eng: ENGINEER
Эта сущность в период выполнения может быть присоединена к объекту типа SOFTWARE_ENGINEER_1. Можно иметь список инженеров, базу данных, включающую инженеров-механиков, инженеров-химиков наряду с программистами.
Методологическое напоминание: использование слов, не относящихся к программе, облегчает понимание концепций, но это нужно делать с осторожностью, особенно для антропологических примеров. Объекты нашего интереса являются программными объектами, поэтому, когда мы говорим "a |
Такие полиморфные эффекты требуют наследования: в случае SOFTWARE_ENGINEER_2 или SOFTWARE_ENGINEER_3 сущности или структуры данных типа ENGINEER не могут непосредственно означать объекты "
Обобщая это наблюдение, характерное не только для этого примера, приходим к правилу, дополняющему правило изменений:
Правило полиморфизма Наследование подходит для описания отношения, воспринимаемого как " является ", если для сущностей может возникнуть потребность присоединения к объектам различных типов. |
Хотя оно и не вводит новых концепций, следующее правило удобно как итог обсуждения критериев, высказывающихся за и против наследования.
Выбор между клиентом и наследованием При решении, как выразить зависимость между классами B изначально имеет компонент типа A, но этот компонент в период выполнения может нуждаться в замене объектом другого типа, сделайте B клиентом A.A обозначали объекты типа B или в полиморфных структурах, содержащих объекты типа A, некоторые могли быть типа B, сделайте B наследником A. |
Приведем пример, использующий предшествующее правило. Он приводит к широко применимому образцу проектирования - описателям (handles).
Первый проект библиотеки для платформенно-независимой графики столкнулся с общей проблемой, как учитывать зависимость от платформы. Первое решение использовало
class WINDOW inherit
GENERAL_WINDOW
PLATFORM_WINDOW
feature
...
end
Класс GENERAL_WINDOW и ему подобные, такие как GENERAL_BUTTON, являются отложенными: они выражают все, что может быть сказано о соответствующих графических объектах и применимых операциях без ссылки на особенности графической платформы. Классы, такие как PLATFORM_WINDOW, обеспечивают связь с графической платформой, такой как Windows, OS/2 Presentation-Manager или Unix WEL или MEL ).
Класс, такой как WINDOW, будет комбинировать свойства родителей, реализуя отложенные компоненты GENERAL_WINDOW механизмами, обеспечиваемыми PLATFORM_WINDOW.
Класс PLATFORM_WINDOW (как и другие подобные классы) должен присутствовать в нескольких вариантах - по одному на каждую платформу. Эти идентично именуемые классы будут храниться в различных каталогах; инструментарий
Это решение работает, но его недостаток в том, что понятие WINDOW становится тесно связанным с выбранной платформой. Перефразируя недавний комментарий о наследовании, можно сказать: окно, став однажды окном , всегда им и останется. Это не слишком печально, поскольку трудно вообразить, что однажды, достигнув почтенного возраста, окно Unix вдруг решит стать окном OS/2. Картина становится менее абсурдной при расширении определения платформы - при включении форматов, таких как
Попытаемся выразить тесную связь между GUI-объектами и поддерживающим инструментарием, используя вместо наследования WINDOW и GENERAL_WINDOW, но зависимость от платформы будет представлена клиентской связью с классом TOOLKIT, представляющим необходимый инструментарий. Как это выглядит, показано на рис. 6.6:
(рис 6.6) Комбинация отношений наследования и клиентаИнтересный аспект этого решения в том, что понятие инструментария (toolkit) становится полноценной абстракцией, представляющей отложенный класс TOOLKIT. Каждый специфический инструментарий, такой как или 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 является отложенным, но реализуется его различными потомками, такими как .
Заметьте, было бы неверным, глядя на этот пример, придти к заключению: "Ага! Вот ситуация, при которой наследование было избыточным, и данная версия призвана избежать его". Начальная версия вовсе не была ошибочной, она работает довольно хорошо, но менее гибкая, чем вторая. И в основе второй версии лежит наследование, TOOLKIT, handle и динамического связывания компонентов, таких как window_display, все бы это не работало. Вовсе не отвергая наследование, эта техника демонстрирует его более сложную форму.
Техника мы применяли ее к библиотеке баз данных Store, где понятие платформы связывается с основанными на SQL различными интерфейсами реляционных баз данных, таких как Oracle, Ingres,
Для каждой из категорий наследования, вводимых в этой лекции, наследник не тривиален - он либо переобъявляет (переопределяет или реализует) некоторые наследуемые компоненты, либо вводит собственные компоненты, либо делает добавления в инвариант класса. Конечно, он может делать все это одновременно. Результатом является следующее правило, фактически являющееся следствием правила Наследования, которое появится в этой лекции чуть позднее:
Правило Таксомании (ограничения таксомании) Каждый наследник обязан ввести новый компонент, или переобъявить наследуемый компонент, или добавить предложение в инвариант класса. |
Это правило призвано бороться с человеческой слабостью, свойственной новичкам, овладевшим ОО-методом, - с энтузиазмом они стараются применить таксономическое деление (отсюда и имя правила, как сокращение "мания
Как часто бывает в таких случаях, вернуться к правильному видению - и возвратить новичков на грешную землю - помогает обращение к АТД. Класс является реализацией АТД, частичной или полной. Различные классы, в частности родитель и его наследники, должны описывать различные АТД. Поскольку АТД полностью характеризуется применимыми компонентами и их свойствами, охватываемые утверждениями класса, новый класс должен изменять наследуемые компоненты, вводить новые компоненты и утверждения. Так как
Иногда можно найти оправдание случаю таксомании: не приносящий ничего нового класс вводится на том основании, что наследник описывает важный частный случай, а пока подстилается соломка, предполагая в будущем возможность внесения изменений. Это может быть особенно разумным, если такой класс существует в естественной иерархии, принятой в данной проблемной области. Но всегда к введению таких классов следует подходить с осторожностью, всячески сопротивляясь появлению классов без новых компонентов.
Вот один пример. Предположим, некоторая система или библиотека включает класс PERSON, и вы рассматриваете целесообразность введения его потомков - MALE и FEMALE. Оправдано ли это? Следует все тщательно взвесить. Система PERSON и
female: BOOLEAN
или
Female: INTEGER is unique Male: INTEGER is unique
Однако если есть шанс, что специфические свойства персон разного пола могут проявиться позднее, то, возможно, предпочтительнее ввести эти классы заранее.
О чем следует помнить, так это о принципе Единого Выбора. Мы научились не доверять явному перечислению вариантов, реализуемых константами , из опасения обнаружить в нашем ПО куски кода с условиями выбора в форме:
if female then
...
else
...
или инструкциями. В данном случае, однако, не стоит особенно беспокоиться:
if менее эффективен, чем основанный на this_person.some_operation, где MALE и FEMALE по-разному определяют some_operation. Но тогда, если необходимо разделять людей по полу, мы нарушаем предпосылки данного обсуждения - отсутствие специфических свойств. Если такие свойства существуют, наследование оправдано.Последний комментарий сигнализирует о реальных трудностях. Простые случаи таксомании, когда без необходимости добавляются узлы в структуру наследования, диагностируются довольно просто (достаточно заметить отсутствие специфических свойств). Но что, если варианты должны иметь специфические свойства, в результате чего классификация конфликтует с другими критериями? Система FEMALE_EMPLOYEE, если специфические свойства пола сотрудника выделяют этот класс, подобно тому как другие свойства выделяют классы постоянных и временных служащих. Но тогда речь больше не идет о таксомании - возникает другая общая и тонкая проблема многокритериальной классификации (multi-criteria classification), чье возможное решение обсуждается позже в этой лекции.
Мощь наследования - это следствие его универсальности. Правда и то, что временами оно наносит вред, заставляя многих авторов вводить ограничения на механизм. Понимая эти опасения, а иногда и разделяя их, отбросим случайные сомнения и страхи и научимся радоваться наследованию во всех его законных вариантах, к исследованию которых мы теперь и переходим.
Дадим обзор правильного использования наследования:
Некоторые из этих категорий (подтипы, вид, реализация, конкретизация, льготы) приводят к специфическим проблемам, обсуждаемым в отдельных разделах.
Относительно широкое рассмотрение наследования, предпринятое в этой книге, не означает, что "подходит все". Мы принимаем и фактически поддерживаем только некоторые формы наследования, часть из которых одобряется не всеми авторами. Конечно, есть много способв неверного использования наследования, вроде CAR_OWNER. Так что случаи наследования строго ограничены:
Правило Наследования Каждое использование наследования должно принадлежать одной из допустимых категорий. |
Это правило утверждает, что все типы наследования известны и что, если встречается ситуация, не покрываемая этими типами, то не следует применять наследование.
Под допустимыми категориями понимаются категории, рассматриваемые в этом разделе. И я надеюсь, что все имеющие смысл ситуации полностью покрываются этим рассмотрением. Но
Мы уже видели следствие правила Наследования - правило Таксомании, устанавливающее необходимость введения собственного вклада для класса наследника. Это непосредственно следует из того, что каждая легитимная форма наследования, детализируемая ниже, требует от наследника выполнения по крайней мере одной из ранее перечисленных операций. |
Правило Наследования не запрещает наследственные связи, принадлежащие более чем к одной категории. Однако такая практика не рекомендуется.
Правило Упрощения Наследования Следует предпочитать наследование, принадлежащее ровно одной допустимой категории. |
Это не абсолютное правило; оно относится к рекомендательным положительным правилам. Оно вводится в интересах простоты и ясности: всякий раз, когда вводится наследственная связь межу двумя классами, неявно применяются методологические принципы, в особенности при решении вопроса выбора одного из применимых вариантов. Простота структуры уменьшает вероятность ошибки проектирования или создания хаоса, усложняющего использование и сопровождение.
Конечно, возможна ситуация, при которой одна наследственная связь служит двум различным целям в нашей классификации. Но такие случаи составляют меньшинство.
К сожалению, я не знаю простого критерия, недвусмысленно говорящего о корректности |
Прежде чем рассмотреть правильные случаи, еще раз поговорим об ошибках. Ошибаться - в природе человека, нельзя надеяться на полноту классификации возможных ошибок, но несколько общих ошибок идентифицируются просто.
Первая типичная ошибка связана с путаницей отношений " has " и " is ". Класс CAR_OWNER служит примером - экстремальным, но не уникальным. Мне доводилось слышать и видеть и другие подобные примеры, такие как APPLE_PIE, наследуемый от APPLE и от PIE, или (упоминаемый Adele Goldberg) ROSE_TREE, наследуемый от ROSE и от TREE.
Другим типичным примером является таксомания, в котором простое булево свойство, такое как пол персоны (или свойство с несколькими фиксированными значениями, такое как цвет светофора), используется как критерий наследования, хотя нет важных вариантов компонентов, зависящих от свойства.
Третьей типичной ошибкой является наследование по расчету (convenience inheritance), при котором разработчик видит некоторые полезные компоненты класса и создает наследника просто для того, чтобы использовать эти компоненты. Заметьте, использование "наследования реализации" или "наследование компонентов класса" являются допустимыми формами наследования, изучаемыми позже в этой лекции. Ошибка в том, что класс используется как родитель без подходящего отношения is-a между соответствующими абстракциями, а в некоторых случаях вообще без адекватной абстракции.
С этого момента речь пойдет о правильном использовании наследования. Список включает двенадцать различных категорий, для удобства сгруппированных в три семейства:
(рис 6.7) Классификация допустимых категорий наследованияКлассификация основана на том наблюдении, что любая программная система отражает как внешнюю модель, так и связь с реалиями в области программных приложений. В связи с этим будем различать:
Эти три общие категории облегчают понимание, но наиболее важные свойства задаются терминальными категориями.
Так как классификация сама по себе является таксономией, можно из любопытства задаться вопросом, как применить к ней самой идентифицируемые категории. Это является темой упражнения У6.2. |
Следующие далее определения используют имя A для B для наследника:
(рис 6.8) Соглашение именования при определении категорий наследованияКаждое из определений будет устанавливать, в каких случаях A и B могут быть отложенными, а когда - эффективными. Обсуждение завершается таблицей, содержащей сводку применимых категорий для каждой комбинации отложенных и эффективных классов.
Начнем с наиболее очевидной формы модельного наследования. При моделировании внешней системы часто возникает ситуация, при которой категория внешних объектов естественно разделяется на непересекающиеся подкатегории. Например, замкнутые фигуры можно разделить на многоугольники и эллипсы. Вот формальное определение:
Определение: наследование подтипов Наследование |
A' может быть множеством замкнутых фигур, B' - множеством многоугольников, A и B - соответствующие классы. В большинстве практических случаев "внешняя система" не принадлежит миру программ, например, определяет некоторые аспекты деятельности компании (внешними объектами являются специальные и
Наследование
( ПОЗВОНОЧНЫЕ $$\gets$$ МЛЕКОПИТАЮЩИЕ и подобные примеры).
Мы настаиваем, что родитель A должен быть отложенным, поскольку он описывает не полностью специфицированное множество объектов, в то время как наследник B может быть как эффективным, так и отложенным. Следующие две категории рассматривают ситуации, где A может быть эффективным классом.
В одном из следующих разделов эта категория наследования будет рассмотрена детальнее, поскольку она не столь уж проста, как может показаться с первого взгляда.
Определение: Наследование c Ограничением Наследование c ограничением применимо, если экземпляры |
Типичным примером является: Прямоугольник $$\gets$$ Квадрат.
Ограничением является утверждение: сторона1 = сторона2 (включается в инвариант класса Квадрат ).
Многие математические примеры подпадают под эту категорию.
Последняя часть определения позволяет избежать смешения этой формы наследования с другими, такими как наследование с расширением, допускающим появление полностью новых компонентов у наследника. Здесь в интересах простоты предпочтительно ограничивать новые компоненты теми, что непосредственно следуют из добавленного ограничения. Например, у класса CIRCLE может появиться новый компонент radius, отсутствующий у ELLIPSE.
Поскольку допускаются только концептуальные изменения класса A, добавляющие некоторые ограничения в класс B, то оба класса должны быть либо отложенными, либо эффективными.
Наследование с ограничением концептуально близко к наследованию
Определение: Наследование с Расширением Наследование с расширением применимо, когда |
Присутствие обоих вариантов - расширения и сужения (ограничения) - является одним из парадоксов наследования. Как отмечалось при обсуждении наследования, расширение применяется к компонентам, в то время как ограничение (понимаемое как специализация) применяется к экземплярам. Но это не устраняет парадокс.
Проблема в том, что добавляемые компоненты обычно включают атрибуты. Так что при наивной интерпретации типа (заданного классом) как множества его экземпляров отношение между классом и наследником (рассматриваемых как множества) "быть подмножеством" становится полностью ошибочным. Рассмотрим пример:
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 был наследником , в отличие от изучаемого нами способа. Причина простая: класс имеет атрибут ; класс RECTANGLE наследует его, добавляя новый компонент other_side. Этот проект был подвергнут критике, он был пересмотрен с обращением наследования.
Но не следует исключать наследование с расширением как общую категорию. У нее есть эквивалент в математике, где специализация некоторого понятия происходит путем добавления новых операций. Такое происходит довольно часто и считается необходимым. Типичным примером является понятие кольца, представляющее специализацию понятия группы. В группе задана некоторая операция, назовем ее +, обладающая рядом свойств. Кольцо является группой, потому имеет ту же операцию + с теми же свойствами. Но в кольцо добавляется новая операция, скажем, *, со своими собственными свойствами. По сути это не отличается от введения нового
Соответствующая схема используется и при разработке ОО-ПО. Конечно, класс должен быть наследником RECTANGLE, а не наоборот, но можно предложить легитимные примеры. Класс MOVING_POINT (в приложениях кинематики) может наследовать от чисто графического класса POINT и добавлять компонент speed, описывающую величину и направление скорости. Другой пример, в может наследовать от DOCUMENT, добавляя специфические свойства - текущую позицию лекции в книге и процедуру ее сохранения.
(Читатели - не математики могут пропустить этот раздел.)
Для успокоения совести следует разрешить видимый парадокс, отмеченный выше (обнаружение того, что MB не является подмножеством MA ), так как мы хотим, чтобы некоторое отношение подмножества имело место между наследником и родителем. И это отношение реально существует, парадокс лишь показывает, что
class C feature
c1: T1
c2: T2
c3: T3
end
Мы не должны выбирать в качестве математической модели C' - множества экземпляров C -
Вместо этого, следует рассматривать любой экземпляр как частичную функцию, отображающую множество возможных имен атрибутов ATTRIBUTE в множество возможных значений VALUE, со следующими свойствами:
c1, c2 и c3.VALUE (множество цели для функции) является супермножеством $$T'1 \cup T'2 \cup T'3$$.c1 лежат в T'1 и так далее.Тогда, если вспомнить, что функция является специальным случаем отношения и что отношение является множеством пар (например, в ранее упоминаемом случае экземпляр класса A может быть промоделирован функцией {<a1, 25>}, а экземпляр класса B - {<a1, 1>, <b1, -2.5>} ), мы получаем ожидаемое свойство - B' является подмножеством A'. Заметьте, здесь уже элементы обоих множеств являются парами и первая функция задает все возможные отображения второго атрибута.
Заметьте также, что принципиально важно установить свойство |
Это обсуждение дает только схему математической модели. С деталями использования частичных функций для моделирования
(Читатели - не математики, добро пожаловать!) Перейдем теперь ко второму семейству категорий - наследованию вариаций.
Определение: Наследование вариаций типа и функций Наследование вариаций применяется, если B переопределяет некоторые компоненты |
Наследование вариаций применимо, когда существующий класс A, задающий некоторую абстракцию, полезен сам по себе, но обнаруживается необходимость представления подобной, хотя и не идентичной абстракции, имеющей те же компоненты, но с отличиями в сигнатуре и реализации.
Определение требует, чтобы оба класса были эффективными (общий случай) или оба отложенными. Оно не рассматривает эффективизацию компонентов, когда речь идет о переходе от абстрактной формы к конкретной. Тесно связанной является рассматриваемая далее категория "отмена эффективизации", в которой некоторые эффективные компоненты становятся отложенными.
Из определения следует, что наследник не должен вводить новых компонентов за исключением непосредственно необходимых для переопределения. Этим проводится граница между наследованием расширением и наследованием вариаций.
При вариациях типа можно изменять только сигнатуры некоторых компонентов (число и типы аргументов и результата). Эта форма наследования подозрительна и часто является признаком таксомании. В законных случаях, однако, это может быть подготовкой для наследования расширением или реализацией. Примером наследования вариации типа могут быть наследники MALE_EMPLOYEE и FEMALE_EMPLOYEE.
Наследование вариации типа не является необходимым, когда начальная сигнатура использует закрепленные (like...) объявления. Например, в классе интерактивного пакета для рисования можно ввести функцию:
perpendicular: SEGMENT is
-- Сегмент, повернутый на 90 градусов
...
Затем определим наследника DOTTED_SEGMENT, дающего графическое представление пунктирными, а не непрерывными линиями. В этом классе perpendicular должен возвращать результат типа DOTTED_SEGMENT, так что необходимо переопределить тип. Этого бы не требовалось, если бы изначально результат объявлялся как like Current. Так что, будь у вас доступ к источнику и его автору, можно было бы предложить модифицировать оригинал, не нанося ущерба существующим клиентам. Но если нет возможности модифицировать оригинал или по ряду причин закрепленное объявление не подходит оригиналу (вероятно, из-за потребностей других потомков), то возможность переопределить тип может стать палочкой-выручалочкой.
При наследовании функциональной вариации изменяются тела некоторых компонентов. Если, как обычно в таком случае, компонент уже эффективен, то это означает изменение реализации. Спецификация компонента, заданная утверждением, также может измениться. Также возможно, хотя и реже встречается, иметь функциональную вариацию между двумя отложенными классами, в этом случае будут меняться только утверждения. Это может повлечь изменения в некоторых функциях, отложенных или эффективных, связанных с утверждениями, или даже добавление новых компонентов.
Наследование функциональной вариации является прямым приложением принципа Открыт-Закрыт: мы хотим адаптировать существующий класс, не затрагивая оригинал (к коду которого мы можем и не иметь доступа) и его клиентов. Это может стать предметом злоупотреблений, некоей формой хакерства, перекручивая существующий класс, приспосабливая его для других целей. Во всяком случае это будет организованное хакерство, позволяющее избежать угроз модификации существующего ПО. Но если есть доступ к оригиналу, то предпочтительной может оказаться реорганизация
Определение: Наследование с Отменой эффективизации Наследование с отменой эффективизации применимо, если |
Отмена эффективизации не является общим приемом и не должна им быть. Основная идея этого способа противоречит общему направлению, так как обычно ожидается конкретизация потомка B своего более абстрактного родителя A (как это имеет место в следующей рассматриваемой категории, для которой A является отложенным, а B эффективным или, по крайней мере, менее отложенным). По этой причине новичкам следует избегать отмены эффективизации. Но она может быть законной в двух случаях:
Для этой категории наследования класс B будет отложенным; A обычно эффективным, но может быть частично отложенным.
Перейдем теперь к третьей и последней группе категорий - программному наследованию.
Определение: Наследование с конкретизацией Наследование с конкретизацией применимо, если |
Примером, используемым многократно в предыдущих лекциях, является отложенный класс TABLE, описывающий таблицы самой общей природы. Конкретизация ведет к потомкам SEQUENTIAL_ TABLE и HASH_TABLE, все еще отложенным. Заключительная конкретизация SEQUENTIAL_TABLE приводит к эффективным классам ARRAYED_TABLE, LINKED_TABLE, FILE_TABLE.
Термин "конкретизация" (reification), введенный Георгом Лукасом, происходит от латинского слова, означающего "превращение в вещь". Он используется в спецификациях и методе разработки |
Определение: структурное наследование Структурное наследование применяется, если |
Обычно A представляет математическое свойство, которым может обладать некоторое множество объектов. Например, A может быть классом , поставляемым с такими операциями, как и , представляющим объекты с заданным отношением полного порядка. Класс, которому необходимо STRING, становится наследником .
Наследование таким способом от нескольких родителей является обычным приемом. Например, класс INTEGER в библиотеке Kernel наследует от и класса (с такими компонентами, как и ), задающего арифметические свойства. (Класс более точно представляет математическое понятие кольца.)
В чем разница между конкретизацией и структурным наследованием? При конкретизации B представляет то же понятие, что и A, отличаясь большей степенью реализации; при структурном наследовании B представляет собственную абстракцию, для которой A задает лишь один из аспектов, такой как порядок на объектах или наличие арифметических операций.
Валден и Нерсон заметили, что новички иногда верят, что они используют подобную форму наследования, подменяя фактически имеющее место отношение "is" вариантом схемы "AIRPLANE наследуется от VENTILATION_SYSTEM ). Они указывают, что этой ошибки просто избежать благодаря абсолютному критерию, не оставляющему места для сомнений или двусмысленности:
При схеме наследования, хотя наследуемые свойства являются вторичными, они все же являются свойствами всего объекта, описываемого классом. Если мы делаем |
Заключение в этом примере очевидно: AIRPLANE должен быть клиентом, а не наследником класса VENTILATION_SYSTEM.
Определение: Наследование Реализации Наследование реализации применяется, если |
Наследование реализации в деталях обсуждается позднее в этой лекции. Общей ситуацией является "брак по расчету", основанный на
Случай наследования константных атрибутов и однократных функций покрывается следующим вариантом.
Льготное наследование является схемой, в которой родитель представляет коллекцию полезных компонентов, предназначенных для использования его потомками.
Определение: Льготное наследование Льготное наследование применяется, если A все являются константами или однократными функциями, описывающими разделяемые объекты.A являются подпрограммами, рассматриваемыми как операции |
Примером льготного наследования может служить класс EXCEPTIONS, класс, предоставляющий множество утилит, обеспечивающих доступ к механизму обработки исключений.
Иногда, как в примерах, которые появятся чуть позже, при льготном наследовании используется только один вариант - константы или абстрактная машина, но в других случаях, как для класса EXCEPTIONS, Incorrect_inspect_value ), так и подпрограммы (такие как для возбуждения исключения разработчика). Так как при нашем обсуждении категории наследования рассматриваются как непересекающиеся, то льготное наследование с двумя пересекающимися вариантами рассматривается как одна категория.
При наследовании констант как A, так и B являются эффективными. При наследовании B должно быть, по меньшей мере, столь же эффективно как и A.
В деталях льготное наследование еще будет обсуждаться в данной лекции.
В следующей таблице обобщены правила, определяющие для каждой категории, должны ли родитель и его потомок быть эффективными или отложенными классами. "Вариация" покрывает случаи вариации типа и функциональной вариации. Элементы, помеченные символом *, появляются более чем в одном входе.
| Потомок | Родитель | |
|---|---|---|
| Отложенный | Эффективный | |
| Отложенный | Константы* Ограничение* Структура* Тип* Потеря эффективизации* Вариация* Вид |
Расширение* Потеря эффективизации* |
| Эффективный | Константы* Конкретизация Структура* Тип* |
Константы* Расширение* Реализация Ограничение* Вариация* |
Заметьте, это обсуждение предполагает в качестве основы раннюю презентацию, определяющую смысл наследования (см. лекцию 14 курса "Основы объектно-ориентированного программирования").
Разнообразие использования наследования, чему свидетельством является предшествующее рассмотрение, может создавать впечатление, что должны существовать разнообразные механизмы языка, покрывающие основополагающие понятия. В частности, ряд авторов предлагают разделение наследование модуля, в особенности как средства повторного использования существующих компонентов в новом модуле, и наследования типа, в частности механизма классификации типов.
Такое разделение нанесло бы больше вреда, чем принесло пользы. Вот несколько доводов.
Во-первых, сведение наследования к двум категориям не отражает всего разнообразия, вводимого нашей классификацией. Так как никто не будет отстаивать введение десяти различных языковых механизмов, то введение двух механизмов приводило бы к ограниченному результату.
Практическим следствием были бы бесполезные методологические обсуждения: предположим, вы хотите наследовать от класса LINEAR_ITERATOR ; следует ли использовать наследование модуля или наследование типа? Можно приводить аргументы в защиту одного и другого решения. Вклад этого предложения в критерий качества нашего ПО и скорость его создания будут фактически нулевыми.
В упражнении У6.8 требуется проанализировать наши категории, отнеся их либо к наследованию модуля, либо к наследованию типа. |
Интересно задуматься и о тех последствиях в усложнении языка, к которым привело бы такое разделение. Наследование сопровождается несколькими вспомогательными механизмами, большинство из которых необходимо обоим видам:
RECTANGLE, переопределяющий perimeter от POLYGON ) и для расширения модуля (принцип Открыт-Закрыт требует при наследовании модуля сохранения гибкости изменений, без чего будет потеряно одно из главных преимуществ ОО-метода).STATE_INSTITUTIONS в географической или выборной информационной системе может иметь потомка LOUISIANA_INSTITUTIONS, отражающего особенности политической структуры штата Луизиана, поэтому вполне ожидаемо желание потомка переименовать компонент counties, задающий список округов штатов, в parishes - имя, используемое для округа в данном штате.select со всеми недостатками при появлении новых случаев. Появляются и другие вопросы - когда разделять компоненты, а когда их дублировать.Все это ради преимуществ пуристской точки зрения - ограниченной и спорной. Нет ничего плохого в защите спорной точки зрения, но следует быть крайне осторожным в нововведениях и учитывать их последствия для . Он не только в деталях объяснил все недостатки этой инструкции, основываясь на теории конструирования ПО и процесса его выполнения, но и показал, как можно без труда заменить этот механизм. В данном же случае убедительные аргументы не представлены, по крайней мере, я не увидел, почему "плохо" иметь единый механизм, покрывающий как наследование модулей, так и наследование типа.
Помимо общих возражений, основанных на предвзятых идеях о том, каково должно быть наследование, приводится лишь один серьезный аргумент против единого механизма наследования - сложность статической проверки типов, возникающая при этом подходе. Эта проблема полностью анализировалась в лекции 17 курса "Основы объектно-ориентированного программирования", бремя ее решения возлагается на компиляторы, которые признают это бремя разумным, если этим существенно облегчается жизнь разработчиков.
Как покажет в конечном итоге это обсуждение, возможность иметь единый механизм для наследования модуля и типа является результатом важнейшего решения ОО-конструирования ПО: унификации концепций модуля и типа в едином понятии класса. Если мы принимаем классы как модули и как типы, то следует принять и наследование, аккумулирующее модули и типы.
Первая категория наследования из нашего списка, вероятно, единственная, с которой согласится каждый, по меньшей мере, тот, кто принимает наследование: то, что мы можем назвать чистым наследованием
Большая часть данного обсуждения применима и к наследованию с ограничением, чье принципиальное отличие состоит в том, что от родителя не требуется быть отложенным классом.
Как указывалось во введении в наследование, мощь этой идеи частично происходит от интеграции механизма модуля с механизмом типов: определения нового типа как специального случая существующего типа, определение нового модуля как расширения уже существующего модуля. Большинство из спорных вопросов о наследовании идут от осознания конфликтов между этими двумя точками зрения. При наследовании
Наследование
FIGURE $$\gets$$ CLOSED_FIGURE $$\gets$$ POLYGON $$\gets$$ QUADRANGLE $$\gets$$ RECTANGLE $$\gets$$ SQUARE DEVICE $$\gets$$ FILE $$\gets$$ TEXT_FILESHIP $$\gets$$ LEISURE_SHIP $$\gets$$ SAILBOATACCOUNT $$\gets$$ SAVINGS_ACCOUNT $$\gets$$ FIXED_RATE_ACCOUNTВ любом из этих случаев четко идентифицируется множество объектов, описываемое родительским типом, и мы выделяем подмножество, характеризуемое некоторыми свойствами, которыми обладают не все объекты родителя. Например, текстовый файл является файлом со специальными свойствами, вытекающими из того, что элементы файла являются строками текста, что нехарактерно, например, для
Общее правило при наследовании
Некоторые из примеров, такие как |
Наследование , QUADRANGLE etc., или следует разделять объекты на правильные многоугольники ( EQUILATERAL_POLYGON, и т. д.) и неправильные?
Несколько стратегий доступно для
Тип - это не просто множество объектов. Он характеризуется также применимыми операциями (компонентами) и их семантическими свойствами (утверждениями:
Правила, применяемые к утверждениям, поддерживают этот взгляд на подтипы:
Для компонентов ситуация более тонкая. С точки зрения на подтип требуется, чтобы все операции, применимые к экземплярам родителя, должны быть применимы к экземплярам наследника. Внутренне это всегда верно: даже для класса 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 курса "Основы объектно-ориентированного программирования" при обсуждении
Исключения [или genus, species etc.] характеризуются свойством XYZ ", которому не предшествовало бы " большинство ", " обычно " или за которым не следовало бы " за исключением нескольких случаев ". Это справедливо на всех уровнях иерархии, даже для наиболее фундаментальных категорий, для которых, казалось бы, существуют бесспорные критерии!
Если вы думаете, например, что просто отличать животных от растений, то ознакомьтесь с текстом, взятым из популярного учебника (курсив добавлен):
Отличие растений от животных Есть несколько общих факторов, позволяющих отличать растения от животных, хотя есть многочисленные исключения. Перемещение. Большинство животных свободно передвигаются, в то время как редкие растения могут перемещаться в окружающем их пространстве. Большинство растений имеют корни в почве или прикреплены к скалам, деревьям или другим материалам. Пища. Зеленые растения, содержащие хлорофилл, сами производят еду для себя, большинство животных питаются растениями или поедают других животных. Рост. Растения обычно растут от концов своих ветвей и корней и от внешних участков ствола в течение всей жизни. У животных рост обычно идет во всех частях их тела и прекращается при достижении зрелости. Химическая регуляция. Для растений и для животных общим является наличие гормонов и других химикалиев, регулирующих |
Те же комментарии применимы к другим областям изучения. Это в полной степени относится и к области человеческой культуры - классификация естественных языков внесла свой вклад в разработку систематической
Известный пример из зоологии, ставший уже клише, иллюстрирует таксономию исключений. (Помните, однако, что это только аналогия, а не программистский пример.) Птицы летают. (Класс BIRD должен иметь процедуру .) Страус - птица, но страус не летает. При создании наследника - класса OSTRICH - необходимо будет указать, что эта самая большая из птиц не летает.
При классификации птиц можно было бы попытаться разделить их на две категории - летающие и не летающие. Но это конфликтовало бы с другими возможными и более важными критериями, применяемыми в классификации, ставшей стандартной.
Пример со страусом ( OSTRICH ) имеет интересный поворот. Хотя, к сожалению, они и не сознают этого, страусы фактически должны летать. Молодые поколения теряют этот наследственный навык из-за случайности эволюционной истории. Анатомически страусы являются самой совершенной аэродинамической машиной среди всех птиц. Это свойство, немного осложняя работу профессионального таксономиста, (хотя ее может облегчить его коллега - профессиональный таксидермист) не помешает классифицировать страусов в иерархии птиц.
В программистских терминах класс OSTRICH будет наследником BIRD, скрывая наследуемый компонент .
Все наши усилия (по классификации) кажутся беспомощными на фоне множественности отношений живых существ, окружающих нас. Эта битва Человека и Природы во всей ее бесконечности описана величайшим ботаником Гете. Одно можно сказать с уверенностью, Человек в ней всегда будет побежден. Анри Бэйлон
Практика разработки ПО и аналогии природного мира свидетельствуют, что даже при самом тщательном проектировании остаются исключения redeem класса NEW_MORTGAGE или из 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 ), и count (переименованный ), доступны для ARRAYED_STACK, хотя и не экспортируются его клиентам. Классу они необходимы для реализации компонентов STACK.
Так что концептуально ничего ошибочного нет в наследовании в интересах реализации. Показательно сравнение с контрпримером, изучаемым в начале этой лекции, класс CAR_OWNER свидетельство непонимания концепции наследования, класс ARRAYED_STACK задает хорошо определенную форму отношения "is-a".
Один недостаток здесь есть: механизм наследования, позволяющий ограничить доступность экспорта наследуемых компонентов (другими словами, разрешение предложения export ), делает более трудной статическую проверку типов. Но это трудности разработчиков компиляторов, а не разработчиков прикладного ПО.
Давайте проверим, как можно выполнить эту работу без использования наследования. Для нашего примера это было уже сделано в классе STACK2 из предыдущих лекций. Он имеет атрибут типа ARRAY [G] и процедуры стека, реализованные в следующем виде (утверждения опущены):
put (x: G) is
-- Добавляет x на вершину
require
...
do
count := count + 1
representation.put (count, x)
ensure
...
end
Каждая манипуляция с представлением требует вызова компонента ARRAY с как цели. Платой являются потери производительности: минимальные по памяти (атрибут ), более серьезные по времени (связанные с , накладные расходы, добавляемые при вызове каждой операции).
Предположим, что проблемы эффективности можно игнорировать. Остается еще одна утомительная необходимость выписывать перед каждой операцией префикс " ". Это придется делать для всех классов, реализующих различные структуры данных - стеки, списки, очереди, все, что реализуется массивами.
ОО-разработчики ненавидят утомительные, повторяющиеся операции. "Встроенное повторение" - вот наш девиз. Если некий образец повторно встречается в множестве классов, естественной и здоровой реакцией является попытка понять общую абстракцию и встроить ее в класс. Абстракция здесь нечто подобное "структуре данных, имеющей доступ к массиву и его операциям".
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
Компоненты |
При таком определении класса не вызывает споров, что классы, такие как ARRAYED_STACK или ARRAYED_LIST, становятся наследниками ARRAYED: они действительно описывают структуры на массивах. Эти классы могут теперь использовать 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 компонента), описывающих
Рассмотрим, например, лексический анализатор, ответственный за идентификацию лексем входного текста. TOKENIZER, необходим доступ к кодам символов для их классификации на цифры, буквы и т. д. Такой класс воспользуется льготами и наследует эти коды от ASCII:
class TOKENIZER inherit ASCII feature
... Программы класса могут использовать компоненты Blank, Case_diff и другие...
end
К классам, подобным ASCII, относятся иногда неодобрительно, но прежде чем перейти к методологической дискуссии, взглянем на еще один пример льготного наследования.
Второй пример демонстрирует наследование программ общего вида, а не константных атрибутов.
Предположим, мы хотим обеспечить общий механизм, позволяющий просмотр всех элементов (итерирование) некоторой структуры данных, например линейных структур, таких как списки. "Итерирование" означает выполнение некоторой процедуры, скажем, action, на элементах этой структуры, просматриваемых в последовательном порядке. Нам хочется обеспечить несколько различных механизмов итерирования, включающих применение action ко всем элементам, удовлетворяющим условию, заданному test, ко всем элементам до появления первого, удовлетворяющего test, или до первого, не удовлетворяющего этой функции. Ну и так далее, вариантов можно придумать много. Система, использующая этот механизм, должна быть способна применять его к произвольным компонентам action и test.
С первого взгляда может показаться, что итерирующие компоненты должны принадлежать классам, описывающих соответствующие структуры данных, таким как LIST или SEQUENCE.
В упражнении У6.7 предлагается показать, что это неправильное решение.
Предпочтительнее ввести независимую иерархию
(рис 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, вместо того чтобы быть описанной в классе, может наследоваться от другого родителя. В этом случае action, наследуемую от одного родителя под именем justify (здесь переименование существенно), с эффективной justify, наследуемой от другого родителя. Реально, это и есть брак по расчету.
Класс LINEAR_ITERATOR является замечательным примером класса поведения (behavior class), рассматривая общее поведение и оставляя открытыми специфические компоненты, так чтобы его потомки могли подключить специальные варианты.
Два примера, ASCII и LINEAR_ITERATOR, демонстрируют два главных варианта льготного наследования:
Как отмечалось ранее, возможна комбинация этих вариантов в единой наследственной связи. Вот почему льготное наследование задается одной категорией, а не двумя.
Некоторые рассматривают льготное наследование как злоупотребление механизмом наследования, как некоторую форму хакерства.
Главный вопрос, заслуживающий рассмотрения, связан не столько с наследованием, сколько с тем, как определены классы ASCII и LINEAR_ITERATOR. Как всегда, при рассмотрении проекта класса, следует спросить себя: "Действительно ли мы описали значимую абстракцию данных - множество объектов, характеризуемых
Для этих примеров ответ менее очевиден, чем для классов RECTANGLE, BANK_ACCOUNT или LINKED_LIST, но, по сути, он тот же:
ASCII представляет абстракцию: "любой объект, имеющий доступ к ASCII ".LINEAR_ITERATOR представляет абстракцию: "любой объект, способный выполнять последовательные итерации на линейной структуре". Такой объект имеет тенденцию быть "Как только эти абстракции принимаются, наследственные связи не вызывают никаких проблем: экземпляру TOKENIZER необходим "доступ к
charset: ASCII ... create charset
При каждом использовании кода символа потребуется задавать целевой объект charset.Lower_a. Присоединяемый объект charset не играет никакой полезной роли. Те же комментарии справедливы и для класса LINEAR_ITERATOR. Но если классу необходимы несколько видов итерации, то тогда создание объектов-action и test становится разумным.
Коль скоро мы хотим иметь объекты-
Вероятно, наиболее трудная проблема наследования возникает при наличии альтернативных критериев классификации.
Традиционная классификация в естественных науках использует единственный критерий (возможно, объединяющий несколько качеств) на каждом уровне: позвоночные или беспозвоночные, ветви, обновляемые один или несколько раз в год, и тому подобное. Результатом будет то, что называется иерархией единственного наследования, чье главное преимущество - простота классификации. Конечно, возникают проблемы, поскольку природа определенно не пользуется единственным критерием. Это очевидно для всякого, кто когда-либо пытался провести классификацию, вооружившись книгой по ботанике с традиционной классификацией Линнея.
При разработке ПО, где единый критерий кажется ограничительным, мы можем использовать все приемы множественного и особенно дублирующего наследования, которыми мы овладели при изучении предыдущих лекций. Рассмотрим, например, класс EMPLOYEE в системе
Оба эти критерия приводят к правильным классам-потомкам. При этом мы не впадаем в таксоманию, так как идентифицируемые классы, такие как TEMPORARY_EMPLOYEE по первому критерию и MANAGER по второму, действительно характеризуются специальными компонентами, не применимыми к другим категориям. Как же следует поступать?
В первой попытке введем все варианты на одном и том же уровне (рис. 6.12).
Для простоты на этой схеме имена классов сокращены. В реальной системе мы действуем более аккуратно и используем, как положено, полные имена, такие как |
Получившуюся
(рис 6.12) Беспорядочная классификация
Помня об идеях использования наследования для классификации, следует ввести промежуточный уровень, описывающий конкурирующие критерии классификации (рис. 6.13).
Появились два вида служащих. Заметьте, имя CONTRACT_EMPLOYEE не означает служащего, имеющего контракт, а служащего, характеризуемого контрактом (он может не иметь контракта!). Имя класса для другого вида означает "служащий, характеризуемый своей специальностью".
То, что эти имена кажутся неестественными, отражает определенную сложность, характерную для наследования видов. При наследовании CONTRACT_EMPLOYEE и SPECIALTY_EMPLOYEE не напрямую, но через наследование от классов и 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.
Эта схема, как вы заметили, почти идентична образцу проектирования с описателями, изучаемому ранее в этой лекции.
Такая техника может использоваться вместо наследования видов. Это усложняет структуру из-за введения независимой иерархии, нового атрибута (здесь ) и соответствующего
(рис 6.14) Многокритериальная классификация с независимой иерархией, построенной для клиента
Нет ничего необычного в рассмотрении наследования видов на ранних этапах анализа проблемной области, когда обсуждаются фундаментальные концепции и рассматриваются несколько равно привлекательных критериев классификации. В дальнейших исследованиях часто оказывается, что один из критериев начинает доминировать, выступая в качестве основы построения иерархической структуры. Тогда, как показывает наше обсуждение, следует отказаться от
Все же я нахожу наследование видов полезным при выполнении следующих трех условий:
Примером приложения, удовлетворяющего этим критериям, является библиотека Base с ее структурой иерархии на верхних уровнях, описанная в последней лекции этой книги. Классы, полученные в результате этих усилий, в деталях описаны в [M 1994а]. Они построены в традиции естественных наук с применением таксономических принципов систематической классификации основных программистских структур. Верхняя часть этой иерархии выглядит так:
(рис 6.15) Классификация, основанная на видах фундаментальных программистских структурКлассификация на первом уровне ( BOX, COLLECTION, ) основана на типах; уровень ниже (и многие другие, не показанные на рисунке) задают классификацию
COLLECTION определяет доступ к элементам. Класс SET позволяет определить сам факт присутствия элемента, в то время как BAG позволяет также посчитать число вхождений данного элемента. Дальнейшие уточнения включают такие абстракции доступа, как SEQUENCE (элементы доступны последовательно), STACK (элементы доступны в порядке, обратном их включению) и так далее.BOX определяет представление элементов. Варианты включают конечные и бесконечные структуры. Конечные структуры могут быть ограниченными и не ограниченными. Ограниченные структуры могут быть фиксированными или изменяемого размера.TRAVERSABLE определяет способы обхода структур.Интересно отметить, что эта иерархия не начиналась, как иерархия видов. Начальная идея состояла в том, чтобы определить BOX, COLLECTION и как несвязанные классы, каждый, задающий вершину своей независимой иерархии. Затем при описании реализации любой специальной структуры данных использовать
(рис 6.16) Построение структуры данных комбинированием абстракций путем множественного наследованияНо затем мы осознали, что независимые семейства классов BOX, COLLECTION и не лучший способ: им всем потребовались некоторые общие компоненты, в частности 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 обрели родителей в результате
В группе разработчиков, правильно применяющих ОО-метод, это регулярное совершенствование уровня абстракции ПО и, как следствие, его качества, заметно ощущается членами команды и служит источником постоянной мотивации.
Наследование никогда не перестанет удивлять нас своей мощью и универсальностью. В этой лекции мы пытались как можно лучше описать, что реально означает наследование и как можно его использовать для получения всех преимуществ. Перечислю несколько центральных выводов.
Прежде всего не следует бояться разнообразия способов использования наследования. Запрет множественного или льготного наследования не достигает никакой другой цели, кроме нанесения вреда самому себе. Механизмы должны помогать нам, используйте их правильно, но используйте их!
Наследование является, как правило, техникой поставщика. Это мощное оружие нашего арсенала в сражениях с противниками (в частности сложностью - безжалостным врагом разработчика).
Конечно, все ПО проектируется для клиентов, клиентские потребности управляют процессом. Множество классов хорошо тогда, когда оно предоставляет замечательные службы клиентам: интерфейсы и ассоциированные с ними реализации, являющиеся полными, свободными от неприятных сюрпризов (таких как неожиданные потери производительности), просты в использовании, легки в обучении, запоминании, допускают расширяемость. Для достижения этих целей проектировщик свободен в использовании наследования и других ОО-методов любым способом, который ему нравится. Победителей не судят.
Напомню также, при проектировании структуры наследования целью является конструирование ПО, а не философия. Редко существует единственное, или лучшее решение. Для нас "лучшее" означает лучшее для целей некоторого класса клиентского приложения. Это особенно верно, когда мы уходим от таких областей как математика или компьютерные науки, где существуют широко применимые теории, и попадаем в мир деловых приложений.
В известной степени в этом есть некоторый комфорт. Натуралист, классифицирующий растения и животных, должен мыслить в абсолютных категориях. В программном мире эквивалентом является создание библиотек общецелевого назначения (фундаментальные структуры данных, графика, базы данных). Большей же частью ваши цели скромны, вам необходимо спроектировать хорошую иерархию, ту, которая удовлетворяет потребностям клиентов.
Заключительный урок этой лекции обобщает комментарий, сделанный при обсуждении льготного наследования: принципиальная трудность построения структуры классов не в наследовании, она в поиске абстракций. Если правильные абстракции идентифицированы, то структура наследования следует из них. Для поиска абстракций следует руководствоваться тем, чем мы руководствуемся на протяжении всей книги, - абстрактными типами данных.
CAR_OWNER.)Основным учебником по
В [Breu 1995] также содержатся полезные концепции, основанные на более ограничительном подходе к наследованию, чем в рассматриваемой лекции.
Техника, подобная "
При подготовке этой статьи использовались комментарии нескольких биологов, ведущих доступные в Интернете Web-ресурсы по
Руководства по теории классификации или систематике приведены в следующем разделе.
Данное приложение является дополнительным материалом и не используется в остальной части курса. Изучение усилий по
Мы прислушиваемся к мнениям биологов, звучащих с двух противоположных сторон: одних, отстаивающих априорную форму классификации, сверху вниз, дедуктивную и основанную на "естественном" порядке вещей, идущую к нам от кладистов, начиная с Линнея, других, отстаивающих эмпирическую, индуктивную точки зрения фенетиков снизу вверх, советующих нам наблюдать и собирать...
Вероятно, подобно эволюционным таксономистам, хотелось бы взять понемногу от тех и от других.
Напишите полностью класс STACK и его потомка ARRAYED_STACK, набросок которого дан в этой лекции, используя технику "брака по расчету".
Представьте себе, что введенная классификация форм наследования представляла бы собой
(Это упражнение пришло из примера Филиппа Дрикса.)
Рассмотрим отложенный класс STACK с процедурой put для вталкивания элемента на вершину с full (которая также может быть названа extendible ; когда вы ознакомитесь с упражнением, то заметите, что выбор имени может влиять на возможные решения).
Задача о Ханойских башнях, известная по многим учебникам как пример рекурсивной процедуры, идет от работы Эдварда Лукаса, Париж, 1883 г.
Рассмотрим теперь известную задачу о Ханойских башнях, где нужно перенести пирамиду (башню), составленную из отдельных дисков разного размера, с одного стержня на другой, используя третий, соблюдая правило: диск может быть переложен только на диск большего размера.
Можно ли определить класс HANOI_STACK, представляющий такие STACK? Если да, каким должен быть этот класс? Если нет, может ли HANOI_STACK как-то использовать STACK? Напишите класс полностью для различных возможных решений, обсудите все "за и против" каждого решения. Установите, какое из них предпочтительнее и объясните ваш выбор.
Реализация нашего примера наследования класса использовала атрибут связного списка vertices для представления числа сторон многоугольника. Следует ли вместо этого наследовать от LINKED_LIST [POINT]?
Приведите один или несколько примеров функциональной вариации. Для каждого из них обсудите, дают ли они законный образец принципа Открыт-Закрыт или являются примерами того, что называется "организованным хакерством".
Для каждого из следующих случаев укажите, к какому виду наследования он относится:
SEGMENT от OPEN_FIGURE ;COMPARABLE (объекты, поставляемые с отношением полного порядка), наследуемые от PART_COMPARABLE (объекты с отношением частичного порядка);EXCEPTIONS.Разумно ли компоненты while_do и ему подобные) включать в классы, описывающие структуры данных, которые они итерируют, такие как LIST? Рассмотрите следующие аргументы:
action и test, выбираемых приложением;Предположим, мы разрабатываем язык с двумя типами наследования: расширением модуля и подтипами. К какому из этих типов следует отнести категории наследования, идентифицируемые в этой лекции?
Из рассмотренных видов наследования этой лекции между родителем A и наследником B, для каких на практике характерно использование полиморфного присоединения, другими словами присваивание x := y или соответствующая передача аргументов x типа A и y типа B?
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.