Основы объектно-ориентированного программирования - Бертран Мейер
Шрифт:
Интервал:
Закладка:
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 иллюстрирует идеи построения подобных фильтров.Инварианты класса
Предусловия и постусловия описывают свойства отдельных программ. Но экземпляры класса обладают также глобальными свойствами. Их принято называть инвариантами класса (class invariants), и они определяют более глубокие семантические свойства и ограничения целостности, характеризующие класс.
Определение и пример
Рассмотрим снова реализацию стека классом STACK2:
class STACK2 [G] creation
make
feature
? make, empty, full, item, put, remove?
capacity: INTEGER
count: INTEGER
feature {NONE} -- Implementation
representation: ARRAY [G]
end
Атрибуты класса - массив representation и целые capacity, count - задают представление стека. Хотя предусловия и постусловия программ отражают семантику стека, их недостаточно для выражения важных свойств, связывающих атрибуты. Например, count всегда должно удовлетворять условию:
0 <= count; count <= capacity
из которого следует, что capacity >=0 и что capacity задает размер массива:
capacity = representation.capacity
Инвариант класса это утверждение, выражающее общие согласованные ограничения, применимые к каждому экземпляру класса как целому. Этим они отличаются от предусловий и постусловий, характеризующих отдельные программы.
Выше приведенные примеры инвариантов включали только атрибуты. Инварианты могут выражать отношения между функциями и между функциями и атрибутами. Например, инвариант STACK2 может включать следующее свойство, описывающее связь между функцией empty и count:
empty = (count = 0)
Этот пример не показателен, он повторяет утверждение, заданное постусловием empty. Более полезные утверждения те, которые включают только атрибуты или более чем одну функцию.
Вот еще один типичный пример. Предположим, что мы имеем дело с банковскими счетами, и есть класс Bank_Account с компонентами: deposits_list, withdrawals_list и balance. Тогда инвариантом такого класса может быть утверждение в форме:
consistent_balance: deposits_list.total - withdrawals_list.total = balance
где функция total дает суммарное значение списка всех операций (приходных или расходных). Инвариант определяет основное условие согласования всех банковских операций над счетом, связывая баланс, приходные и расходные операции.
Форма и свойства инвариантов класса
Синтаксически инвариант класса является утверждением, появляющимся в предложении invariant, стоящим после всех предложений feature, и перед предложением end. Вот пример: