Опубликован: 04.04.2012 | Доступ: свободный | Студентов: 1989 / 61 | Оценка: 4.60 / 4.40 | Длительность: 13:49:00
Лекция 4:

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

< Лекция 3 || Лекция 4: 123 || Лекция 5 >

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

Наследование позволяет нам обеспечить общий каркас, где каждый элемент ПО имеет свое четкое место. Большинство ОО-языков (значимым исключением является С++) определяют специальный класс — общего предка всех классов, иногда называемого 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 || Лекция 4: 123 || Лекция 5 >
Надежда Александрова
Надежда Александрова

Уточните пожалуйста, какие документы для этого необходимо предоставить с моей стороны. Курс "Объектно-ориентированное программирование и программная инженения".