Использование реестра адаптеров

Данный документ содержит небольшую демонстрацию пакета zope.interface и его реестра адаптеров. Документ рассчитывался как конкретный, но более узкий пример того как использовать интерфейсы и адаптеры вне Zope 3.

Сначала нам необходимо импортировать пакет для работы с интерфейсами:

>>> import zope.interface

Теперь мы разработаем интерфейс для нашего объекта - простого файла. Наш файл будет содержать всего один атрибут - body, в котором фактически будет сохранено содержимое файла:

>>> class IFile(zope.interface.Interface):
...
...     body = zope.interface.Attribute(u'Содержимое файла.')
...

Для статистики нам часто необходимо знать размер файла. Но было бы несколько топорно реализовывать определение размера прямо для объекта файла, т.к. размер больше относится к мета-данным. Таким образом мы создаем еще один интерфейс для представления размера какого-либо объекта:

>>> class ISize(zope.interface.Interface):
...
...     def getSize():
...         'Return the size of an object.'
...

Теперь мы должны создать класс реализующий наш файл. Необходимо что бы наш объект хранил информацию о том, что он реализует интерфейс IFile. Мы также создаем атрибут с содержимым файла по умолчанию (для упрощения нашего примера):

>>> class File(object):
...
...      zope.interface.implements(IFile)
...      body = 'foo bar'
...

Дальше мы создаем адаптер, который будет предоставлять интерфейс ISize получая любой объект предоставляющий интерфейс IFile. По соглашению мы используем атрибут __used_for__ для указания интерфейса который как мы ожидаем предоставляет адаптируемый объект, IFile в нашем случае. На самом деле этот атрибут используется только для документирования. В случае если адаптер используется для нескольких интерфейсов можно указать их все в виде кортежа.

Опять же по соглашению конструктор адаптера получает один аргумент - context (контекст). В нашем случае контекст - это экземпляр IFile (объект, предоставляющий IFile) который используется для получения из него размера. Так же по соглашению контекст сохраняется а адаптере в атрибуте с именем context. Twisted комьюнити ссылается на контекст как на объект original. Таким образом можно также дать аргументу любое подходящее имя, например file:

>>> class FileSize(object):
...
...      zope.interface.implements(ISize)
...      __used_for__ = IFile
...
...      def __init__(self, context):
...          self.context = context
...
...      def getSize(self):
...          return len(self.context.body)
...

Теперь когда мы написали наш адаптер мы должны зарегистрировать его в реестре адаптеров, что бы его можно было запросить когда он понадобится. Здесь нет какого-либо глобального реестра адаптеров, таким образом мы должны самостоятельно создать для нашего примера реестр:

>>> from zope.interface.adapter import AdapterRegistry
>>> registry = AdapterRegistry()

Реестр содержит отображение того, что адаптер реализует на основе другого интерфейса который предоставляет объект. Поэтому дальше мы регистрируем адаптер который адаптирует интерфейс IFile к интерфейсу ISize. Первый аргумент к методу register() реестра - это список адаптируемых интерфейсов. В нашем случае мы имеем только один адаптируемый интерфейс - IFile. Список интерфейсов имеет смысл для использования концепции мульти-адаптеров, которые требуют нескольких оригинальных объектов для адаптации к новому интерфейсу. В этой ситуации конструктор адаптера будет требовать новый аргумент для каждого оригинального интерфейса.

Второй аргумент метода register() - это интерфейс который предоставляет адаптер, в нашем случае ISize. Третий аргумент - имя адаптера. Сейчас нам не важно имя адаптера и мы передаем его как пустую строку. Обычно имена полезны если используются адаптеры для одинакового набора интерфейсов, но в различных ситуациях. Последний аргумент - это класс адаптера:

>>> registry.register([IFile], ISize, '', FileSize)

Теперь мы можем использовать реестр для запроса адаптера:

>>> registry.lookup1(IFile, ISize, '')
<class '__main__.FileSize'>

Попробуем более практичный пример. Создадим экземпляр File и создадим адаптер использующий запрос реестра. Затем мы увидим возвращает ли адаптер корректный размер при вызове getSize():

>>> file = File()
>>> size = registry.lookup1(IFile, ISize, '')(file)
>>> size.getSize()
7

На самом деле это не очень практично, т.к. нам нужно самим передавать все аргументы методу запроса. Существует некоторый синтаксический леденец который позволяет нам получить экземпляр адаптера просто вызвав ISize(file). Что бы использовать эту функциональность нам понадобится добавить наш реестр к списку adapter_hooks, который находится в модуле с адаптерами. Этот список хранит коллекцию вызываемых объектов которые вызываются автоматически когда вызывается IFoo(obj); их предназначение - найти адаптеры которые реализуют интерфейс для определенного экземпляра контекста.

Необходимо реализовать свою собственную функцию для поиска адаптера; данный пример описывает одну из простейших функций для использования с реестром, но также можно реализовать поисковые функции которые, например, используют кэширование, или адаптеры сохраняемые в базе. Функция поиска должна принимать желаемый на выходе интерфейс (в нашем случае ISize) как первый аргумент и контекст для адаптации (file) как второй. Функция должна вернуть адаптер, т.е. экземпляр FileSize:

>>> def hook(provided, object):
...     adapter = registry.lookup1(zope.interface.providedBy(object),
...                                provided, '')
...     return adapter(object)
...

Теперь мы просто добавляем нашу функцию к списку adapter_hooks:

>>> from zope.interface.interface import adapter_hooks
>>> adapter_hooks.append(hook)

Как только функция зарегистрирована мы можем использовать желаемый синтаксис:

>>> size = ISize(file)
>>> size.getSize()
7

После нам нужно прибраться за собой, что бы другие получили чистый список adaper_hooks после нас:

>>> adapter_hooks.remove(hook)

Это все. Здесь намеренно отложена дискуссия об именованных и мульти-адаптерах, т.к. данный текст рассчитан как практическое и простое введение в интерфейсы и адаптеры Zope 3. Для более подробной информации имеет смысл прочитать adapter.txt из пакета zope.interface, что бы получить более формальное, справочное и полное трактование пакета. Внимание: многие жаловались, что adapter.txt приводит их мозг к расплавленному состоянию!