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

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

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

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

Хотя было показано, что обычно это не лучший подход к проектированию, полезно рассмотреть, как выглядят классы, если использовать толерантный стиль в некоторых особых случаях. Класс 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 нет способа решить, что делать в ошибочной ситуации: выдать сообщение об ошибке, произвести корректировку ситуации...

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

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

[x]. Экземпляр STACK3 - содержит атрибут representation, представляющий ссылку на экземпляр STACK2, содержащий, в свою очередь, ссылку на массив. Эти обходные пути пагубно отражаются на эффективности, избежать этого можно введением наследования, изучаемого в последующих лекциях.

[x]. Булева операция or else подобна or, но если первый операнд равен True, игнорирует второй операнд, возможно неопределенный в такой ситуации.

[x]. Инструкция check, используемая в put и remove, служит для проверки выполнения некоторых утверждений. Она будет изучаться позднее в этой лекции.

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

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

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