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

Инварианты

С правилом об инвариантах класса мы встречались и прежде:

Правило родительских инвариантов

Инварианты всех родителей применимы и к самому классу.

Инварианты родителей добавляются к классу. Инварианты соединяются логической операцией and then. (Если у класса нет явного инварианта, то инвариант True играет эту роль.) По индукции в классе действуют инварианты всех его предков, как прямых, так и косвенных.

Как следствие, выписывать инварианты родителей в инварианте потомка еще раз не нужно (хотя семантически такая избыточность не вредит: a and then a есть то же самое, что a).

Полностью восстановленный инвариант класса можно найти в плоской и краткой плоской форме последнего (см. лекцию 15).

Предусловия и постусловия при наличии динамического связывания

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

Анализируя механизмы повторного объявления, полиморфизма и динамического связывания, можно дать точную формулировку искомого правила. Но для начала представим типичный случай.

Рассмотрим класс и его подпрограммы, имеющие как предусловие, так и постусловие:

Рис. 16.1.  Подпрограмма, клиент и контракт

На рис. 16.1 показан клиент C класса A. Чтобы быть клиентом, класс C, как правило, включает в одну из своих подпрограмм объявление и вызов вида:

a1: A

...

a1.r

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

Вызов будет корректен лишь тогда, когда он удовлетворяет предусловию. Гарантировать, что C соблюдает свою часть контракта, можно, к примеру, предварив вызов проверкой предусловия, написав вместо a1.r конструкцию:

if a1. then

a1.r

check a1.β end -- постусловие должно выполняться

... Инструкции, которые могут предполагать истинность a1.. ...

end

(Как отмечалось при обсуждении утверждений, не всегда требуется проверка: достаточно, с помощью if или без него, гарантировать выполнение условия a перед вызовом r. Для простоты будем использовать if-форму, игнорируя предложение else.)

Обеспечив соблюдение предусловия, клиент C рассчитывает на выполнение постусловия a1.β при возврате из r.

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

Что происходит, когда вводится наследование?

Рис. 16.2.  Подпрограмма, клиент, контракт и потомок

Пусть новый класс A' порожден от A и содержит повторное объявление r. Как он может, если вообще может, заменить прежнее предусловие новым γ, а прежнее постусловие β - новым ?

Чтобы найти ответ, рассмотрим обязательства клиента. В вызове a1.r цель a1 может - в силу полиморфизма - иметь тип A'. Однако C об этом не знает! Единственным объявлением a1 остается исходная строка

a1: A

где упоминается A, но не A'. На деле C может использовать A', даже если его автор не знает о наличии такого класса. Вызов подпрограммы r может произойти, например, в процедуре C вида:

some_routine_of_C (a1: A) is

do

...; a1.r;...

end

Тогда при вызове some_routine_of_C из другого класса в нем может использоваться фактический параметр типа A', даже если в тексте клиента C класс A' нигде не упоминается. Динамическое связывание как раз и означает тот факт, что обращение к r приведет в этом случае к использованию переопределенной версии A'.

Итак, может сложиться ситуация, в которой C, являясь только клиентом A, фактически во время выполнения использует версии компонентов класса A'. (Можно сказать, что C - "динамический клиент" A', хотя в тексте C об этом и не говорится.)

Что это значит для C? Только одно - проблемы, которые возникнут, если не предпринять никаких действий. Клиент C может добросовестно выполнять свою часть контракта, и все же в результате он будет обманут. Например,

if a1. then a1.r end

если a1 полиморфно присоединена к объекту типа A', инструкция вызовет подпрограмму, ожидающую выполнения γ и гарантирующую выполнение , в то время как клиент получил указание соблюдать и ожидать выполнения β. Налицо возможное расхождение во взглядах клиента и поставщика на контракт.

Как обмануть клиентов

Чтобы понять, как удовлетворить клиентов, мы должны сыграть роль адвокатов дьявола и на секунду представить себе, как их обмануть. Так поступает опытный криминалист, разгадывая преступление. Как мог бы поступить поставщик, желающий ввести в заблуждение своего честного клиента C, гарантирующего при вызове и ожидающего выполнения β? Есть два пути:

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

[x]. Гарантировать меньше, чем это следует из начального постусловия β. Более слабое постусловие позволяет нам дать в результате меньше, чем было обещано исходной спецификацией.

Вспомните, что мы неоднократно говорили при обсуждении Проектирования по Контракту: усиление предусловия облегчает задачу поставщика ("клиент чаще не прав"), иллюстрацией чего служит крайний случай - предусловие false (когда "клиент всегда не прав").