Выбрать главу

Определение и пример

Рассмотрим снова реализацию стека классом 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. Вот пример:

class STACK4 [G] creation

...Как в STACK2...

feature

...Как в STACK2...

invariant

count_non_negative: 0 <= count

count_bounded: count <= capacity

consistent_with_array_size: capacity = representation.capacity

empty_if_no_elements: empty = (count = 0)

item_at_top: (count > 0) implies (representation.item (count) = item)

end

Инвариант класса C это множество утверждений, которым удовлетворяет каждый экземпляр класса во все "стабильные" времена. В эти времена экземпляр класса находится в наблюдаемом состоянии:

[x]. на момент создания экземпляра, сразу после выполнения create a или create a.make(...), где a класса C;

[x]. перед и после каждого удаленного вызова a.r(...) программы r класса С.

Следующий рисунок, показывающий жизнь объектов, поможет разобраться в инвариантах и стабильных временах:

Рис. 11.4.  Жизнь объектов

Жизнь объектов не столь уж захватывающая. Вначале - слева на рисунке - он просто не существует. При выполнении инструкции create a или create a.make(...) или clone объект создается и достигает первой станции S1 в своей жизни. Затем идет череда довольно скучных событий: клиенты, для которых доступен объект, один за другим вызывают его компоненты в форме a.f(..). Так все продолжается, пока не завершится вычисление.

Инвариант является характеристическим свойством состояний, представленных большими квадратиками на рисунке: S1, S2, S3 и т.д. Эти состояния соответствуют стабильным временам, упомянутым выше.

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

Инвариант в момент изменения

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

Кто должен обеспечить сохранность инвариантов

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

Закончим обсуждение правилом, точно определяющим, когда утверждение является корректным инвариантом класса:

Правило инварианта

Утверждение Inv является корректным инвариантом класса, если и только если оно удовлетворяет следующим двум условиям:

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

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

Заметьте, в этом правиле:

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

[x]. Состояние объекта определяется значениями всех его полей (значениями атрибутов класса для этого конкретного экземпляра).