Адаптация как способ ассоциации компонент
2008-01-11 18:57Ассоциацию (т.е. объединение компонент в одно целое) можно истолковать как такой способ обеспечения доступа к компонентам, который выглядит так, как будто компоненты представляют из себя одно целое. Естественным способом обеспечить это в рамках компонентной модели - воспользоваться адаптерами, написанными так, что бы находить компонент связанный с данным.
Ассоциация через адаптацию:
Компонентная модель позволяет использовать интерфейсы, не задумываясь о внутренней структуре предоставляющих их компонентов. Более того: адаптация позволяет привести к интерфейсу компонент, который этот интерфейс не предоставляет непосредственно, причем синтаксически это выгляди точно так же:
ia = IA(ob)
Когда говорят, что интерфейс "не предоставляется непосредственно", первое, что приходит в голову - это адаптация к интерфейсу данных, которые существуют в компоненте, хотя и предоставляются другим интерфейсом. В этой статье будет показан более интересный случай: адаптация, позволяющая использовать несколько компонент как единое целое, т.е. ассоциация компонент.
Ассоциация компонент может быть проведена различными способами, но некоторые из них наиболее распространены и имеют код поддержки:
- Наследование
- Способ ассоциации компонентов, основанный на множественном наследовании классов, реализующих разные интерфейсы.
- Аннотирование
- Способ ассоциации компонентов, основанный на применении интерфейса IAnnotations;
- Обращение к утилите
- Способ ассоциации компонентов, основанный на применении реестра утилит;
- Использовние контейнера
- Способ ассоциации компонентов, основанный на обращении к контейнеру, в который вложен компонент;
Перечисленные способы различаются не только деталями реализации, но даже рангом отношения. Тем не менее, как будет показано ниже, использование компонентного подхода позволяет полностью абстрагироваться от реализации и не только использовать универсальный код для работы с компонентами, ассоциированными любым способом, но и сам переход от одного способа ассоциации к другому свести, в некоторых случаях, к паре деклараций в ZCML.
Базовые интерфейсы и классы :
В качестве примера будем использовать компоненты гипотетической системы, реализующей учет отпуска товаров со склада и рассмотрим три составляющих: товар, склад и алгоритм выписки счетов.
Пусть IGoods - интерфейс, описывающий товар:
class IGoods(Interface) :
title = TextLine(title=u"Title")
price = Int(title=u"Price")
description = Text(title=u"Description")
Тогда Goods - класс, в экземплярах которого хранится описание товара:
class Goods(Persistent) :
implements(IGoods)
title = u""
price = 0
description = u""
Пусть IStorage - интерфейс, описывающий хранение товара на складе и доставку потребителю:
class IStorage(Interface) :
storage = Int(title=u"Storage Number")
delivery = Int(title=u"Delivery Price")
Тогда Storage - класс, в экземплярах которого хранится описание склада и доставки:
class Storage(Persistent) :
implements(IStorage)
storage = 0
delivery = 0
Пусть description - функция, используемая для получения описания (стоимости и т.п) продаваемого товара:
def description(ob,count) :
goods = IGoods(ob)
storage = IStorage(ob)
return (
"Title: %s\n"
"Storage: %u\n"
"Price: %u\n"
"Delivery: %u\n"
"Count: %u\n"
"-------------\n"
"Money: %u\n"
) % (
goods.title,
storage.storage,
goods.price,
storage.delivery
count,
goods.price * count + storage.delivery
)
Функция получает компонент, который можно привести к обоим интерфейсам, иными словами, этот компонент является ассоциацией Goods и Storage. Нижеследующие примеры демонстрируют, как эта ассоциация может быть реализована четырьмя разными способами таким образом, что функция description корректно работает без изменения своего алгоритма.
Наследование :
Множественное наследование - самый простой способ ассоциации, который, строго говоря, выходит за рамки компонентной модели и является частью обычного ООП. Его очевидный недостаток - любые изменения в составе ассоцированного объекта требуют перепрограммирования. Пример кода:
class GoodsInStorage(Goods,Storage) :
pass
Этот класс наследует реализацию обоих интерфейсов от своих суперклассов, и следовательно, при передаче экземпляра этого класса в функцию description, возможно выполнение приведения к обоим интерфейсам.
Аннотирование :
Аннотация - компонент, связанный с другим компонентом посредством специального протокола. Аннотация содержит дополнительную информацию к содержимому объекта и при правильном программировани выглядит как еще один интерфейс объекта. В то же время аннотация это самостоятельный объект с независимым (в общем случае) хранилищем.
Аннотирование - один из самых распространенных способов ассоциации в Zope3. Его очевидное достоинство в том, что изменение списка классов, ассоциированных с данным, возможно путем добавление пары директив конфигурации. Кода для реализации, однако, потребуется несколько больше.
Пусть основным классом будет Goods, а класс Storage будет аннотацией к нему. Чтобы обеспечить возможность настройки аннотирования, введем специальный интерфейс, предоставление которого классом будет информировать о том, что данный класс является хранителем аннотации Storage:
class IStorageAnnotable(Interface) :
pass
Этот интерфейс можно декларировать для класса Goods тремя строчками в ZCML:
<class = ".goods.Goods">
<implement interface=".interface.IStorageAnnotable"/>
</class>
Для обращения к аннотации Storage компонента, предоставляющего интерфейс IStorageAnnotable, требуется адаптер IStorageAnnotable к IStorage. Этот адаптер можно реализовать так:
def StorageAnnotation(ob) :
try :
st = IAnnotations(ob)['storage']
except KeyError :
st = IAnnotations(ob)['storage'] = Storage()
return IStorage(st)
Регистрация адаптера может быть написана так (при условии, что код адаптера напиcан в файле storageannotation.py):
<adapter
for = ".interface.IStorageAnnotable"
provides = ".interface.IStorage"
factory = ".storageannotation.StorageAnnotation"
/>
Таким образом, если для класса Goods декларирована реализация интерфейса IStorageAnnotable, то переданный экземпляр класса в функцию description можно привести к интерфейсу IGoods (прямая реализация интерфейса) и к интерфейсу IStorage (адаптация).
Очевидно, что оба класса совершенно независимы друг от друга, что является неким удобством (классы могут разрабатываться раздельно и ассоциироваться только в целях конкретного применения).
Обращение к утилите :
В отличие от предыдущих примеров, использование утилит позволяет использовать ассоциацию "Один-ко-многим". В этом случае оба класса, Storage и Goods, используются как контент-классы, причем экземпляр Storage должен быть зарегистрирован как утилита. Адаптер, аналогичный случаю аннотаций, можно реализовать так:
from zope.app.zapi import getUtility
def StorageUtility(ob) :
return getUtility(IStorage,context=ob)
Так же как и в случае аннотаций, введем интерфейс IStorageUtilitable, предоставление которого компонентом обозначает, что данный компонент ассоциирован с утилитой IStorage. Тогда адаптер регистрируется следующим образом:
<adapter
for = ".interface.IStorageUtilitable"
provides = ".interface.IStorage"
factory = "storageutility.StorageUtility"
/>
Как и в случае аннотаций, переданный функции description экземпляр класса Goods можно привести к IGoods и адаптировать к IStorage.
В этом случае не только классы независимы друг от друга, но и их экземпляры хранятся и обслуживаются раздельно.
Нужно обратить внимание и на то, что вообще говоря утилита имеет имя, в отличие от аннотации. В нашем адаптере принято, что это имя является пустым (т.е. константой). В реальной жизни выбор имени может быть решен каким-либо иным способом:
- Использованием специальной утилиты (например, "реестра");
- Введением дополнительного поля в интерфейс IStorageUtilitable и сохранением этого поля в компоненте;
- Введением вспомогательной аннотации, которая привязывается к компонентам, предоставляющим IStorageUtilitable и используемой для хранения поля названия.
В любом случае, решение этой проблемы - это не более чем еще один случай ассоциации компонент при помощи адаптации, и может быть решен аналогично описанию этой статьи.
Использование контейнера :
Такой способ ассоциации требует реализации композиции Storage и Goods, и адаптер полезен только как способ предоставить неотличимый от предыдущих способов сценарий использования.
Класс Storage придется доработать до контейнера:
class StorageContainer(Storage, BTreeContainer) :
implements(IStorageContainer)
В таком контейнере можно разместить несколько экземпляров класса Goods, причем желательно настроить интерфейсы таким образом, чтобы гарантировать возможность размещения Goods только в StorageContainer:
from zope.app.container.constraints import ContainerTypesConstraint
from zope.app.container.constraints import ItemTypePrecondition
class IStorageContent(IContained) :
__parent__. = Field(
constraint = ContainerTypesConstraint(IStorage))
class IStorageContainer(IContainer, IStorage) :
def __setitem__(name, value) :
pass
__setitem__.precondition \
= ItemTypePrecondition(IStorageContained)
Добавим три строчки в ZCML, чтобы декларировать реализацию интерфейса IStorageContent классом Goods:
<class = ".goods.Goods"/>
<implement interface=".interface.IStorageContent"/>
</class>
Адаптер к интерфейсу IStorage можно реализовать так:
def StorageUp(ob) :
return IStorage(IContained(ob).__parent__)
Регистрация адаптера :
<adapter
for = ".interface.IStorageContent"
provides = ".interface.IStorage"
factory = ".storageutility.StorageUp"
/>
Как и в предыдущих примерах, функция description окажется вполне работоспособной, если передать в нее экземпляр Goods.
В этом случае классы независимы друг от друга, хотя и существует код, декларирующий их взаимодействие и один из классов специально перепрограммирован, чтобы композироваться с другим.
Немного про формы редактирования :
Интересно отметить, что во всех четырех случаях могут быть созданы формы, позволяющие добавлять или редактировать каждый из интерфейсов, к которому может быть приведен объект Goods, причем декларация форм может быть сделана универсальной:
<editform
for = ".interfaces.IGoods"
schema = ".interfaces.IStorage"
name = "storage.html"
label = "Storage"
/>
<editform
for = ".interfaces.IGoods"
schema = ".interfaces.IGoods"
name = "goods.html"
label = "Goods"
/>
Форма добавления может задействовать только один из двух интерфейсов, хотя возможно и более продвинутое решение: создание специального адаптера к объединенному интерфейсу IStorage и IGoods. Этот же адаптер может использоваться и для создания общей формы редактирования ассоциированных объектов.
Какой бы вариант не был выбран, суть остается той же: единая декларация форм редактирования и добавления, которая, также как и функция description, работает для всех перечисленных реализаций ассоциации классов.



