учебники, программирование, основы, введение в,

 

Техника наследования

Наследование и утверждения
Обладая изрядной мощью, наследование может быть и опасным. Не будь механизма утверждений, создатели классов могли бы весьма "вероломно" пользоваться повторными объявлениями и динамическим связыванием для изменения семантики операций без возможности контроля со стороны клиента. Утверждения способны на большее: они дают нам боле глубокое понимание природы наследования. Не будет преувеличением сказать, что лишь понимание принципов Проектирования по Контракту позволяет в полной мере постичь сущность концепции наследования.
Вкратце мы уже очертили основные правила, управляющие взаимосвязью наследования и утверждений: все утверждения (предусловие и постусловия подпрограмм, инварианты классов), заданные в классах-родителях, остаются в силе и для их потомков. В этом разделе мы уточним эти правила и используем полученные результаты, чтобы дать новый взгляд на наследование как на субподряды (subcontracts).
Инварианты
С правилом об инвариантах класса мы встречались и прежде:
Правило родительских инвариантов
Инварианты всех родителей применимы и к самому классу.
Инварианты родителей добавляются к классу. Инварианты соединяются логической операцией and then. (Если у класса нет явного инварианта, то инвариант True играет эту роль.) По индукции в классе действуют инварианты всех его предков, как прямых, так и косвенных.
Как следствие, выписывать инварианты родителей в инварианте потомка еще раз не нужно (хотя семантически такая избыточность не вредит: a and then a есть то же самое, что a).
Полностью восстановленный инвариант класса можно найти в плоской и краткой плоской форме последнего .
Предусловия и постусловия при наличии динамического связывания
В случае с предусловиями и постусловиями ситуация чуть сложнее. Общая идея, как отмечалось, состоит в том, что любое повторное объявление должно удовлетворять утверждениям оригинальной подпрограммы. Это особенно важно, если подпрограмма отложена: без такого ограничения на будущую реализацию, задание предусловие и постусловий для отложенных подпрограмм было бы бесполезным или, хуже того, привело бы к нежелательному результату. Те же требования к предусловию и постусловию остаются и при переопределении эффективных подпрограмм.
Анализируя механизмы повторного объявления, полиморфизма и динамического связывания, можно дать точную формулировку искомого правила. Но для начала представим типичный случай.
Рассмотрим класс и его подпрограммы, имеющие как предусловие, так и постусловие:
На показан клиент 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.
Все это является основой Проектирования по Контракту: в момент вызова подпрограммы клиент должен обеспечить соблюдение предусловия, а в ответ при возврате из подпрограммы он полагается на выполнение постусловия.
Что происходит, когда вводится наследование?
Пусть новый класс 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, гарантирующего при вызове и ожидающего выполнения β? Есть два пути:

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

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

Как уже было сказано, утверждение A называется более сильным, чем B, если A логически влечет B, но отличается от него: например, x >= 5 сильнее, чем x >= 0. Если утверждение A сильнее утверждения B, говорят еще, что утверждение B слабее утверждения A.
Как быть честным
Теперь нам понятно, как обманывать. Но как же быть честным? Объявляя подпрограмму повторно, мы можем сохранить ее исходные утверждения, но также мы вправе:

  • заменить предусловие более слабым;
  • заменить постусловие более сильным.

Первый подход символизирует щедрость и великодушие: мы допускаем большее число случаев, чем изначально. Это не причинит вред клиенту, который на момент вызова удовлетворяет исходному предусловию. Второй подход означает, что мы выдаем больше, чем от нас требовалось. Это не причинит вред клиенту, полагающемуся на выполнение по завершении вызова исходных постусловий.
Итак, основное правило:
Правило (1) Утверждения Переобъявления (Assertion Redeclaration)
При повторном объявлении подпрограммы предусловие может заменяться лишь равным ему или более слабым, постусловие - лишь равным ему или более сильным.
Это правило отражает тот факт, что новый вариант подпрограммы не должен отвергать вызовы, допустимые в оригинале, и должен, как минимум, представлять гарантии, эквивалентные гарантиям исходного варианта. Он вправе, хоть и не обязан, допускать большее число вызовов или давать более сильные гарантии.
Как явствует из названия, это правило применимо к обеим формам повторного объявления: переопределению и реализации отложенного компонента. Второй случай важен особо, - утверждения будут связаны со всеми эффективными версиями потомков.
Утверждения подпрограммы, как отложенной, так и эффективной, задают ее семантику, применимую к ней самой и ко всем повторным объявлениям ее потомков. Точнее говоря, они специфицируют область допустимого поведения подпрограммы и ее возможных версий. Любое повторное объявление может лишь сужать эту область, не нарушая ее.
Как следствие, создатель класса должен быть осторожным при написании утверждений эффективной подпрограммы, не привнося излишнюю спецификацию (overspecification). Утверждения должны описывать намерения подпрограммы, - ее абстрактную семантику, - но не свойства реализации. Иначе можно закрыть возможность создания иной реализации подпрограммы у будущих потомков.
Пример
Предположим, я написал класс MATRIX, реализующий операции линейной алгебры. Среди прочих возможностей я предлагаю своим клиентам подпрограмму расчета обратной матрицы. Фактически это сочетание команды и двух запросов: процедура invert инвертирует матрицу, присваивает атрибуту inverse значение обратной и устанавливает логический атрибут inverse_valid. Значение атрибута inverse имеет смысл тогда и только тогда, когда inverse_valid является истинным; в противном случае матрицу инвертировать не удалось, так как она вырождена. В ходе нашего обсуждения случай вырожденной матрицы мы можем проигнорировать.
Конечно же, я могу найти лишь приближенное значение обратной матрицы и готов гарантировать определенную точность расчетов, однако, не владея численными подпрограммами в совершенстве, буду принимать лишь запросы с точностью не выше 10-6. В итоге, моя подпрограмма будет выглядеть приблизительно так:
invert (epsilon: REAL) is
-- Обращение текущей матрицы с точностью epsilon
require
epsilon >= 10 ^ (-6)
do
"Вычисление обратной матрицы"
ensure
((Current * inverse) |-| One) <= epsilon
end

Постусловие предполагает, что класс содержит инфиксную функцию infix "|-|" такую, что m1 |-| m2 есть |m1 - m2| (норма разности матриц m1 и m2), а также функцию infix "*", результатом которой является произведение двух матриц. One - единичная матрица.
Как человек негордый, летом я приглашу программиста, и он перепишет мою подпрограмму invert, используя более удачный алгоритм, лучше аппроксимирующий результат и допускающий меньшее значение epsilon (как повторное объявление, эта запись синтаксически некорректна:
require
epsilon >= 10 ^ (-20)
...
ensure
((Current * inverse) |-| One) <= (epsilon / 2)

Автор новой версии достаточно умен, чтобы не переписывать MATRIX в целом. Изменения коснутся лишь нескольких подпрограмм. Они будут включены в состав порожденного от MATRIX класса NEW_MATRIX.


Если повторное объявление содержит новые утверждения, они должны иметь иной синтаксис, нежели приведенный выше. Правило появится чуть позднее.

Изменения, внесенные в утверждения, удовлетворяют правилу повторного объявления: новое предусловие epsilon >= 10 ^ (-20) слабее исходного epsilon >= 10 ^ (-6), новое же постусловие сильнее сформулированного вначале.
Вот как все должно происходить. Клиент исходного класса MATRIX запрашивает расчет обратной матрицы именно у него, но на деле - ввиду динамического связывания - вызывает реализацию класса NEW_MATRIX. Тот же клиент может иметь в своем составе подпрограмму
some_client_routine (m1: MATRIX; precision: REAL) is
do
... ; m1.invert (precision); ...
-- Возможен вызов версии как MATRIX, так и NEW_MATRIX
end

которой один из его собственных клиентов передает первый параметр типа NEW_MATRIX.
NEW_MATRIX должен воспринимать и корректно обрабатывать любой вызов, который принимается его предком. Используя более слабое предусловие и более сильное постусловие, мы корректно обработаем все обращения клиентов MATRIX и предложим своим клиентам решение, лучше прежнего.
При усилении предусловия invert, например, epsilon >= 10 ^ (-5), вызов, корректный для класса MATRIX, мог стать теперь некорректным. При ослаблении постусловия возвращаемый результат стал бы хуже, чем гарантируемый для MATRIX.
Устранение посредника
Последний комментарий указывает на весьма интересное следствие правила Утверждений Переобъявления. В общей схеме
утверждения γ и , введенные при повторном объявлении, предпочтительнее для клиентов, если они отличаются от и β (предусловия - более слабые, постусловия - более сильные). Но клиент класса A, использующий A' благодаря полиморфизму и динамическому связыванию, не может в полной мере воспользоваться более выгодным контрактом, ибо единственный контракт клиента заключен с классом A.
Воспользоваться преимуществом нового контракта можно лишь став непосредственным клиентом A' (пунктирная связь с вопросительным знаком на), как в случае:
a1: A'
...
if a1.γ then a1.r end
check a1. end      -- постусловие выполняется

При этом вы, естественно, объявляете a1 как объект типа A', а не объект типа A, как прежде. В результате теряется универсальность полиморфизма, идущая от A.
Компромисс ясен. Клиент класса MATRIX должен обеспечивать выполнение исходного (более сильного) предусловия, а в ответ вправе ожидать выполнения исходного (более слабого) постусловия. Даже если его запрос динамически подготовлен к обслуживанию классом NEW_MATRIX, воспользоваться новыми возможностями - большей толерантностью входа и большей точностью выхода - ему никак не удастся. Для обращения к улучшенной спецификации клиент должен объявить матрицу типа NEW_MATRIX, тем самым, потеряв доступ к иным порожденным от MATRIX реализациям, не являющимся производными классами самого NEW_MATRIX.
Субподряды
Правило Утверждения Переобъявления великолепно сочетается с теорией Проектирования по Контракту.
Мы видели, что утверждения подпрограммы описывают связанный с ней контракт, в котором клиент гарантирует выполнение предусловия, получая право рассчитывать на истинность постусловия; для поставщика все наоборот.
Наследование совместно с повторным объявлением и динамическим связыванием приводит к созданию субподрядов. Приняв условия контракта, вы не обязаны выполнять его сами. Подчас вы знаете кого-то еще, способного сделать это лучше и с меньшими издержками. Так происходят, когда клиент запрашивает подпрограмму из MATRIX, но благодаря динамическому связыванию может на этапе выполнения фактически вызывать версию, переопределенную в потомке. "Меньшие издержки" означают здесь более эффективную реализацию, как в знакомом нам примере с периметром прямоугольника, а "лучше" - усовершенствование утверждений, в описанном здесь смысле.
Правило Утверждения Переобъявления просто устанавливает, что честный субподрядчик, приняв условия контракта, должен выполнить работу на тех же условиях, что и подрядчик или лучших, но никак не худших.
С позиции Проектирования по Контракту, инварианты классов - это ограничения общего характера, применимые и к подрядчикам, и к клиентам. Правило родительских инвариантов отражает тот факт, что все подобные ограничения передаются субподрядчикам.
Свое истинное значение для ОО-разработки наследование приобретает лишь совместно с утверждениями и двумя приведенными выше правилами. Метафора контрактов и субподрядов - прекрасная аналогия, помогающая разрабатывать корректное ОО-ПО. Несомненно, в этом - одна из центральных идей теории проектирования.
Абстрактные предусловия
Правило ослабления предусловий может оказаться чересчур жестким в случае, когда наследник понижает уровень абстракции, характерный для его предка. К счастью, есть легкий обходной путь, полностью согласующийся с теорией.
Типичным примером этого является порождение BOUNDED_STACK от универсального класса стека (STACK). Процедура занесения в стек элемента (put) в порожденном классе имеет предусловие count <= capacity, где count - текущее число элементов в стеке, capacity - физическая емкость накопителя.
В общем понятии стека нет понятия емкости. Поэтому создается впечатление, будто при переходе к BOUNDED_STACK предусловие приходится усилить (от бесконечной емкости перейти к конечной). Как выстроить структуру наследования, не нарушая правило Утверждения Переобъявления?
Ответ становится очевиден, если мы ближе познакомимся с требованиями к клиенту. То, что нужно сохранить или ослабить, не обязательно является конкретным предусловием, как оно видится в реализации поставщика (реализация это его забота), но касается предусловия, как оно видится клиенту. Пусть процедура put класса STACK имеет вид:
put (x: G) is
-- Поместить x на вершину.
require
not full
deferred
ensure
...
end

где функция full всегда возвращает ложное значение, а значит, стек по умолчанию никогда не бывает полным.
full: BOOLEAN is
-- Заполнено ли представление стека?
-- (По умолчанию, нет)
do Result := False end

Тогда в BOUNDED_STACK достаточно переопределить full:
full: BOOLEAN is
-- Заполнено ли представление стека?
-- (Да, если число элементов равно емкости стека)
do Result := (count = capacity) end

Предусловие, такое как not full, включающее свойство, которое переопределяется потомками, называется абстрактным (abstract) предусловием.
Такое использование абстрактных предусловий для соблюдения правила Утверждения Переобъявления может показаться обманом, однако это не так. Несмотря на то, что конкретное предусловие фактически становится более сильным, абстрактное предусловие не меняется. Важно не то, как реализуется утверждение, а то, как оно представлено клиентам в интерфейсе класса (краткой или плоско-краткой форме). Предваренный условием вызов
if not s.full then s.put (a) end

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

  • ограниченный стек является стеком;
  • в стек всегда можно добавить еще один элемент.

Если предпочесть первое свойство и допускать порождение BOUNDED_STACK от STACK, мы должны согласиться с тем, что общее понятие стека включает предположение о невозможности в ряде случаев выполнить операцию put, абстрактно выраженное запросом full.


Было бы ошибкой включить в виде постусловия подпрограммы full в классе STACK выражение Result = False или (придерживаясь рекомендуемого стиля, эквивалентный ему) инвариант not full. Это - случай излишней спецификации, ограничивающей свободу реализации компонентов потомками класса.

Правило языка
Правило Утверждений Переобъявления, так как оно сформулировано, является концептуальным руководством. Как преобразовать его в безопасное и проверяемое правило языка?
В принципе, чтобы убедиться в том, что старые предусловия влекут новые, а новые постусловия - старые, следует провести логический анализ тех и других утверждений. К сожалению, это требует наличия сложного механизма доказательства теорем (несмотря на десятилетия исследований в области искусственного интеллекта). Его применение в компиляторе пока не реально.
К счастью, возможно простое техническое решение. Нужное нам правило можно сформулировать через простое лингвистическое соглашение, основанное на том наблюдении, что для любых утверждений a и b:

  • влечет or γ независимо от значения γ;
  • β and влечет β независимо от значения .

Итак, гарантируется, что новое предусловие слабее исходного либо равно ему, если оно имеет вид or γ. Гарантируется, что новое постусловие сильнее исходного β либо равно ему, если оно имеет вид β and . Отсюда следует искомое языковое правило:
Правило (2) Утверждения Переобъявления
При повторном объявлении подпрограммы нельзя использовать предложения require или ensure. Вместо них следует использовать предложение, начинающееся с:

  • require else, объединенное с исходным предусловием логической связкой or
  • ensure then, объединенное с исходным постусловием логической связкой and.

При отсутствии таких предложений действуют исходные утверждения.
Заметим, что используются нестрогие булевы операторы and then и or else, а не обычные and и or, хотя чаще всего это различие несущественно.
Иногда получаемые утверждения могут оказаться сложнее, чем необходимо на самом деле. В примере с подпрограммой обращения матриц, где исходным было утверждение
invert (epsilon: REAL) is
-- Обращение текущей матрицы с точностью epsilon
require
epsilon >= 10 ^ (-6)
...
ensure
((Current * inverse) |-| One) <= epsilon
мы не вправе в повторном объявлении использовать require и ensure, поэтому результат
примет вид
...
require else
epsilon >= 10 ^ (-20)
...
ensure then
((Current * inverse) |-| One) <= (epsilon / 2)

а стало быть, предусловие формально станет таким: (epsilon >= 10 ^ (-20)) or else (epsilon >= 10 ^ (-6)).
Ситуация с постусловием аналогична. Такое расширение не имеет особого значения, поскольку преобладает более слабое предусловие или более сильное постусловие. Если γ влечет , то or else γ имеет то же значение, что и . Если β влечет , то β and then имеет то же значение, что и β. Поэтому математически предусловие повторного объявления есть: epsilon >= 10 ^ (-20), а его постусловие есть: ((Current * inverse) |-| One) <= (epsilon / 2), хотя запись утверждений в программе (а также, вероятно, их расчет во время выполнения при отсутствии средств символьных преобразований) является более сложной.
Повторное объявление функции как атрибута
Правило Утверждения Переобъявления нуждается в небольшом дополнении ввиду возможности при повторном объявлении задать функцию как атрибут. Что произойдет с предусловием функции и ее постусловием, если таковые имелись?
Атрибут доступен всегда, а потому мы вправе считать, что его предусловие равно True. В итоге можно полагать, что предусловие атрибута, согласно правилу Утверждения Переобъявления, было ослаблено.
Но атрибут не имеет постусловий. Мы же должны гарантировать, что он наделен всеми свойствами, заданными исходной функцией. Поэтому (в дополнение к правилу Утверждения Переобъявления) будем считать, что в этом случае автоматически постусловие добавляется к инварианту класса. Плоская форма класса будет содержать это условие в составе своего инварианта.


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

Замечание математического характера
Неформально, правило Утверждения Переобъявления гласит: "Повторное объявление утверждений может лишь сужать область допустимого поведения, не нарушая ее". Сейчас, завершая обсуждение этой темы, приведем строгую формулировку данного свойства.
Пусть подпрограмма реализует частичную функцию r, отображающую множество возможных входных состояний I в множество возможных выходных состояний O. Утверждения подпрограммы определяют правила действия r и ее возможных переопределений.

  • Предусловие задает область определения DOM функции r (подмножество I, на котором r гарантированно вырабатывает результат).
  • Постусловие задает для каждого x из DOM подмножество RESULTS(x) множества O, такое, что r (x) RESULTS (x). Так как постусловие не всегда однозначно описывает результат, это подмножество может иметь больше одного элемента.

Правило Утверждения Переобъявления означает, что повторное объявление может расширять область определения и сужать множество результатов. Пометив новые множества знаком ', запишем требования, закрепленные этим правилом:
DOM'  DOM
RESULTS' (x)  RESULTS (x) для всех x из DOM

Предусловие устанавливает, что подпрограмма и ее повторные объявления, как минимум, должны принимать некоторые входы (DOM), хотя повторные объявления могут это множество и расширить. Постусловие говорит, что результаты, возвращаемые подпрограммой и ее повторными объявлениями, могут, самое большее, содержать значения из RESULTS(x), однако, постусловия при повторных объявлениях могут это множество сузить.
В этом описании состояние системы в период выполнения определяется состоянием (значениями) всех достижимых объектов. Кроме того, входные состояния (элементы I) также включают в себя значения аргументов. Более подробное введение в математическое описание программ и языков программирования см. в [M 1990].

Глобальная структура наследования

Ранее мы уже ссылались на универсальные (universal) классы GENERAL и ANY, а также на безобъектный (objectless) класс NONE. Пришло время пояснить их роль и представить глобальную структуру наследования.
Универсальные классы
Удобно использовать следующее соглашение:
Правило Универсального Класса
Любой класс, не содержащий предложение наследования, неявно содержит предложение вида:
inherit ANY,
ссылающееся на класс ANY из библиотеки Kernel.
Тем самым становится возможным определить по умолчанию целый ряд компонентов, наследуемых всеми классами. Эти компоненты реализуют общие, универсальные операции: копирование, клонирование, сравнение, базовый ввод и вывод.
Для большей гибкости поместим эти компоненты в класс GENERAL, чьим потомком является ANY. Сам класс ANY по умолчанию не имеет никаких компонентов, будучи классом вида: class ANY inherit GENERAL end. При создании нового проекта его менеджер может решить, какие общие для проекта компоненты следует включить в класс ANY, в то время как GENERAL остается всегда неизменным.


Для построения нетривиального ANY можно прибегнуть к наследованию. В самом деле, класс ANY можно породить от некоторого HOUSE_STYLE или нескольких таких классов, не вводя циклы в иерархию наследования и не нарушая правило об универсальном классе: достаточно сделать класс HOUSE_STYLE и другие классы потомками GENERAL. Вынесенный на текст "Классы разработчика" означает все классы, написанные разработчиком и не порожденные от GENERAL явным образом.

Нижняя часть иерархии
На представлен также класс NONE, антипод класса ANY, потомок всех классов, не имеющих собственных наследников и превращающий глобальную иерархию наследования классов в решетку (математическую структуру). NONE не имеет потомков, его нельзя переопределить - это лишь удобная фикция, однако, теоретическое существование такого класса оправдано и служит двум практическим целям:

  • Void - пустая ссылка, используемая наряду с другими ссылками, по соглашению имеет тип NONE. (Фактически, Void -это один из компонентов класса GENERAL.)
  • Чтобы скрыть компонент от всех клиентов, достаточно экспортировать его только классу NONE. Предложение feature {NONE}(практически эквивалентное feature {}, но записанное явно) или предложение наследования export {NONE}(на практике дающее тот же результат, что и export {}), делает компонент недоступным для любого класса, написанного разработчиком, ибо NONE не имеет потомков. Обратите внимание на то, что NONE скрывает и все свои компоненты.

Первое свойство объясняет, почему значение Void можно присвоить любому элементу ссылочного типа данных. До сих пор статус Void оставался некой загадкой, теперь, когда Void связано с классом NONE, этот статус становится очевидным, официальным и согласующимся с системой типов: по построению NONE является потомком всех классов, а потому мы можем использовать Void как допустимое значение любой ссылки, не нарушая правил описания типов.
По симметрии ко второму свойству заметим, что объявление, начинающееся с feature и экспортирующее все компоненты во все классы, написанные разработчиком, считается сокращением от feature {ANY}. Для повторного экспорта во все классы компонента родителя, доступ к которому был ограничен, можно использовать предложение export {ANY} или его не столь очевидное сокращение export.
Классы ANY и NONE обеспечивают замкнутость системы типов и полноту структуры наследования: решетка (это строго определенный математический термин) имеет свой верхний и нижний элемент.
Универсальные компоненты
Вот лишь некоторые компоненты, содержащиеся в классе GENERAL, а значит, доступные всем другим классам. Часть из них была введена и использована в предшествующих лекциях курса:

  • clone для создания клона (дубля) объекта, а также его "глубинный" вариант deep_clone для рекурсивного дублирования полной структуры объекта;
  • copy для копирования содержимого одного объекта в другой;
  • equal для сравнения объектов (поле-с-полем), а также его "глубинный" вариант deep_equal;
  • print и print_line - печать простого представления по умолчанию любого объекта (default representation);
  • tagged_out - строка, содержащая представление по умолчанию любого объекта, в котором каждое поле сопровождается своей меткой (tag) (соответствующим именем атрибута);
  • same_type и conforms_to - булевы функции, сопоставляющие тип текущего объекта с типом другого;
  • generator - возвращает имя порождающего (generating) класса объекта, то есть класса, экземпляром которого является данный объект.

Замороженные компоненты

При обсуждении идеи наследования неоднократно подчеркивался принцип Открыт-Закрыт - право, взяв компонент класса-родителя, переопределить его, возложив на него иные задачи. Могут ли появиться причины запрета такой возможности?
Запрет повторного объявления
Обсуждение утверждений в начале лекции дало нам теоретическое понимание сути переопределений. Часть "Открыт" принципа Открыт-Закрыт дает возможность изменять компоненты потомков, но под контролем утверждений. Разрешены лишь те повторные объявления, для которых реализация согласуется со спецификацией, заданной предусловием и постусловиям оригинала.
В ряде случаев клиентам класса и клиентам классов потомков нужна гарантия, что компонент не только соблюдает спецификацию, но и пользуется в точности исходной реализацией. Достичь этого можно лишь "заморозив" его реализацию - полностью запретив переопределение компонента. Подобную возможность дает простая языковая конструкция:
frozen feature_name ... is... Остальные объявления - как обычно...

При таком описании ни один из потомков класса не может включать данный компонент в предложения redefine и undefine ни под своим, ни под любым другим именем (смена имен, конечно же, по-прежнему разрешена). Отложенный компонент по своей сути должен быть переопределен и, следовательно, не может быть заморожен.

 

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г.Яндекс.Метрика