Вот другой пример - наш старый друг стек. Нашей библиотеке потребуется общий класс STACK [G], который будет отложенным, так как он должен покрывать всевозможные реализации. Его собственные потомки, такие как FIXED_STACK и LINKED_STACK, будут описывать конкретные реализации. Одной из отложенных процедур класса STACK является put:
put (x: G) is
-- Поместить x на вершину.
require
not full
deferred
ensure
not_empty: not empty
pushed_is_top: item = x
one_more: count = old count + 1
end
Булевские функции empty и full (также отложенные на уровне STACK) выражают свойство стека быть пустым и заполненным.
Только с помощью утверждений отложенные классы достигают своей полной силы. Как уже отмечалось (хотя детали появятся через две лекции), предусловия и постусловия применимы ко всем переопределениям процедуры. Это особенно важно в отложенном случае: в нем такие утверждения будут ограничивать все допустимые реализации. Таким образом, приведенная спецификация ограничивает все варианты put в потомках класса STACK.
Благодаря использованию утверждений, можно сделать отложенные классы достаточно информативными и семантически богатыми, несмотря на отсутствие у них реализаций.
В конце этой лекции мы вновь обратимся к отложенным классам и исследуем глубже их роль в процессе ОО-анализа, проектирования и реализации.
Способы изменения объявлений
Возможность изменить объявление компонента - переопределить или дать его реализацию - обеспечивает гибкость и последовательное проведение разработки. Имеется еще два метода, усиливающих эти качества:
[x]. Возможность изменить объявление функции на атрибут.
[x]. Простой способ сослаться на первоначальную версию в теле нового определения.
Повторное объявление функции как атрибута
Повторные объявления позволяют активно применять один из центральных принципов модульности - принцип Унифицированного Доступа (Uniform Access).
Напомним (см. лекцию 3), что этот принцип утверждает (первоначально в менее технических терминах, но сейчас мы можем позволить себе быть более точными), что с точки зрения клиента не должно быть никакой существенной разницы между атрибутом и функцией без аргументов. В обоих случаях компонент является запросом и все, что их отличает, - это их внутреннее представление.
Первым примером этого был класс, описывающий банковские счета, в котором компонент balance мог быть реализован как функция, которая добавляет вклады и вычитает снимаемые суммы, или как атрибут, изменяемый по мере необходимости так, чтобы отражать текущий баланс. Для клиента это было все равно (за исключением, возможно, эффективности).
С появлением наследования можно пойти дальше и позволить, чтобы в классе наследуемая функция была переопределена как атрибут.
Наш прежний пример хорошо подходит для иллюстрации. Пусть имеется класс ACCOUNT1:
class ACCOUNT1 feature
balance: INTEGER is
-- Текущий баланс
do
Result := list_of_deposits.total - list_of_withdrawals.total
end
...
End
Тогда в потомке может быть выбрана вторая реализация из нашего первоначального примера, переопределяющая balance как атрибут:
class ACCOUNT2 inherit
ACCOUNT1
redefine balance end
feature
balance: INTEGER
-- Текущий баланс
...
end
По-видимому, в классе ACCOUNT2 нужно будет переопределить некоторые процедуры, такие как withdraw и deposit, чтобы, кроме других своих обязанностей они еще модифицировали нужным образом balance, сохраняя в качестве инварианта свойство: balance = list_of_deposits.total - list_of_withdrawals.total.
В этом примере новое объявление является переопределением. Его результатом может также оказаться превращение отложенного компонента в атрибут. Например, пусть в отложенном классе LIST имеется компонент
count: INTEGER is
-- Число вставленных элементов
deferred
end
Тогда в реализации списка этот компонент может быть реализован как атрибут:
count: INTEGER
Если нас попросят применить эту классификацию, чтобы разбить компоненты на атрибуты и подпрограммы, то мы условимся рассматривать отложенный компонент как подпрограмму, несмотря на то, что для отложенного компонента с результатом и без аргументов само понятие отложенности означает, что мы еще не сделали выбор, как его реализовать - функцией или атрибутом. Фраза "отложенный компонент" передает эту неопределенность и предпочтительней фразы "отложенная подпрограмма". |
Переобъявление функции как атрибута, объединенное с полиморфизмом и динамическим связыванием, приводят к полной реализации принципа Унифицированного Доступа. Сейчас можно не только реализовать запрос клиента вида a.service либо через память, либо посредством вычисления, но один и тот же запрос в процессе одного вычисления может в одних случаях запустить доступ к некоторому полю, а в других - вызвать некоторую функцию. Это может, в частности, случиться при выполнении одного и того же вызова a.balance, если по ходу вычисления a будет полиморфно присоединяться к объектам разных классов.
Обратного пути нет
Можно было бы ожидать, что допустимо и обратное переопределение атрибута в функцию без аргументов. Но нет. Присваивание - операция применимая к атрибутам, - становится бессмысленной для функций. Предположим, что a - это атрибут класса C, и некоторая подпрограмма содержит команду
a := some_expression
Если потомок C переопределит a как функцию, то эта функция будет не применима, поскольку нельзя использовать функцию в левой части присваивания.
Отсутствие симметрии (допустимо изменять объявление функции на объявление атрибута, но не наоборот) неприятно, но неизбежно и не является на практике серьезным препятствием. Оно означает, что объявление некоторого компонента атрибутом является окончательным и необратимым выбором, в то время как объявление его функцией все еще оставляет место для последующих реализаций через память, а не через вычисление.