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

Проектирование по контракту: построение надежного ПО

Работа с утверждениями

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

Класс стек

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

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

Реализация стека на базе массива

Рис. 11.2. Реализация стека на базе массива

Массив, названный representation, имеет границы 1 и capacity: реализация использует также целочисленный атрибут count, отмечающий вершину стека. Заметьте, после того, как мы откроем для себя наследование, появится возможность писать классы с отложенной реализацией, что позволит покрывать несколько возможных реализаций, а не одну конкретную. Даже для класса c фиксированной реализацией, например, как здесь на базе массива, мы будем иметь возможность строить его как потомка родительского класса Array. В данный момент никакие методы наследования применяться не будут.

Вот он класс. Остается напомнить, что для массива a операция, присваивающая значение x его i -му элементу, записывается так: a.put(x,i). Получить i -й элемент можно так: a.item(i) или a @ i. Если, как здесь, границы заданы, то i во всех случаях лежит между этими границами: 1<= i <= capacity.

indexing
   description: "Стеки: Структуры с политикой доступа Last-In, First-Out %
      % Последний пришел - Первый ушел, и с фиксированной емкостью"
class STACK2 [G] creation
   make
feature - Initialization (Инициализация)
   make (n: INTEGER) is
            -- Создать стек, содержащий максимум n элементов
         require
            positive_capacity: n >= 0
         do
            capacity := n
            create representationlmake (1, capacity)
         ensure
            capacity_set: capacity = n
            array_allocated: representation /= Void
            stack_empty: empty
         end
feature - Access (Доступ)
   capacity: INTEGER
            -- Максимальное число элементов стека
   count: INTEGER
            -- Число элементов стека
   item: G is
            -- Элемент на вершине стека
      require
            not_empty: not empty -- т.е. count > 0
      do
            Result := representation @ count
      end
feature -- Status report (Отчет о статусе)
   empty: BOOLEAN is
            -- Пуст ли стек?
      do
            Result := (count = 0)
      ensure
            empty_definition: Result = (count = 0)
      end
   full: BOOLEAN is
            -- Заполнен ли стек?
      do
            Result := (count = capacity)
      ensure
            full_definition: Result = (count = capacity)
      end
feature -- Element change (Изменение элементов)
   put (x: G) is
            -- Добавить элемент x на вершину
      require
            not_full: not full -- т.е. count < capacity в этом представлении
      do
            count := count + 1
            representation.put (count, x)
      ensure
            not_empty: not empty
            added_to_top: item = x
            one_more_item: count = old count + 1
            in_top_array_entry: representation @ count = x
      end
      remove is
            -- удалить элемент вершины стека
      require
            not_empty: not empty -- i.e. count > 0
      do
            count := count - 1
      ensure
            not_full: not full
            one_fewer: count = old count - 1
      end
feature {NONE} -- Implementation (Реализация)
   representation: ARRAY [G]
            -- Массив, используемый для хранения элементов стека
invariant
            ... Будет добавлен позднее ...
      end

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

Это первый законченный класс этой лекции, не слишком далеко отличающийся от того, что можно найти в профессиональных библиотеках повторно используемых ОО-компонентов, таких как Базовые библиотеки. Одно замечание о структуре класса. Поскольку класс имеет более двух-трех компонентов, возникает необходимость сгруппировать его компоненты подходящим образом. Нотация позволяет реализовать такую возможность введением множества предложений feature. Это свойство группировки компонентов, введенное в предыдущей лекции, использовалось там, как способ задания различного статуса экспорта компонентов. И здесь в последней части класса, помеченной Implementation, это свойство используется для указания защищенности компонента representation. Но преимущества группирования можно использовать и при неизменном статусе экспорта. Его цель - сделать класс простым при чтении и легче управляемым, группируя компоненты по категориям. После каждого ключевого слова feature появляется комментарий, называемый комментарий к предложению Feature (Feature Clause Comment). Он позволяет дать содержательное описание данной категории - роль компонентов, включенных в этот раздел. Категории, используемые в примере, те же, что и при описании класса STACK1 с добавлением раздела Initialization с процедурой создания (конструктором).

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

Императив и аппликатив (применимость)

Утверждения из STACK2 иллюстрируют фундаментальную концепцию - разницу между императивным и аппликативным видением.

Утверждения empty и full могут вызвать удивление. Приведу еще раз текст full:

full: BOOLEAN is
            -- Заполнен ли стек?
      do
            Result := (count = capacity)
      ensure
            full_definition: Result = (count = capacity)
      end

Постусловие говорит, что Result имеет значение выражения ( count = capacity ). Но оператор присваивания именно это значение присваивает переменой Result. В чем же смысл написания постусловия? Не является ли оно избыточным?

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

Инструкция предписывает ( prescriptive ), утверждение описывает ( descriptive ). Инструкция описывает "как", утверждение описывает "что". Инструкция является частью реализации, утверждение - элементом спецификации.

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

  • Компьютерные операции могут изменять состояние аппаратно-программной машины. Инструкции в языках программирования являются командами (императивные конструкции), заставляющие машину выполнять такие операции.
  • Математические вычисления никогда ничего не меняют. Как отмечалось при рассмотрении АТД, взятие квадратного корня от числа 2 не меняет это число. Вместо этого математики описывают как, используя свойства одних объектов, вывести свойства других, таких как v2, полученных применением ( applying - отсюда и термин "аппликативный") математических трансформаций.

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

Причина близости нотаций в том, что присваивание зачастую кратчайший путь достижения эквивалентности. Но при переходе к более сложным примерам концептуальное различие между спецификацией и реализацией будет только возрастать. Даже в простейшем случае вычисления квадратного корня постусловие может быть задано в форме: abs(Result^2 -x) <= tolerance, где abs - обозначает абсолютное значение, а tolerance - допустимое отклонение от точного значения. Инструкции, вычисляющие квадратный корень, могут быть не тривиальными, реализуя определенный алгоритм вычисления квадратного корня.

Даже для put в классе STACK2 одной и той же спецификации могут соответствовать различные алгоритмы, например:

if count = capacity then Result := True else Result := False end

или упрощенный вариант, учитывающий правила инициализации:

if count = capacity then Result := True end

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

Предупреждение: по практическим соображениям допускается включение в утверждение функций - по внешнему виду императивных элементов. Эта проблема исследуется в конце этой лекции.

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

Таблица 11.2. Императивно - аппликативное противопоставление
Реализация Спецификация
Инструкция Выражение
Как Что
Императив Аппликатив
Предписание Описание
Александр Шалухо
Александр Шалухо
Анатолий Садков
Анатолий Садков

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