Опубликован: 23.10.2005 | Уровень: специалист | Доступ: платный
Лекция 6:

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

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

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

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

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

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

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

  • A1 Функция определена для c1, c2 и c3.
  • A2 Множество VALUE (множество цели для функции) является супермножеством T'1 \cup  T'2 \cup  T'3.
  • A3 Значения функции для c1 лежат в T'1 и так далее.

Тогда, если вспомнить, что функция является специальным случаем отношения и что отношение является множеством пар (например, в ранее упоминаемом случае экземпляр класса A может быть промоделирован функцией {<a1, 25>}, а экземпляр класса B - {<a1, 1>, <b1, -2.5>} ), мы получаем ожидаемое свойство - B' является подмножеством A'. Заметьте, здесь уже элементы обоих множеств являются парами и первая функция задает все возможные отображения второго атрибута.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Для этой категории наследования класс B будет отложенным; A обычно эффективным, но может быть частично отложенным.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Алексей Щербанов
Алексей Щербанов
Россия, г. Оренбург
Ксения Маковецкая
Ксения Маковецкая
Россия, Москва, МГЮА им. О.Е. Кутафина, 2014