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

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

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

Что происходит с контрактами?

В определение метода входит не только имя, сигнатура и (для эффективных методов) реализация, — определение может также включать предусловие и постусловие. Для класса может быть задан инвариант класса. Мы знаем, что означают эти понятия в отсутствие наследования. А как наследование влияет на эту картину?

Аккумуляция инварианта

Первое правило воздействует на инвариант класса. Оно отражает взгляд на наследование как на отношение "является" и на роль наследования как механизма таксономии. Указание того, что класс TAXI является наследником класса VEHICLE, не только избавляет от дублирования кода, но и задает полиморфизм: когда ожидается транспортное средство LIST[VEHICLE], то возможно появление такси. Отсюда следует, что любое ограничение, определенное для экземпляров родительского класса, должно применяться и к наследникам. В классе VEHICLE находим:

invariant
  not_too_small: count >= 0
  not_too_large: count <= capacity

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

Эти наследуемые предложения можно увидеть, просматривая плоский или контрактный облик класса. Понятно, что наследник может вводить дополнительные ограничения. Действительно, в классе TAXI можно видеть предложение:

invariant
   legal_limit: capacity = 4
   …Другие предложения, которые воздействуют на методы, специфические для такси…

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

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

Следующее определение задает семантику.

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

Инвариантом класса является утверждение (p1 and... and pn) and then i, где i является утверждением, явно заданным в собственном инварианте класса (или True в случае его отсутствия), а p1... pn являются (рекурсивно) инвариантами родительских классов, если таковые есть.

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

Ослабление предусловия и усиление постусловия

Вторая проблема — влияние наследования на предусловие и постусловие — ведет к важному правилу разработки ПО. Для ее понимания следует рассмотреть ее в контексте с полиморфизмом и динамическим связыванием:

(рис 3.1) Контекст адаптации контракта

Рассмотрим метод r из класса поставщика S, заданный с предусловием и постусловием (названными α и β на рисунке). Потомок T переопределяет метод r, что может быть эффективизацией (заданием реализации), если r был отложенным в S. Возникает вопрос: какие изменения в контракте (α и β) допустимы для новой версии r?

Для получения правильного ответа следует рассмотреть эту ситуацию с позиций класса C — клиента класса S, в методах которого встречается вызов x.r (...), где x по объявлению имеет тип S. Контракт устанавливает права и обязанности клиента: он должен перед вызовом метода гарантировать выполнение предусловия, в этом случае по завершении метода ему гарантируется выполнение постусловия. Возможна, например, такая схема работы клиента:

if x._ then
  x.r (...)
    — Здесь гарантируется, что выполняется x.β
end

Это прямое применение принципа проектирования по контракту. Но теперь включается полиморфизм. Наш x, типа S по объявлению, во время выполнения не обязан быть присоединенным к прямому экземпляру класса S, он может обозначать объект класса T или экземпляр любого другого класса потомка S.

Из-за динамического связывания будет вызвана версия r, переопределенная потомком. Но, конечно же, клиенту нет необходимости знать это — он заключил контракт с классом S, более того, во время написания клиентского класса C класс T мог вообще не существовать. Приведенный выше код мог быть частью кода такого метода класса C :

do_something_with_an_S_object (x: S)

Как видите, в данном случае x — это аргумент метода, имеющий тип S. Фактический аргумент, приходит в класс C, возможно, из внешнего мира, и его тип должен быть лишь согласован с S, так что сам класс C изначально может находиться в неведении о фактическом типе x в момент вызова. Класс T мог быть написан и добавлен в иерархию наследования спустя два года после создания класса C. Некий другой программист, знающий о появлении класса T, мог написать в своей программе: c1.do_something_with_an_S_object(t1), где c1 — класса C, а t1 — класса T. Можно только пожалеть автора исходного кода класса C, который должен написать клиентский код и гарантировать его корректность даже в том случае, когда он имеет дело с объектами, не существовавшими в момент написания кода.

Для обеспечения корректности C может опираться на известные ему свойства поставщиков, таких как S, и их методов, таких как r. Это строго ограничивает потомков, например T, не позволяя им "баловаться" с контрактом, допуская, например, такие вольности:

  • усиливать предусловие r в T. В этом случае вызов уже не гарантировал бы нормального выполнения, поскольку клиент гарантирует выполнения условия α, а этого становится недостаточно для объектов типа T ;
  • ослаблять постусловие r в T. В этом случае при вызове клиенту не гарантировалось бы выполнение ожидаемого постусловия β.
  • Другими словами, T как субподрядчик должен выполнять обязательства, взятые исходным подрядчиком S, которого только и знают такие клиенты, как C.

    В этом обсуждении "a сильнее b" означает (a implies b) and not (a = b). Утверждение "быть слабее" означает обращение приведенной формулы.

    Из этих наблюдений следует правило:

    Правило переопределения контракта

    Переопределяемая версия метода может только: сохранить или ослабить предусловие метода; сохранить или усилить постусловие метода.

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

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

    Как можно в языке программирования задать переопределение контракта? Решение, принятое в Eiffel (и в других нотациях, использующих проектирование по контракту), просто:

    при переопределении метода не разрешается запись предусловия и постусловия в базисной форме — require и ensure ;

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

    для ослабления предусловия следует использовать предложение в форме require else new_pred. В этом случае семантика такова: переопределяемый метод имеет предусловие old_pred or else new_pred, где old_pred наследуемое предусловие;

    для усиления постусловия следует использовать предложение в форме ensure then new_post. В этом случае семантика такова: переопределяемый метод имеет предусловие old_post and then new_post, где old_post — наследуемое постусловие.

    Это решение удовлетворяет правилу, поскольку по правилам логики: "a implies a or b" и "а and b implies a".

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

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

    Предусловие take в классе DISPATCH_TAXI, отражающее наше обсуждение, имеет вид:

    require else
    in_zone: customer.is_in_zone (Current)
    

    Много других примеров можно найти при анализе текстов библиотеки EiffelBase.

    Контракты в отложенных классах

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

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

    Пример для forth является типичным:

    forth
        — Передвинуть курсор к следующей позиции
      require
        in_range: not after
      deferred
      ensure
        increased: index = old index + 1
      end
    

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

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

    Точно так же search позволяет подключать различные версии forth и других программ, определенных потомками класса LINEAR, но при условии, что они удовлетворяют контрактам, заданным в классе LINEAR для этих методов.

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

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

    Как для отложенных, так и для эффективных классов правила адаптации контрактов служат основой корректного использования наследования. Полиморфизм и динамическое связывание являются мощным механизмом, который по этой причине одновременно и опасен. Так как каждый тип может адаптировать наследуемый компонент, как можно гарантировать, что вызов myvehicle. turn _ left после переопределения не заставит ваше транспортное средство поворачивать направо, или останавливаться, или ездить по кругу? Гибкость, которую привносит в программирование комбинация переопределения, полиморфизма и динамического связывания, может зайти слишком далеко. Как проектировщик метода turn _ left, требующего левого поворота, вы хотите позволить потомку дать собственную реализацию, но при условии сохранения исходной семантики, гарантирующей левый поворот.

    Правило переопределения контракта и связанный с ним механизм языка ( require else... ) обеспечивают нужную степень контроля. Можно указать границы, в рамках которых допустима свобода реализации. Наследование и связанная с ним техника — не просто мощная форма повторного использования, но и техника субподрядов. Классы используют переопределение как субподряд на выполнение некоторых операций потомками. Из-за полиморфизма и динамического связывания клиент не знает, какой субподрядчик будет работать в текущем вызове. Ситуация аналогична покупке iPhone: вы не знаете, где сделана та или иная часть аппарата — в Шанхае, Тайване, Бангалоре или Будапеште. Правило переопределения контракта с успехом может быть названо правилом сохранения честного субподряда.

    Общая структура наследования

    Наследование позволяет нам обеспечить общий каркас, где каждый элемент ПО имеет свое четкое место. Большинство ОО-языков (значимым исключением является С++) определяют специальный класс — общего предка всех классов, иногда называемого Object (Smalltalk, Java, C#). В Eiffel такой класс называется ANY. Он входит в библиотеку "Kernel", которая содержит также некоторые фундаментальные классы, тесно связанные с определением языка (ARRAY, STRING, базисные типы данных — BOOLEAN, INTEGER, REAL, CHARACTER ).

    Показанные на следующем рисунке классы А, В могут быть классами, написанными вами или мной. Правило, определяющее роль ANY, просто: любой класс, в котором даже не задано предложение наследования inherit, записанный в виде

    class A feature... end
    

    понимается, как если бы он был явно записан в форме

    class A inherit ANY feature... end
    

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

    (рис 3.2) Общая структура наследования

    Имеет место следующее свойство:

    Теорема об универсальном наследовании и согласовании (Eiffel)

    Каждый класс является наследником ANY. Каждый тип согласован с ANY.

    Класс ANY включает общецелевые компоненты, полезные для всех классов: is_equal и другие полезные методы. Мы уже встречались с использованием метода print этого класса, который позволяет печатать представление, заданное по умолчанию для любого объекта. Тип ANY является наиболее общим типом, с ним согласован любой другой тип. Переменная, объявленная как v: ANY, является полиморфной и может обозначать объекты любого типа.

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

  • Как тип, он позволяет задавать тип Void — предопределенное значение, представляющее ссылку void (значение, не связанное ни с каким объектом).
  • Как класс, он поддерживает скрытие информации. Как отмечалось ранее, мы объявляем скрытые компоненты класса, используя предложение в форме feature { NONE }. Формально это означает, что компонент экспортируется только классу NONE — фактически, никакому классу.
  • Данный синтаксис задает специальную форму выборочного экспорта. Предложение feature можно задавать в форме feature {C, D,...}, означающей, что компоненты, объявленные в разделе feature, экспортируются классам, указанным в скобках, и их потомкам.

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

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

    Почувствуй методологию

    Развенчание легенды о сложности множественного наследования

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

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

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

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

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

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

    class TROLLEY inherit
      TRAM
        redefine add_station, remove_station end
      BUS
    feature
    …
    end
    

    Простой пример может быть найден в базисных библиотеках. Класс NUMERIC задает объекты, поставляемые со стандартными математическими операциями:

    deferred class NUMERIC feature
      plus alias "+" (other: NUMERIC): NUMERIC deferred end
      minus alias "-" (other: NUMERIC): NUMERIC deferred end
      times alias "_" (other: NUMERIC): NUMERIC deferred end
      divided alias "/" (other: NUMERIC): NUMERIC deferred end
    end
    

    (Это только набросок. Полный текст класса можно увидеть в EiffelStudio)

    Еще один библиотечный класс, COMPARABLE, задает объекты, которые поставляются с операциями отношениями, задающими полный порядок:

    deferred class COMPARABLE feature
      lesser alias "<" (other: NUMERIC): BOOLEAN deferred end
      lesser_or_equal alias "<=" (other: NUMERIC): BOOLEAN do...end
      greater alias ">" (other: NUMERIC): BOOLEAN do...end
      greater_or_equal alias ">=" (other: NUMERIC): BOOLEAN do...end
    end
    

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

    (рис 3.3) Множественное наследование

    Такие языки, как Java и C#, допускают множественное наследование, но не классов, а интерфейсов, подобных, как отмечалось ранее, полностью отложенным классам. Следующий пример иллюстрирует разницу. Класс COMPARABLE нуждается только в одном отложенном свойстве, например lesser (alias <). Все остальные могут быть определены как эффективные компоненты, например:

    lesser_or_equal alias "<=" (other: NUMERIC): BOOLEAN
        — Является ли текущий объект меньше или равным other?
      do
        Result := (Current < other) or (Current ~ other)
      ensure
        definition: Result = ((Current < other) or (Current ~ other))
      end
    

    Аналогично greater(other) определяется как other.lesser(Current). Нетрудно определить и отношение "больше или равно". Класс не просто задает начальную реализацию, но и устанавливает отношения, существующие между операциями, что отражается в постусловиях.

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

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

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

    Множественное наследование приводит к проблемам перегрузки, если класс С наследует от двух классов с идентично названными именами компонентов:

    (рис 3.4) Конфликт имен

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

    class C inherit
      A
      B
    end
    

    Здесь А и В имеют компоненты, названные f. Компиляция класса C в этом случае не пройдет. Необходимо переименование :

    class C inherit
    A rename f as first_f end
    B
    end
    

    Предложение rename (которое может быть скомбинировано с переопределением : rename f as first_f redifine first_f end ) просто указывает, что компонент, известный в А как f, будет известен в классе С под именем first_f. Конечно, мы могли бы переименовать компонент и в классе В или в обоих классах.

    Переименованный компонент все же остается прежним компонентом — компонентом, ранее известным как f в С. Так что для a1:A; c1:C следующие вызовы оба являются правильными:

    a1.f
    c1.first_ f
    

    Конечно же, возникнет ошибка при вызове al.first_f, поскольку понятно, что класс А не знает имя first_f. Вызов cl.f синтаксически правилен, но ссылается на компонент из класса В. Если бы и этот компонент был переименован, то и этот вызов был бы ошибочным. В полиморфной ситуации после присваивания a1:=c1 два приведенных выше вызова имели бы одинаковый эффект, так как al и cl обозначали бы один и тот же объект, а f и first_f обозначали бы один и тот же компонент в соответствующих классах.

    Плоский и контрактный облик класса отражают эффект как переименования, так и переопределения.

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

    Переименование в сравнении с переобъявлением

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

    Когда требуется изменить и сам компонент, и его имя, допускается комбинация переименования и переобъявления.

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

    От множественного наследования к повторному наследованию

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

    к предку:

    (рис 3.5) Повторное наследование

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

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

    Для случая, изображенного на рисунке, первый вопрос, который следует задать относительно f — компонента из класса А : в классе D ему должны соответствовать два компонента или один? Ответ прост и соответствует духу предыдущего обсуждения.

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

    При репликации может возникнуть неоднозначность, когда встречается переопределение. Такая ситуация может встретиться, например, при повторном наследовании от ANY, если один из классов вдоль пути наследования вводит свое собственное понятие копирования и эквивалентности, переопределив copy и is_equal из класса ANY :

    (рис 1.6) Необходимость "select"

    Методы copy и is_equal должны всегда переопределяться совместно, поскольку постусловие copy(other) устанавливает is_equal(other). Любая операция копирования должна убеждаться, что результат эквивалентен цели копирования в соответствии с локальным определением эквивалентности.

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

    Теперь D наследует эти методы от LIST, а также от класса C, который сохранил версии по умолчанию, наследованные от ANY. Все это работает в соответствии с предыдущими правилами. Класс D должен переименовать компоненты во избежание конфликта имен, чтобы они дублировались.

    Проблема возникает в связи с полиморфизмом. Рассмотрим a типа ANY и dl типа D. Выполним присваивание, а потом вызов:

    a : = dl
    a.copy (...)
    

    В этом случае при динамическом связывании возникает неоднозначность: для родительского метода copy у потомка имеются две реализации. Возникает вопрос: следует ли для связывания использовать версию LIST (известную как copy в классе) или С-версию ( Ccopy )? Ситуация возникает всякий раз, когда одновременно встречается репликация и переопределение. Для устранения неоднозначности требуется ввести предложение select. Вот как следует записать класс D :

    class D inherit
      LIST [T] select copy, is_equal end
      C rename copy as C_copy, is_equal as C_is_equal end
    feature
       ... Остаток текста класса...
    end
    

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

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

    Страницы:

    Что происходит с контрактами?

    В определение метода входит не только имя, сигнатура и (для эффективных методов) реализация, — определение может также включать предусловие и постусловие. Для класса может быть задан инвариант класса. Мы знаем, что означают эти понятия в отсутствие наследования. А как наследование влияет на эту картину?

    Аккумуляция инварианта

    Первое правило воздействует на инвариант класса. Оно отражает взгляд на наследование как на отношение "является" и на роль наследования как механизма таксономии. Указание того, что класс TAXI является наследником класса VEHICLE, не только избавляет от дублирования кода, но и задает полиморфизм: когда ожидается транспортное средство LIST[VEHICLE], то возможно появление такси. Отсюда следует, что любое ограничение, определенное для экземпляров родительского класса, должно применяться и к наследникам. В классе VEHICLE находим:

    invariant
      not_too_small: count >= 0
      not_too_large: count <= capacity
    

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

    Эти наследуемые предложения можно увидеть, просматривая плоский или контрактный облик класса. Понятно, что наследник может вводить дополнительные ограничения. Действительно, в классе TAXI можно видеть предложение:

    invariant
       legal_limit: capacity = 4
       …Другие предложения, которые воздействуют на методы, специфические для такси…
    

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

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

    Следующее определение задает семантику.

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

    Инвариантом класса является утверждение (p1 and... and pn) and then i, где i является утверждением, явно заданным в собственном инварианте класса (или True в случае его отсутствия), а p1... pn являются (рекурсивно) инвариантами родительских классов, если таковые есть.

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

    Ослабление предусловия и усиление постусловия

    Вторая проблема — влияние наследования на предусловие и постусловие — ведет к важному правилу разработки ПО. Для ее понимания следует рассмотреть ее в контексте с полиморфизмом и динамическим связыванием:

    (рис 3.1) Контекст адаптации контракта

    Рассмотрим метод r из класса поставщика S, заданный с предусловием и постусловием (названными α и β на рисунке). Потомок T переопределяет метод r, что может быть эффективизацией (заданием реализации), если r был отложенным в S. Возникает вопрос: какие изменения в контракте (α и β) допустимы для новой версии r?

    Для получения правильного ответа следует рассмотреть эту ситуацию с позиций класса C — клиента класса S, в методах которого встречается вызов x.r (...), где x по объявлению имеет тип S. Контракт устанавливает права и обязанности клиента: он должен перед вызовом метода гарантировать выполнение предусловия, в этом случае по завершении метода ему гарантируется выполнение постусловия. Возможна, например, такая схема работы клиента:

    if x._ then
      x.r (...)
        — Здесь гарантируется, что выполняется x.β
    end
    

    Это прямое применение принципа проектирования по контракту. Но теперь включается полиморфизм. Наш x, типа S по объявлению, во время выполнения не обязан быть присоединенным к прямому экземпляру класса S, он может обозначать объект класса T или экземпляр любого другого класса потомка S.

    Из-за динамического связывания будет вызвана версия r, переопределенная потомком. Но, конечно же, клиенту нет необходимости знать это — он заключил контракт с классом S, более того, во время написания клиентского класса C класс T мог вообще не существовать. Приведенный выше код мог быть частью кода такого метода класса C :

    do_something_with_an_S_object (x: S)
    

    Как видите, в данном случае x — это аргумент метода, имеющий тип S. Фактический аргумент, приходит в класс C, возможно, из внешнего мира, и его тип должен быть лишь согласован с S, так что сам класс C изначально может находиться в неведении о фактическом типе x в момент вызова. Класс T мог быть написан и добавлен в иерархию наследования спустя два года после создания класса C. Некий другой программист, знающий о появлении класса T, мог написать в своей программе: c1.do_something_with_an_S_object(t1), где c1 — класса C, а t1 — класса T. Можно только пожалеть автора исходного кода класса C, который должен написать клиентский код и гарантировать его корректность даже в том случае, когда он имеет дело с объектами, не существовавшими в момент написания кода.

    Для обеспечения корректности C может опираться на известные ему свойства поставщиков, таких как S, и их методов, таких как r. Это строго ограничивает потомков, например T, не позволяя им "баловаться" с контрактом, допуская, например, такие вольности:

  • усиливать предусловие r в T. В этом случае вызов уже не гарантировал бы нормального выполнения, поскольку клиент гарантирует выполнения условия α, а этого становится недостаточно для объектов типа T ;
  • ослаблять постусловие r в T. В этом случае при вызове клиенту не гарантировалось бы выполнение ожидаемого постусловия β.
  • Другими словами, T как субподрядчик должен выполнять обязательства, взятые исходным подрядчиком S, которого только и знают такие клиенты, как C.

    В этом обсуждении "a сильнее b" означает (a implies b) and not (a = b). Утверждение "быть слабее" означает обращение приведенной формулы.

    Из этих наблюдений следует правило:

    Правило переопределения контракта

    Переопределяемая версия метода может только: сохранить или ослабить предусловие метода; сохранить или усилить постусловие метода.

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

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

    Как можно в языке программирования задать переопределение контракта? Решение, принятое в Eiffel (и в других нотациях, использующих проектирование по контракту), просто:

    при переопределении метода не разрешается запись предусловия и постусловия в базисной форме — require и ensure ;

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

    для ослабления предусловия следует использовать предложение в форме require else new_pred. В этом случае семантика такова: переопределяемый метод имеет предусловие old_pred or else new_pred, где old_pred наследуемое предусловие;

    для усиления постусловия следует использовать предложение в форме ensure then new_post. В этом случае семантика такова: переопределяемый метод имеет предусловие old_post and then new_post, где old_post — наследуемое постусловие.

    Это решение удовлетворяет правилу, поскольку по правилам логики: "a implies a or b" и "а and b implies a".

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

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

    Предусловие take в классе DISPATCH_TAXI, отражающее наше обсуждение, имеет вид:

    require else
    in_zone: customer.is_in_zone (Current)
    

    Много других примеров можно найти при анализе текстов библиотеки EiffelBase.

    Контракты в отложенных классах

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

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

    Пример для forth является типичным:

    forth
        — Передвинуть курсор к следующей позиции
      require
        in_range: not after
      deferred
      ensure
        increased: index = old index + 1
      end
    

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

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

    Точно так же search позволяет подключать различные версии forth и других программ, определенных потомками класса LINEAR, но при условии, что они удовлетворяют контрактам, заданным в классе LINEAR для этих методов.

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

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

    Как для отложенных, так и для эффективных классов правила адаптации контрактов служат основой корректного использования наследования. Полиморфизм и динамическое связывание являются мощным механизмом, который по этой причине одновременно и опасен. Так как каждый тип может адаптировать наследуемый компонент, как можно гарантировать, что вызов myvehicle. turn _ left после переопределения не заставит ваше транспортное средство поворачивать направо, или останавливаться, или ездить по кругу? Гибкость, которую привносит в программирование комбинация переопределения, полиморфизма и динамического связывания, может зайти слишком далеко. Как проектировщик метода turn _ left, требующего левого поворота, вы хотите позволить потомку дать собственную реализацию, но при условии сохранения исходной семантики, гарантирующей левый поворот.

    Правило переопределения контракта и связанный с ним механизм языка ( require else... ) обеспечивают нужную степень контроля. Можно указать границы, в рамках которых допустима свобода реализации. Наследование и связанная с ним техника — не просто мощная форма повторного использования, но и техника субподрядов. Классы используют переопределение как субподряд на выполнение некоторых операций потомками. Из-за полиморфизма и динамического связывания клиент не знает, какой субподрядчик будет работать в текущем вызове. Ситуация аналогична покупке iPhone: вы не знаете, где сделана та или иная часть аппарата — в Шанхае, Тайване, Бангалоре или Будапеште. Правило переопределения контракта с успехом может быть названо правилом сохранения честного субподряда.

    Общая структура наследования

    Наследование позволяет нам обеспечить общий каркас, где каждый элемент ПО имеет свое четкое место. Большинство ОО-языков (значимым исключением является С++) определяют специальный класс — общего предка всех классов, иногда называемого Object (Smalltalk, Java, C#). В Eiffel такой класс называется ANY. Он входит в библиотеку "Kernel", которая содержит также некоторые фундаментальные классы, тесно связанные с определением языка (ARRAY, STRING, базисные типы данных — BOOLEAN, INTEGER, REAL, CHARACTER ).

    Показанные на следующем рисунке классы А, В могут быть классами, написанными вами или мной. Правило, определяющее роль ANY, просто: любой класс, в котором даже не задано предложение наследования inherit, записанный в виде

    class A feature... end
    

    понимается, как если бы он был явно записан в форме

    class A inherit ANY feature... end
    

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

    (рис 3.2) Общая структура наследования

    Имеет место следующее свойство:

    Теорема об универсальном наследовании и согласовании (Eiffel)

    Каждый класс является наследником ANY. Каждый тип согласован с ANY.

    Класс ANY включает общецелевые компоненты, полезные для всех классов: is_equal и другие полезные методы. Мы уже встречались с использованием метода print этого класса, который позволяет печатать представление, заданное по умолчанию для любого объекта. Тип ANY является наиболее общим типом, с ним согласован любой другой тип. Переменная, объявленная как v: ANY, является полиморфной и может обозначать объекты любого типа.

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

  • Как тип, он позволяет задавать тип Void — предопределенное значение, представляющее ссылку void (значение, не связанное ни с каким объектом).
  • Как класс, он поддерживает скрытие информации. Как отмечалось ранее, мы объявляем скрытые компоненты класса, используя предложение в форме feature { NONE }. Формально это означает, что компонент экспортируется только классу NONE — фактически, никакому классу.
  • Данный синтаксис задает специальную форму выборочного экспорта. Предложение feature можно задавать в форме feature {C, D,...}, означающей, что компоненты, объявленные в разделе feature, экспортируются классам, указанным в скобках, и их потомкам.

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

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

    Почувствуй методологию

    Развенчание легенды о сложности множественного наследования

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

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

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

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

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

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

    class TROLLEY inherit
      TRAM
        redefine add_station, remove_station end
      BUS
    feature
    …
    end
    

    Простой пример может быть найден в базисных библиотеках. Класс NUMERIC задает объекты, поставляемые со стандартными математическими операциями:

    deferred class NUMERIC feature
      plus alias "+" (other: NUMERIC): NUMERIC deferred end
      minus alias "-" (other: NUMERIC): NUMERIC deferred end
      times alias "_" (other: NUMERIC): NUMERIC deferred end
      divided alias "/" (other: NUMERIC): NUMERIC deferred end
    end
    

    (Это только набросок. Полный текст класса можно увидеть в EiffelStudio)

    Еще один библиотечный класс, COMPARABLE, задает объекты, которые поставляются с операциями отношениями, задающими полный порядок:

    deferred class COMPARABLE feature
      lesser alias "<" (other: NUMERIC): BOOLEAN deferred end
      lesser_or_equal alias "<=" (other: NUMERIC): BOOLEAN do...end
      greater alias ">" (other: NUMERIC): BOOLEAN do...end
      greater_or_equal alias ">=" (other: NUMERIC): BOOLEAN do...end
    end
    

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

    (рис 3.3) Множественное наследование

    Такие языки, как Java и C#, допускают множественное наследование, но не классов, а интерфейсов, подобных, как отмечалось ранее, полностью отложенным классам. Следующий пример иллюстрирует разницу. Класс COMPARABLE нуждается только в одном отложенном свойстве, например lesser (alias <). Все остальные могут быть определены как эффективные компоненты, например:

    lesser_or_equal alias "<=" (other: NUMERIC): BOOLEAN
        — Является ли текущий объект меньше или равным other?
      do
        Result := (Current < other) or (Current ~ other)
      ensure
        definition: Result = ((Current < other) or (Current ~ other))
      end
    

    Аналогично greater(other) определяется как other.lesser(Current). Нетрудно определить и отношение "больше или равно". Класс не просто задает начальную реализацию, но и устанавливает отношения, существующие между операциями, что отражается в постусловиях.

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

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

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

    Множественное наследование приводит к проблемам перегрузки, если класс С наследует от двух классов с идентично названными именами компонентов:

    (рис 3.4) Конфликт имен

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

    class C inherit
      A
      B
    end
    

    Здесь А и В имеют компоненты, названные f. Компиляция класса C в этом случае не пройдет. Необходимо переименование :

    class C inherit
    A rename f as first_f end
    B
    end
    

    Предложение rename (которое может быть скомбинировано с переопределением : rename f as first_f redifine first_f end ) просто указывает, что компонент, известный в А как f, будет известен в классе С под именем first_f. Конечно, мы могли бы переименовать компонент и в классе В или в обоих классах.

    Переименованный компонент все же остается прежним компонентом — компонентом, ранее известным как f в С. Так что для a1:A; c1:C следующие вызовы оба являются правильными:

    a1.f
    c1.first_ f
    

    Конечно же, возникнет ошибка при вызове al.first_f, поскольку понятно, что класс А не знает имя first_f. Вызов cl.f синтаксически правилен, но ссылается на компонент из класса В. Если бы и этот компонент был переименован, то и этот вызов был бы ошибочным. В полиморфной ситуации после присваивания a1:=c1 два приведенных выше вызова имели бы одинаковый эффект, так как al и cl обозначали бы один и тот же объект, а f и first_f обозначали бы один и тот же компонент в соответствующих классах.

    Плоский и контрактный облик класса отражают эффект как переименования, так и переопределения.

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

    Переименование в сравнении с переобъявлением

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

    Когда требуется изменить и сам компонент, и его имя, допускается комбинация переименования и переобъявления.

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

    От множественного наследования к повторному наследованию

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

    к предку:

    (рис 3.5) Повторное наследование

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

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

    Для случая, изображенного на рисунке, первый вопрос, который следует задать относительно f — компонента из класса А : в классе D ему должны соответствовать два компонента или один? Ответ прост и соответствует духу предыдущего обсуждения.

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

    При репликации может возникнуть неоднозначность, когда встречается переопределение. Такая ситуация может встретиться, например, при повторном наследовании от ANY, если один из классов вдоль пути наследования вводит свое собственное понятие копирования и эквивалентности, переопределив copy и is_equal из класса ANY :

    (рис 1.6) Необходимость "select"

    Методы copy и is_equal должны всегда переопределяться совместно, поскольку постусловие copy(other) устанавливает is_equal(other). Любая операция копирования должна убеждаться, что результат эквивалентен цели копирования в соответствии с локальным определением эквивалентности.

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

    Теперь D наследует эти методы от LIST, а также от класса C, который сохранил версии по умолчанию, наследованные от ANY. Все это работает в соответствии с предыдущими правилами. Класс D должен переименовать компоненты во избежание конфликта имен, чтобы они дублировались.

    Проблема возникает в связи с полиморфизмом. Рассмотрим a типа ANY и dl типа D. Выполним присваивание, а потом вызов:

    a : = dl
    a.copy (...)
    

    В этом случае при динамическом связывании возникает неоднозначность: для родительского метода copy у потомка имеются две реализации. Возникает вопрос: следует ли для связывания использовать версию LIST (известную как copy в классе) или С-версию ( Ccopy )? Ситуация возникает всякий раз, когда одновременно встречается репликация и переопределение. Для устранения неоднозначности требуется ввести предложение select. Вот как следует записать класс D :

    class D inherit
      LIST [T] select copy, is_equal end
      C rename copy as C_copy, is_equal as C_is_equal end
    feature
       ... Остаток текста класса...
    end
    

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

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

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