Адаптация как способ ассоциации компонент
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, работает для всех перечисленных реализаций ассоциации классов.