Опубликован: 17.10.2005 | Доступ: свободный | Студентов: 8825 / 590 | Оценка: 4.38 / 4.10 | Длительность: 41:16:00
ISBN: 978-5-7502-0255-3
Специальности: Программист
Лекция 16:

Техника наследования

Абстрактные предусловия

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

Типичным примером этого является порождение BOUNDED_STACK от универсального класса стека ( STACK ). Процедура занесения в стек элемента ( put ) в порожденном классе имеет предусловие count <= capacity, где count - текущее число элементов в стеке, capacity - физическая емкость накопителя.

В общем понятии стека нет понятия емкости. Поэтому создается впечатление, будто при переходе к BOUNDED_STACK предусловие приходится усилить (от бесконечной емкости перейти к конечной). Как выстроить структуру наследования, не нарушая правило Утверждения Переобъявления?

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

put (x: G) is
                  -- Поместить x на вершину.
         require
                  not full
         deferred
         ensure
                  ...
         end

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

full: BOOLEAN is
                  -- Заполнено ли представление стека?
                  -- (По умолчанию, нет)
         do Result := False end

Тогда в BOUNDED_STACK достаточно переопределить full:

full: BOOLEAN is
                  -- Заполнено ли представление стека?
                  -- (Да, если число элементов равно емкости стека)
         do Result := (count = capacity) end

Предусловие, такое как not full, включающее свойство, которое переопределяется потомками, называется абстрактным (abstract) предусловием.

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

if not s.full then s.put (a) end

будет корректен независимо от вида STACK, присоединенного к s.

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

  • ограниченный стек является стеком;
  • в стек всегда можно добавить еще один элемент.

Если предпочесть первое свойство и допускать порождение BOUNDED_STACK от STACK, мы должны согласиться с тем, что общее понятие стека включает предположение о невозможности в ряде случаев выполнить операцию put, абстрактно выраженное запросом full.

Было бы ошибкой включить в виде постусловия подпрограммы full в классе STACK выражение Result = False или (придерживаясь рекомендуемого стиля, эквивалентный ему) инвариант not full. Это - случай излишней спецификации, ограничивающей свободу реализации компонентов потомками класса.

Правило языка

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

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

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

  • \alpha влечет \alpha or \gamma независимо от значения \gamma ;
  • \beta and \delta влечет \beta независимо от значения \delta.

Итак, гарантируется, что новое предусловие слабее исходного \alpha либо равно ему, если оно имеет вид \alpha or \gamma . Гарантируется, что новое постусловие сильнее исходного \beta либо равно ему, если оно имеет вид \beta and \delta . Отсюда следует искомое языковое правило:

Правило (2) Утверждения Переобъявления

При повторном объявлении подпрограммы нельзя использовать предложения require или ensure. Вместо них следует использовать предложение, начинающееся с:

  • require else, объединенное с исходным предусловием логической связкой or
  • ensure then, объединенное с исходным постусловием логической связкой and.

При отсутствии таких предложений действуют исходные утверждения.

Заметим, что используются нестрогие булевы операторы and then и or else, а не обычные and и or, хотя чаще всего это различие несущественно.

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

invert (epsilon: REAL) is
                  -- Обращение текущей матрицы с точностью epsilon
         require
                  epsilon >= 10 ^ (-6)
         ...
         ensure
                  ((Current * inverse) |-| One) <= epsilon
мы не вправе в повторном объявлении использовать require и ensure, поэтому результат
примет вид
...
         require else
                  epsilon >= 10 ^ (-20)
         ...
         ensure then
                  ((Current * inverse) |-| One) <= (epsilon / 2)

а стало быть, предусловие формально станет таким: (epsilon >= 10 ^ (-20)) or else (epsilon >= 10 ^ (-6)).

Ситуация с постусловием аналогична. Такое расширение не имеет особого значения, поскольку преобладает более слабое предусловие или более сильное постусловие. Если \gamma влечет \alpha, то \alpha or else \gamma имеет то же значение, что и \alpha. Если \beta влечет \delta, то \beta and then \delta имеет то же значение, что и \beta. Поэтому математически предусловие повторного объявления есть: epsilon >= 10 ^ (-20), а его постусловие есть: ((Current * inverse) |-| One) <= (epsilon / 2), хотя запись утверждений в программе (а также, вероятно, их расчет во время выполнения при отсутствии средств символьных преобразований) является более сложной.

Повторное объявление функции как атрибута

Правило Утверждения Переобъявления нуждается в небольшом дополнении ввиду возможности при повторном объявлении задать функцию как атрибут. Что произойдет с предусловием функции и ее постусловием, если таковые имелись?

Атрибут доступен всегда, а потому мы вправе считать, что его предусловие равно True. В итоге можно полагать, что предусловие атрибута, согласно правилу Утверждения Переобъявления, было ослаблено.

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

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

Замечание математического характера

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

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

  • Предусловие задает область определения DOM функции r (подмножество I, на котором r гарантированно вырабатывает результат).
  • Постусловие задает для каждого x из DOM подмножество RESULTS(x) множества O, такое, что r (x) \in  RESULTS (x). Так как постусловие не всегда однозначно описывает результат, это подмножество может иметь больше одного элемента.

Правило Утверждения Переобъявления означает, что повторное объявление может расширять область определения и сужать множество результатов. Пометив новые множества знаком ', запишем требования, закрепленные этим правилом:

DOM' \supseteq  DOM
\\
RESULTS' (x) \subseteq  RESULTS (x)\ для\ всех\ x\ из\ DOM

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

В этом описании состояние системы в период выполнения определяется состоянием (значениями) всех достижимых объектов. Кроме того, входные состояния (элементы I ) также включают в себя значения аргументов. Более подробное введение в математическое описание программ и языков программирования см. в [M 1990].

Александр Шалухо
Александр Шалухо
Анатолий Садков
Анатолий Садков

При заказе pdf документа с сертификатом будет отправлен только сертификат или что-то ещё?