2008-01-11

Адаптация как способ ассоциации компонент

Андрей Орлов  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.

В этом случае не только классы независимы друг от друга, но и их экземпляры хранятся и обслуживаются раздельно.

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

  1. Использованием специальной утилиты (например, "реестра");
  2. Введением дополнительного поля в интерфейс IStorageUtilitable и сохранением этого поля в компоненте;
  3. Введением вспомогательной аннотации, которая привязывается к компонентам, предоставляющим 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, работает для всех перечисленных реализаций ассоциации классов.

Официальный сайт Zope3 Московская группа изучения реактивного движения The Dream Bot Site noooxml