Как использовать IAdding и INameChooser
На примере интерфейса 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 этого объекта.
конец документа