2008-01-11

Введение в компоненты.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 :
                    ......

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

  1. Если нужные вам атрибуты и функции декларированы каким-то интерфейсом, обращайтесь к ним только через этот интерфейс;
  2. Если нужные вам атрибуты и функции не декларированы никаким интерфейсом не обращайтесь к ним вообще;
  3. Постарайтесь нигде не передать (случайно) вместо самого объекта, интерфейс, к которому его привели.

Следование этим правилам в 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) или целого приложения (как в нашем примере). Однако выбор можно делать и локально, если воспользоваться именованными адаптерами. Да. Адаптер может иметь имя. Может быть зарегистрировано несколько одинаковых адаптеров под разными именами. И тогда для данного контекстного объекта можно будет организовать выбор потребителем одной из множества услуг. На самом деле, такое свойство реестра адаптеров используется повсеместно, подробнее об этом лучше посмотреть неизвестно где, а наша статья на этом заканчивается.

Заключение:

Главная цель этой статьи - продемонстрировать жизнь компонент-ориентированной системы. Жизнь, в которой компоненты находять в состоянии хаоса до появления запроса, хаоса, который под запрос упорядочивается и делает возможным выполнить заявленную этим запросом задачу. Были продемострированы разные возможности такого упорядочивания, и, вероятно, эта статья достигла своей цели - введения в компоненты.

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