Введение в компоненты.txt
2008-01-11 19:04Слово "запрос" здесь появилось не случайно, именно так называется обращение к системе в Zope. Недостаток вышеописанного случая именованных утилит - это некоторая "глобальность" выбора, как минимум в пределах сайта (в Zope) или целого приложения (как в нашем примере). ...
Введение в компоненты :
Одно из основных достоинств компонентной модели состоит в том, что можно строить взаимодействие между объектами (до раздела определений будем называть их так), даже не вдаваясь в подробности их реализации: достаточно иметь описание их внешних функций. Но главное достоинство компонентной модели все-таки не в этом, а в том, что она предлагает и поддерживает сценарии поиска подходящих для взаимодействия объектов. Таким образом, программирование в рамках компонентной модели сводится не к детерминированному определлению конкретных объектов для взаимодейстствия, а к декларации того, объекты какого сорта могли бы быть использованы для этого вззаимодействия.
Цель этой статьи - учебное руководство по компонентной модели, поэтому здесь даются по возможности точные определения терминологии и простые примеры ее использования. Подробности работы реестров и других упомянутых компонент хорошо описаны в API работы с реестрами.txt.
Краткие определения:
Чтобы избежать в дальнейшем путаницы в терминологии, дадим краткое определение составляющих компонентной модели и будем ему следовать
- Объект
- Некоторая сущность, обладающая состоянием и поведением;
- Интерфейс
- Декларация предпочтительного способа использования объекта, конкретизирующая то, что данный объект обладает определенным поведением и, в силу этого, может вступать во взаимодействие с другими объектами;
- Схема (Схема Интерфейса)
- Декларация атрибутов и методов, наличие которых ожидается от объекта, для которого декларировано наличие интерфейса. Схема не является обязательной частью интерфейса, но является его удобным расширением;
- Компонент
- Некоторая сущность, предоставляющая предопределенный интерфейс и способная взаимодействовать посредством его с другими компонентами. Фактически, компонент это тот же самый объект, но когда говорят о компоненте, рассматривают только его внешнее описание, данное через декларацию интерфейсов;
- Адаптер
- Программная реализация предоставления одного интерфейса через обращение к другому, предоставляемому компонентом. Сущестуют мультиадаптеры, которые принимают на вход несколько компонент с определенными интерфейсами. Адаптер, в общем случае, не обладает собственным состоянием или поведением: это только транслятор интерфейсов входных компонент в выходной интерфейс.
- Реестр адаптеров
- Реестр, позволяющий зарегистрировать адаптер, и по данному преобразованию одного интерфейса в другой подобрать наиболее оптимальный адаптер. При регистрации адаптер может получать имя, тогда в реестре могут существовать несколько адаптеров, осуществяющих заявленное преобразование, но под разными именами;
- Реестр утилит
- Реестр, позволяющий зарегистрировать компонент, и по затребованному интерфейсу] и имени найти наиболее подходящий. Такой компонент называется Утилита;
- Утилита
- Зарегистрированный в реестре утилит под определенным интерфейсом и именем компонент, такой компонент обычно используется как некий поставщик услуг или сервис;
В компонентной модели существуют другие, пропущенные пока для простоты понятия, которые будут введены позднее.
Принципы использования компонент ;
Компонентно-ориентиованная система похожа на хаос, в котором беспорядочно плавают компоненты. Правда, компоненты хорошо-определены (снабжены декларациями интерфейсов) и внесены в несколько реестров, в частности, реестр адаптеров и реестр утилит. Реестры позволяют превратить хаос в порядок - но для этого должен появится некоторый принцип наведения порядка, такой принцип можно свести к введению двух сущностей: поставщика и потребителя услуги.
Порядок начинается с потребителя услуги, который ищет под себя поставщиков, которые, для оказания услуги, сами становятся потребителями каких-то услуг следующего уровня, и, таким образом, в момент появления в системе запроса потребителя, из хаоса возникает упорядоченная иерархия компонент, способная дать ответ на запрос.
Слово "запрос" здесь появилось не случайно, именно так называется обращение к системе в Zope. Пока описание доберется до такого сложного случая как описан выше, напишется немало строк, а пока начнем с деталей.
Обращение к интерфейсу :
Простейший случай взаимодействия компонент, это когда одна компонента получила (каким-то способом) другую и пытается вступить с ней во взаимодействие. Но один компонент может взаимодействовать с другим только при наличии у него интерфейса, например IA. Вот определение интерфейса, для наглядности:
class IA(Interface) :
pass
Введем компонент предоставляющий этот интерфейс:
class A(object) :
implements(IA)
И не предоставляющий :
class B(object) :
implements(IB)
А вот код взаимодействия, в котором компонент складывает другие компоненты в корзину:
def add(self,comp) :
if IA.providedBy(comp) :
self.basket.append(comp)
получая их, например, в цикле:
for comp in [A(),B(),A(),A()] :
ob.add(comp)
В данном случае проверяется то, что компонент предоставляет интерфейс IA, и если это так - компонент в корзине. В этом случае интерфейс используется как своебразная метка, откуда даже произошло название, интерфейс-маркер. На самом деле, функция метки для интерфейса - основная, просто выглядит это не так явно, поэтому понятие интерфейс-маркер (или интерфейс-без-схемы) не включено в список определений.
Усложним взаимодействие. Введем в интерфейс схему, состояющую из пары переменных и функции:
class IB(Interface) :
a = TextLine()
b = Bool()
def bind(parent) :
pass
Обратите внимание, что метод bind() не содержит аргумент self. Это концептуальное свойство интерфейса, так как интерфейс описывает внешнее поведение компонента, а по внешнему поведению понять, что метод содержит аргумент self нельзя (в идеале).
Воспользуемся этим интерфейсом для взаимодействия. Потребитель будет складывать в корзину компоненты с интерфейсом IB, если у них установлен флаг b, а в противном случае писать в a "Отказано":
def add(self,comp) :
try :
ib = IB(comp)
except TypeError :
pass
else :
if ib.b :
self.basket.append(comp)
ib.bind(self)
else :
ib.a = "Отказано"
Другой вариант, без исключений:
def add(self,comp) :
ib = IB(comp,None)
if ib :
if ib.b :
self.basket.append(comp)
ib.bind(self)
else :
ib.a = "Отказано"
Операция IB(comp) называется приведение к интерфейсу. Обратите внимание, в корзину добавляется непосредственно компонент comp, а все операции проводятся через интерфейс к которому его привели.
На самом деле (к сожалению) интерфейсы в компонентной модели Zope не енфорсятся, т.е. запись вида:
IB(comp).qq = 1
вполне может сработать, хотя интерфейс IB аттрибут qq не декларирует. Сработает и обращение к объекту мимо интерфейса:
if IB(comp,None) :
if comp.b :
......
По крайней мере до тех пор, пока мы действуем непосредственно с объектом, предоставляющим интерфейс, а не адаптированном к нему, но в силу возможной адаптации, да и просто потому что "так принято", рекомендуем считать последние пару примеров грязными хаками и никогда так не делать. Старайтесь следовать трем простым правилам:
- Если нужные вам атрибуты и функции декларированы каким-то интерфейсом, обращайтесь к ним только через этот интерфейс;
- Если нужные вам атрибуты и функции не декларированы никаким интерфейсом не обращайтесь к ним вообще;
- Постарайтесь нигде не передать (случайно) вместо самого объекта, интерфейс, к которому его привели.
Следование этим правилам в Zope просто избавит вас от ряда ошибок и заставит писать более архитектурно грамотный код, хотя в некоторых других системах нарушение этих правил привело бы к немедленной ошибке.
Адаптация к интерфейсу :
Когда объект сам не предоставляет интерфейс, можно написать адаптер, который адаптирует интерфейс, имеющийся у объекта к требуемому. Адаптер - это некая сущность, сильно напоминающая прокси. Основное видимое отличие от прокси в том, что для нее декларированы интерфейсы аргументов конструктора адаптера и интерфейс, предоставляемый адаптером. На самом деле есть еще одно отличие: адаптер может вообще не быть прокси. Этом может быть что угодно - главное, наличие описания.
Пусть A2B - адаптер IA к IB :
class A2B(object) :
implements(IB)
adapts(IA)
allparents = []
def __init__(self,context) :
self.context = context
def bind(self,parent) :
self.allparents.append(parent)
def setA(self,val)
print val
def getA(self,val)
return ""
a = property(getA,setA)
b = True
Вообще, такой адаптер надо еще зарегистрировать в реестре, а реестр заерегистрировать в списке реестров, но об этом лучше почитать в "Использовании реестра адаптеров" из исходников Zope3
Это очень простой адаптер, который на самом деле ничего не делает, только позволяет отработать правильно написанному коду add для объекта с интерфейсом IB. Приведем его еще раз:
def add(self,comp) :
ib = IB(comp,None)
if ib :
if ib.b :
self.basket.append(comp)
ib.bind(self)
else :
ib.a = "Отказано"
Легко заметить, код совершенно не изменился, но после введения в "хаос" нового компонента, "хаос" приобрел новые свойства, в корзину теперь складываются компоненты с интерфейсом IA и с ними выполняются те же действия, хотя, вероятно, на уровне _структуры_ совсем другим способом.
Хороший практический пример такого применения адаптеров описан в статье HOWTO Use mtime.txt
Адаптер как алгоритм :
Адаптер может выступать не только как способ "перевода" интерфейса, но и как алгоритм работы с объектом, имеющим некий интерфейс. Работы - т.е. изменения и переделки. Как пример возьмем адаптер, исправляющий ошибки в текстовых атрибутах объектов. Этот алгоритм предлагает следующий интерфейс управления:
class ISpell(Interface) :
def next() :
""" Получить ошибочное слово """
def change(word) :
""' Заменить ошибочное слово другим """
Предположим, существует библиотека, поставляющая класс Spell, способный проверить одну текстовую строку. Его устройство нам не важно, а внешний интерфейс ясен из дальнейшего:
class IC(Interface) :
title = TextLine()
abstract = Text()
Опишем алгоритм адаптера:
class SpellBase(object) :
implements(ISpell)
attributes = []
iface = lambda x : x
def __init__(self,context) :
self.context = context
def _spell(self) :
for attribute in self.attributes :
text = getattr(self.iface(self.context),attribute)
self.spell = Spell(text)
res = self.spell.next()
if res :
yield res
setattr(self.iface(self.context),attribute,self.spell.get())
def next(self) :
if self.gen is None :
self.gen = self._spell()
try :
return self.gen.next()
except StopIteration :
return None
def change(self,word) :
self.spell.change(word)
Теперь сделаем два адаптера для разных интерфейсов :
class C2Spell(SpellBase) :
adapts(IC)
iface = IC
attributes = ['title','abstract']
class B2Spell(SpellBase) :
adapts(IB)
iface = IB
attributes = ['a']
Все, делая вот такие вызовы:
ISpell(comp)
Можно получать интерфейсы алгоритма исправления ошибок независимо от того, компонент с каким интерфейсом (и с какими атрибутами) нужно исправить.
Хороший практический пример такого применения адаптеров описан в статье HOWTO use adding and namechooser.txt
Интерфейс как способ выбора алгоритма :
Описанный выше пример того, как один и тот же (по сути) алгоритм приспосабливался для разных компонент, можно повернуть и другим способом: интерфейс компонента может позволять выбрать разные алгоритмы с одним и тем же интерфейсом для его обработки.
Пусть интерфейс алгоритма прост как никогда:
class ICall(Interface) :
def __call__() :
""" Преобразует объект """
Это некий совершенно абстрактный алгоритм что-то делающий с компонентом. У нас в наличии три компонента: для интерфейсов IA, IB, IC. Пусть:
- IA - печатаются;
- IB - кладутся в корзину;
- IC - все неправильные слова заменяются завездочками;
Адаптер A2Call:
class A2Call(object) :
adapts(IA)
implements(ICall)
def __init__(self,context) :
self.context = context
def __call__(self) :
print self.context
Адаптер B2Call:
class B2Call(object) :
adapts(IB)
implements(ICall)
basket = []
def __init__(self,context) :
self.context = context
def __call__(self) :
self.basket.append(self.context)
Адаптер C2Call:
class C2Call(object) :
adapts(IC)
implements(ICall)
basket = []
def __init__(self,context) :
self.context = context
def __call__(self) :
spell=ISpell(self)
while spell.next() :
spell.change("*****")
Замечательно. Берем кортеж объектов и с каждым делаем действие, определяемое его интерфейсом:
for item in (A(), B(), C()) :
ICall(item)()
Такой способ использования интерфейсов используется, например, для выбора алгоритма хранения аннотаций, описаный в статье AnnotationKeeper.txt.
Утилита - поиск поставщика услуг :
Возможно, по примерам выше вы уже почувствовали как порядок из хаоса рождается силами реестров - хотя реестры не использовались напрямую в наших примерах, но они бы не заработали, если бы все адаптеры и интерфейсы не регистрировались в реестрах.
Пример этой главы без упоминания реестров не обойдется, так как речь пойдет об утилитах. Утилита это, в сущности, компонент который был зарегисрирован как Утилита.
Если вы обратили внимание, во всех вышеприведенных адаптерах, в качестве корзины использовался список в переменной класса. На самом деле, это конечно не подходящий способ, и в реальной жизни должен быть какой-то сервис корзины, например вот такой:
class IBasket(Interface) :
def append(comp) :
""" добавить компонент в корзину """
Не будем заострять внимание на коде такого компонента, просто назовем класс Basket (этого достаточно для дальнейшего изложения).
Зарегистрируем утилиту :
zope.component.provideUtility(Basket(),IBasket,'basket')
И теперь любую компоненту, использующую корзину, можно переписать под использование утилиты:
class B2Call(object) :
adapts(IB)
implements(ICall)
basket = []
def __init__(self,context) :
self.context = context
def __call__(self) :
basket = zope.component.queryUtility(IBasket,'basket')
basket.append(self.context)
Жизнь несравненно сложнее этого простого случая, тем более что и реестр утилит обладает несравненно большими возможностями - например, можно найти все утилиты с данным интерфейсом независимо от имени, и предоставить гипотетическому потребителю услуг, введенному в начале статьи, возможность выбрать конкретную утилиту. Учитывая то, что утилиты может объединять только одинаковый интерфейс (а внутренняя логика быть разной) так и вовсе легко организовать что-то типа: "А что моя зайка хотела бы сделать сегодня?". Подробнее о возможностях реестра утилит лучше посмотреть неизвестно где
Именованные адаптеры;
Недостаток вышеописанного случая именованных утилит - это некоторая "глобальность" выбора, как минимум в пределах сайта (в Zope) или целого приложения (как в нашем примере). Однако выбор можно делать и локально, если воспользоваться именованными адаптерами. Да. Адаптер может иметь имя. Может быть зарегистрировано несколько одинаковых адаптеров под разными именами. И тогда для данного контекстного объекта можно будет организовать выбор потребителем одной из множества услуг. На самом деле, такое свойство реестра адаптеров используется повсеместно, подробнее об этом лучше посмотреть неизвестно где, а наша статья на этом заканчивается.
Заключение:
Главная цель этой статьи - продемонстрировать жизнь компонент-ориентированной системы. Жизнь, в которой компоненты находять в состоянии хаоса до появления запроса, хаоса, который под запрос упорядочивается и делает возможным выполнить заявленную этим запросом задачу. Были продемострированы разные возможности такого упорядочивания, и, вероятно, эта статья достигла своей цели - введения в компоненты.



