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

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

Предусловия и статус экспорта

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

Рассмотрим следующую ситуацию:

-- Предупреждение: это неправильный класс, только в целях иллюстрации.
class SNEAKY feature
   tricky is
         require
            accredited
         do
           ...
         end
feature {NONE}
   accredited: BOOLEAN is do ... end
end

Спецификация для tricky устанавливает, что любой вызов этой процедуры должен удовлетворять условию, выраженному булевой функцией accredited. Но при экспорте класса эта функция для клиентов является закрытой, поэтому у них нет способа проверить выполнимость условия перед вызовом tricky. Очевидно, подобная ситуация неприемлема.

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

Это правило учитывает все возможные ситуации экспорта, а не только случаи доступности всем клиентам ( tricky ) или полной недоступности ( accredited ). Как отмечалось, при обсуждении проблемы скрытия информации, компонент класса можно сделать доступным для некоторых клиентов, явно перечислив их в feature предложении, например feature {A, B, ... }, определяющего доступность только для классов A, B, ... и их потомков. Сформулируем правило языка:

Правило Доступности предусловия

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

В соответствии с этим правилом каждый клиент, способный вызвать программу, способен проверить ее предусловие. По этому правилу класс SNEAKY является коварным, некорректно построенным, поскольку экспортирует tricky с недоступным предусловием. Нетрудно превратить этот класс в правильно построенный, изменив статус экспорта у accredited. Если tricky появится с предложением feature в форме feature {A, B, C}, то accredited должна экспортироваться, по меньшей мере, клиентам A, B, C, появляясь в той же группе feature, что и tricky. Можно задать для accredited собственное feature -предложение в одной из форм: feature {A, B, C}, feature {A, B, C, D, ...} или просто feature. Любое нарушение этого правила приведет к ошибке в период компиляции. Класс SNEAKY, например, будет забракован компилятором.

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

put (x: G) is
            -- Добавить элемент x на вершину
   require
            not full
   do
            ...
   ensure
            ... Другие предложения...
            in_top_array_entry: representation @ count = x
   end

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

Толерантные модули

(При первом чтении этот раздел можно опустить или ограничиться его беглым просмотром.)

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

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

Поскольку классу понадобятся целочисленные коды ошибок, удобно для этой цели использовать ранее не введенную нотацию " unique " для целочисленных констант. Если объявить множество атрибутов следующим образом:

a, b, c, ...: INTEGER is unique

то в результате этого объявления a, b, c получат последовательно идущие целочисленные значения. Эти значения будут даваться компилятором с гарантией того, что все объявленные таким образом константы получат различные значения (будут уникальными). По принятому соглашению, всем объявляемым таким образом константам даются имена, начинающиеся с буквы в верхнем регистре и с остальными символами в нижнем регистре, например Underflow.

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

indexing
   description: "Стеки: Структуры с политикой доступа Last-In, First-Out %
         %Первый пришел - Последний ушел, с фиксированной емкостью; %
         %толерантная версия, устанавливающая код ошибки в случае %
         %недопустимых операций."
class STACK3 [G] creation
   make
feature - Initialization (Инициализация)
   make (n: INTEGER) is
            -- Создать стек, содержащий максимум n элементов, если n > 0;
            -- в противном случае установить код ошибки равным Negative_size.
            -- Без всяких предусловий!
      do
            if capacity >= 0 then
               capacity := n
               create representation.make (capacity)
      else
               error := Negative_size
            end
      ensure
           error_code_if_impossible: (n < 0) = (error = Negative_size)
           no_error_if_possible: (n >= 0) = (error = 0)
           capacity_set_if_no_error: (error = 0) implies (capacity = n)
           allocated_if_no_error: (error = 0) implies (representation /= Void)
      end
feature - Access (Доступ)
   item: G is
           -- Элемент вершины, если существует; в противном случае
           -- значение типа по умолчанию.
           -- с ошибкой категории Underflow.
           -- Без всяких предусловий!
      do
           if not empty then
                       check representation /= Void end
                 Result := representation.item
                 error := 0
           else
              error := Underflow
                    -- В этом случае результатом является значение по умолчанию
           end
      ensure
           error_code_if_impossible: (old empty) = (error = Underflow)
           no_error_if_possible: (not (old empty)) = (error = 0)
   end
feature -- Status report (Отчет о статусе)
   empty: BOOLEAN is
           -- Пуст ли стек?
      do
           Result := (capacity = 0) or else representation.empty
      end
   error: INTEGER
         -- Индикатор ошибки, устанавливаемый различными компонентами
         -- в ненулевое значение, если они не могут выполнить свою работу
   full: BOOLEAN is
         -- Заполнен ли стек?
      do
            Result := (capacity = 0) or else representation.full
      end
   Overflow, Underflow, Negative_size: INTEGER is unique
         -- Возможные коды ошибок
feature -- Element change (Изменение элементов)
   put (x: G) is
             -- Добавить x на вершину, если возможно; иначе задать код ошибки.
             -- Без всяких предусловий!
      do
         if full then
         error := Overflow
         else
                     check representation /= Void end
            representation.put (x); error := 0
      end
      ensure
         error_code_if_impossible: (old full) = (error = Overflow)
         no_error_if_possible: (not old full) = (error = 0)
         not_empty_if_no_error: (error = 0) implies not empty
         added_to_top_if_no_error: (error = 0) implies item = x
         one_more_item_if_no_error: (error = 0) implies count = old count + 1
      end
   remove is
            -- Удалить вершину, если возможно; иначе задать код ошибки.
            -- Без всяких предусловий!
      do
         if empty then
            error := Underflow
         else
                  check representation /= Void end
            representation.remove
            error := 0
         end
      ensure
         error_code_if_impossible: (old empty) = (error = Underflow)
         no_error_if_possible: (not old empty) = (error = 0)
         not_full_if_no_error: (error = 0) implies not full
         one_fewer_item_if_no_error: (error = 0) implies count = old count - 1
      end
feature {NONE} - Implementation (Реализация)
   representation: STACK2 [G]
            -- Незащищенный стек используется для реализации
   capacity: INTEGER
            -- Максимальное число элементов стека
end - class STACK3

Операции этого класса не имеют предусловий (более точно, имеют True в качестве предусловия). Результат выполнения может характеризовать ненормальную ситуацию, постусловие переопределено так, чтобы позволить отличать корректную и ошибочную обработку. Например, при вызове s.remove, где s это экземпляр класса STACK3, в корректной ситуации значение s.error будет равно 0 ; в ошибочной - Underflow. В последнем случае никакая другая работа выполняться не будет. Клиент несет ответственность за проверку s.error после вызова. Как уже отмечалось, у общецелевого модуля, такого как STACK3 нет способа решить, что делать в ошибочной ситуации: выдать сообщение об ошибке, произвести корректировку ситуации...

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

Несколько технических замечаний к приведенному примеру класса.

  • Экземпляр STACK3 - содержит атрибут representation, представляющий ссылку на экземпляр STACK2, содержащий, в свою очередь, ссылку на массив. Эти обходные пути пагубно отражаются на эффективности, избежать этого можно введением наследования, изучаемого в последующих лекциях.
  • Булева операция or else подобна or, но если первый операнд равен True, игнорирует второй операнд, возможно неопределенный в такой ситуации.
  • Инструкция check, используемая в put и remove, служит для проверки выполнения некоторых утверждений. Она будет изучаться позднее в этой лекции.

В заключение: вы, наверное, отметили тяжеловесность STACK3 в сравнении с простотой STACK2, достигнутой благодаря предусловиям. Это хороший пример, показывающий, что толерантный стиль может приводить к бесполезно усложненному ПО. Требовательный стиль, по контрасту, вытекает из общего духа Проектирования по контракту. Попытка управлять всем, - и возможными и невозможными случаями - совсем не лучший способ помочь вашим клиентам. Если вместо этого вы построите классы, влекущие возможно более строгие условия на их использование, точно опишите эти условия, включив их в документацию класса, вы реально облегчите жизнь вашим клиентам. Требовательная любовь (tough love) может быть лучше всепрощающей; лучше эффективная поддержка функциональности с проверяемыми ограничениями, чем страстная попытка предугадать желания клиентов, принятие возможно неадекватных решений, жертвой чего становятся простота и эффективность.

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

Толерантный подход остается полезным для модулей, принимающих данные от внешнего мира. Как отмечалось, в этом случае строятся фильтры, отделяющие внешний мир от обрабатывающих модулей. Класс STACK3 иллюстрирует идеи построения подобных фильтров.
Александр Шалухо
Александр Шалухо
Анатолий Садков
Анатолий Садков

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