Personal tools
You are here: Home Статьи Как использовать IAdding и INameChooser
Document Actions

Как использовать IAdding и INameChooser

by cray last modified 2007-12-23 14:36

На примере интерфейса IAdding и адаптера INameChooser рассматривается важная концепция программирования с использованием компонентного подхода.

Как использовать IAdding и INameChooser :

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

Краткое введение:

Взаимодействие двух компонент в Zope3 может быть построено так. Пусть есть компонент A, компонент B и множество алгоритмов взаимодействия {C}. Использование компонентной модели предполагает, что в качестве алгоритма используется специальный адаптер к интерфейсу, который используется для управления алгоритмом, тогда множество интефейсов {IC} будет соответствовать множеству алгоритмов. Реализуя выбор алгоритма в зависимости от интерфейсов аргументов, можно пойти двумя альтернативными путями:

  • Использование реестра мультиадаптеров и двойного адаптирования:
    реестр
    {IC(x,y):x,y -> IC};
    код
    IC(A,B).do().

    Такой способ гарантирует, что для компонент будет выбран правильный алгоритм, в соответствии с предоставляемыми ими интерфейсами. К сожалению, это порождает понятную N x M проблему - количество алгоритмов в общем случае растет пропорционально квадрату компонент.

  • Использование реестров одиночных адаптеров комплиментарного адаптирования внутри реализации алгоритма:
    реестр
    {IC(x):x->IC; IC'(y):y->IC'};
    код
    IC(A).(F(do))(B), причем F: do(x) -> do(IC'(x)).

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

    Комплиментарное адаптирование позволяет снизить количество алгоритмов, за счет относительной стандартизации IC или IC' (что, в общем, безразлично).

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

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

Использование INameChooser:

Где-то в глубинах IAdding :

            Zope-3.3/lib/python/zope/app/container/browser/adding.py

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

Проблема решается вырожденным комплиментарным адаптированием: вводится интерфейс INameChooser, предоставляющий две функции:

chooseName: (name, object) -> name
выбрать имя,
checkName: (name, object) -> bool
проверить имя.

Простейший код, использующий NameChooser выглядит так:

            container[INameChooser(container).chooseName(name, object)] = object

Заглянем в код простейшего адаптера :

            class NameChooserAdapter(NameChooser):

                zope.component.adapts(IContainer)
                zope.interface.implements(INameChooser)

                def chooseName(self, name, ob):
                    if not name:
                        name = u'klass:' + ob.__class__.__name__
                    return  super(NameChooserAdapter, self).chooseName(name, ob)

Здесь используется вырожденный случай (т.е. мы не делаем комплиментарного интерфейса и адаптера к нему, так как предполагаем что атрибут __class__ есть у любого объекта). Слово klass добавляется к имени объекта для того, чтобы было гарантировано видно, что используется именно этот адаптер, а не какой-то другой. Обратите, внимание, что имя создаваемому объекту даётся принудительно только в том случае, если желаемое имя не указано явно.

Нарисуем (вполне практический) невырожденный случай:

            import time

            class NameChooserAdapter(NameChooser):

                zope.component.adapts(IContainer)
                zope.interface.implements(INameChooser)

                def chooseName(self, name, ob):
                    if not name:
                        name = time.strftime(
                            "%Y%m%d%H%M",
                            time.localtime((IPersistent(ob)._p_mtime))
                        )
                return super(NameChooserAdapter, self).chooseName(name, ob)

Данный адаптер при добавлении объектов IContainer c пустым именем будет вместо имени подставлять текущее время в определённом формате. Это уже точно соответствует смыслу комплиментарного адаптирования, и можно было бы закончить на этом, но интересно обратить внимание еще на один поворот этой темы: создается впечатление, что INameChooser подразумевает, что контейнер решает, как делать выбор: в конфигах прописывается адаптер к INameChooser для конкретного контейнера, и вроде бы для всех добавляемых объектов будет использоваться один и тот же алгоритм. Но давайте вспомним про комплиментарное адаптирование :

            class NameChooserAdapter(object):

                zope.component.adapts(IContainer)
                zope.interface.implements(INameChooser)

                def __init__(self, container):
                    self.container = container

                def chooseName(self, name, ob):
                    return INameChooserItem(ob).chooseName(name, self.container)

                def checkName(self, name, content):
                    if not bool(name) :
                        raise UserError(u"Empty name not possible")
                    elif name in self.container :
                        raise UserError(u"The given name is already being used")

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

            import re

            match = re.compile("^(?P<name>.*[^0-9])(?P<number>[0-9]+)$").match

            class NameChooserItemBase(object) :
                zope.component.adapts(Interface)
                zope.interface.implements(INameChooserItem)

                def __init__(self, ob) :
                    self.ob = ob

                def chooseName(self, name, container) :
                    if name :
                        while name in container :
                            res = match(name)
                            if res :
                                d = res.groupdict()
                                d['number'] = int(d['number']) + 1
                                name = "%(name)s%(number)u" % d
                            else :
                                name = name + "-0"

                    return name

Базовый адаптер можно зарегистрировать для всех типов объектов (он не зависит от типа), а использующий INameChooserItem адаптер NameChooserAdapter - зарегистрировать как адаптер для IContainer (т.е. практически для всех контейнеров).

Теперь напишем пару специализированных адаптеров INameChooserItem. Пусть, например, все новости (объекты интерфейсов INote) добавляются с указанием в качестве имени даты их создания:

            from note.interfaces import INote
            import zope.component

            class NameChooserItemNoteAdapter(NameChooserItemBase):

                zope.component.adapts(INote)

                def chooseName(self, name, container):
                    if not name:
                        name = INote(self.ob).datetime.strftime("%Y%m%d%H%M-0")
                    return super(NameChooserItemNoteAdapter, self).chooseName(name, container)

Ну а все записные книжки с указанием заголовка:

            from note.interfaces import INote
            from notebook.interfaces import INotebook

            class NameChooserItemNotebookAdapter(NameChooserItemBase):

                zope.component.adapts(INotebook)

                def chooseName(self, name, container):
                    if not name:
                        name = INotebook(self.ob).title
                    return super(NameChooserItemNotebookAdapter, self).chooseName(name, container)

А все папки (Folder) добавляем с указанием системного времени:

            import time

            class NameChooserItemFolderAdapter(NameChooserItemBase):

                zope.component.adapts(IFolder)

                def chooseName(self, name, container):
                    if not name:
                        name = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))
                    return super(NameChooserItemFolderAdapter, self).chooseName(name, container)

Ну и напоследок, вернемся к INameChooserAdapter и вернем в него некий инвариант, который мы вынесли в NameChooserItemBase (данные примеры присутствуют в кодах, но отключены; для их изучения и использования необходимо нужным образом отредактировать файлы configure.zcml для подключения соответствующих адаптеров):

            class NameChooserAdapterNG(NameChooser) :
                zope.component.adapts(IContainer)
                zope.interface.implements(INameChooser)

                def chooseName(self, name, ob) :
                    super(NameChooserAdapter,self).chooseName(
                            INameChooserItem(ob).chooseName(name, context),
                            ob)

            class NameChooserItemBaseNG(object) :
                zope.component.adapts(Interface)
                zope.interface.implements(INameChooserItem)

                def __init__(self,ob) :
                    self.ob = ob

                def chooseName(self,name,container) :
                    return name

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

Заключение и выводы:

В статье были рассмотрены способы вынесения алгоритма взаимодействия компонентов в отдельный адаптер, было введено два таких решения. Одно из них - с использованием комплиментарного адаптирования - было продемонстрировано на интерфейсе INameChooser, причем был показан способ изменения решающего правила алгоритма с одного участника взаимодействия на другого, оставаясь в рамках уже настроенной и работающей среды.

Последний способ и является основным нововведением данной статьи.

Приложение 1:

Первая проблема, на которую вы наверняка наткнётесь при написании собственного адаптера к INameChooser, возникает тогда, когда вы захотите написать такой адаптер, который вместо имени объекта подставляет некоторое свойство создаваемого объекта, например, как в примере с NameChooserItemNotebookAdapter, свойство title.

Проблема состоит в том, что попытка внутри адаптера написать просто так нечто следующее:

        name = INotebook(self.ob).title

не приведёт ни к чему ожидаемому и в результате работы данного адаптера вместо ожидаемого значения поля title свойству __name__ присвоится значение None (в лучшем случае). Задумавшись, и посмотрев в отладчик, можно прийти к выводу, что причина подобной ситуации в том, что в момент взятия свойства title добавляемый объект его ещё не имеет.

Как быть, и как обяснить то, что данная строка кода работает в приведённом примере? Легко. Ответ кроется в использовании директивы addform. Оказывается, все свойства объекта можно разделить на 3 группы: свойства, устанавливаемые до добавления объекта, свойства, устанавливаемые вместе с объектом и свойства, устанавливаемые после добавления объекта. То, что в нашем случае у объекта отсутствует свойство title обясняется тем, что оно устанавливается вместе с объектомм, а адаптер, как можно понять, вызывается перед тем, как происходит добавление объекта в zodb.

Решение данной проблемы заключается в том, что для свойства title необходимо явно указать, что его следует установить до того, как производить добавление объекта, и для этого Zope3 предоставляет специальный атрибут set_before_add директивы addform, которая для объектов Notebook выглядит следующим образом:

        <addform
            label="Add Notebook"
            name="AddNotebook.html"
            schema="..interfaces.INotebook"
            content_factory="..notebook.Notebook"
            permission="zope.ManageContent"
            set_before_add="title"
        />

Благодаря использованию set_before_add свойство title будет установлено до того, как произойдёт вызов написаного адаптера и он будет работать имено так, как это ожидалось изначально, в результате чего при неуказанном имени объекта вместо него будет использоваться значение свойства title этого объекта.

конец документа


Powered by Plone CMS, the Open Source Content Management System