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

 

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

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

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

Рис. 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' , инструкция вызовет подпрограмму, ожидающую выполнения γ и гарантирующую выполнение , в то время как клиент получил указание соблюдать и ожидать выполнения β . Налицо возможное расхождение во взглядах клиента и поставщика на контракт.