Блоґ одного кібера

Історія хвороби контуженого інформаційним вибухом

Трактат про Zope Component Architecture

with 2 comments

Zope – це дуже-дуже старий фреймворк (історія почалася ще з 1995), але деякі його частини, а іноді і сам він використовуються в різних системах і досі. Наприклад Twisted використовує інтерфейси Zope (їх свідчення). Біда з ним в тому, що якась пристойна інструкція в інтернеті не доступна. Може вона й була там колись, але на жаль посилання не завжди працюють, а з такою старовиною як Zope – дуже часто ведуть в тупик 404. Своїм трактатом постараюсь цю біду хоч трохи виправити.

Ах, ще цікавий факт, ZCA з’явилась в Zope 3 (BlueBream), при спробі переписати все “правильно”, ігноруючи зворотню сумісність. Таким чином ситуація трохи нагадує ситуацію з Python 3. Тільки Python 3 з’явився в 2008, і здається таки має деякі шанси вижити, а от Zope 3 (BlueBream) – в 2004, і від нього відросли інші фреймворки, наприклад BFG, який об’єднався з Pylons і став Pyramid. Але цю всі історію дуже цікаво розповідає Пол Еверітт, один з тих хто все це почав. Він пояснить вам що Zope ще не вмерла, вона просто спить:

А тут мова піде лише про таку частинку Zope, як ZCA. І ми будемо писати код. З нуля!

Термінологія

Але почнемо з теорії, аби розуміти що збираємось писати.

  • Zope – Z Object Publishing Environment – фреймворк і веб-сервер які намагались публікувати у вебі об’єкти, ще в ті часи коли Apache віддавав статичний HTML і ганяв CGI скрипти. Шлях в URL в ньому відповідає не шляху до файлу зі скриптом, а шляху в ієрархії об’єктів. Кажуть то був прорив.
  • Компонент (Component) – якийсь код на Python, зазвичай клас, що реалізує інтерфейс. Компоненти поділяються на адаптери та утиліти (див. нижче).
  • Інтерфейс (Interface) – спеціальний клас, єдиною метою якого є опис функціоналу що реалізується іншим класом чи модулем. Вони реєструються в реєстрі компонент, і до цього реєстру можна робити запити “Дайте мені будь-ласка компоненти що реалізують оцей інтерфейс”. Класи можуть декларувати що вони реалізують (implement) певний інтерфейс. Екземпляри цих класів тоді надають (provide) цей же інтерфейс.
  • Сервіс (Service) – це об’єкт що працює з компонентами. Думайте про компоненти як про “речі”, а про сервіс – як про частину коду що бере ці “речі” і робить з ними щось корисне.
  • Реєстр компонентів (Component registry, також Site Manager) – база даних компонентів та інтерфейсів, сервіс за означенням вище. Якщо ви реєструєте компоненти в реєстрі, вони стають доступними для інших користувачів того ж реєстру. Існує глобальний реєстр компонентів що створюється автоматично, і можна використати ZODB як локальний реєстр.
  • ZODB – ZOpe Object Database – база даних об’єктів Zope. Використовувати її з компонентами не обов’язково, тому про неї якось іншим разом.
  • Глобальний реєстр компонентів (global component registry) – сервіс що автоматично створюється коли ваша програма що містить компоненти Zope запускається і наповнюється компонентами що описані в конфігураційних файлах ZCML. До глобального реєстру можна отримати доступ з будь-якого місця програми. Коли програма завершує роботу всі зміни внесені до глобального реєстру зникають і при наступному запуску він заново завантажується з ZCML.
  • Локальні компоненти (Local components) – це компоненти що не створюються в глобальному реєстрі компонентів. Зазвичай їх зберігають в ZODB, що також означає що вони персистентні, тобто можуть пережити перезапуск програми.
  • ZCML (вимовляється “зе-кемел”, Zope Configuration Markup Language) – XML файли, що описують компоненти які потрібно зареєструвати в глобальному реєстрі.
  • Адаптери (Adapter)- це класи що дозволяють використовувати з об’єктом нові інтерфейси. Клас адаптера містить виклик adapts() який вказує інтерфейс що адаптує адаптер, що означає що ви можете передати будь-який об’єкт, що реалізує цей інтерфейс, в метод __init__ класу адаптера, наприклад a = myAdapter(myobject). Тепер MyAdapter бере на себе обов’язок фронт-енда для myobject, таким чином, що будь-який інтерфейс реалізований за допомогою myAdapter, буде правильно працювати з myobject. В певному сенсі адаптер “огортає” адаптований об’єкт.
  • Фабрика (Factory) – це щось (якщо конкретніше, не щось, а утиліта, визначення якої буде нижче), яка створює компоненти та об’єкти. Це як більш високорівнева версія конструктора. Фабрика має бути об’єктом який можна викликати, і повинна реалізувати інтерфейс IFactory. Фабрики надають зручні засоби для доступу до конструкторів компонентів через реєстр компонентів, а не прямим викликом конструктора. Важливо зауважити що ZCM автоматично створює фабрики, коли ви реєструєте компоненти. В багатьох випадках це означає що ви не матимете справи зі створенням фабрики напряму.
  • Утиліта (Utility) – об’єкт який ви хочете додати до реєстру компонентів. Вона може бути будь-яким об’єктом мови Python, який може знадобитись іншій частині програми. Утиліта повинна реалізовувати принаймі один інтерфейс.

Я знаю що ви з цього словника майже нічого не зрозуміли. Не бійтесь, я теж. Далі ми розберемо це на практиці, і ситуація проясниться.

Інсталяція

Є два пакети що стосуються ZCA:

  • zope.interface використовується для опису інтерфейсів компонентів.
  • zope.component займається реєстрацією та отриманням компонентів.

Щоб їх встановити, досить написати pip install zope.component, він має в залежностях zope.interface і zope.event.

Приклад коду без ZCA

Візьмемо наприклад простенький калькулятор інфіксного виразу на одну дію:

operations = { # словник можливих операцій
    '+': lambda a, b: a + b,
    '-': lambda a, b: a - b,
}

def calc(expr):
    # "парсимо" наш вираз. Він складається з двох аргументів розділених операцією.
    a, op, b = expr.split()
    #  # обчислюємо обидва аргументи, передаємо їх в операцію.
    return operations[op](eval(a), eval(b))

assert calc('2 + 2') == 4
assert calc('2 - 2') == 0
assert calc('2 - 0.2') == 1.8
# Тут буде KeyError, тому що операцію "*" ми не записали в словник.
assert calc('2 * 2') == 4

Це дуже проста програма, яка розв’язує дуже просту задачу, і саме так потрібно писати програми які розв’язують прості задачі. В даному випадку словник вирішує проблему того, що ми наперед не знаємо яку функцію доведеться викликати (і дозволяє уникнути довжелезного списку if-ів, всередині функції calc). Ще один плюс в тому, що якщо ми захочемо розширити наш калькулятор, нам досить буде імпортувати цей модуль (назвемо його calc1.py), і додати нову функцію наступним чином:

calc1.operations['*'] = lambda a, b: a * b

Ні код функції calc, ні код всього модуля calc1 змінювати не доведеться, а функціональність розшириться.

ZCA розв’язує цю саму задачу з’єднання компонентів, які наперед невідомі, і невідомо які будуть потрібні, дещо складнішим чином. І для такої простої задачі як наш калькулятор буде здаватись надмірним. Але його все ж використовують там, де потрібно організувати взаємодію багатьох різних компонентів, написаних різними людьми, які знаходяться в різних модулях, і можуть додаватись та видалятись залежно від конфігурації системи. Тут ми все ж розв’язуватимемо надто просту для ZCA задачу, просто тому щоб не відволікатись на складність самої задачі.

Інтерфейси

Почнемо організовувати наш калькулятор компонентною архітектурою з того, що всі наші операції мали б бути компонентами і мати спільний інтерфейс. Інтерфейси – це класи що наслідуються від класу Interface і за домовленістю, назва починається з префіксу I. Ось так:

from zope.interface import Interface

class IOperation(Interface):
    def __call__(a, b):
        ''' Performs operation on two operands '''

Що робить цей код? По суті – нічого. Він просто є чимось на зразок документації, просто описаної в коді. Окрім методів, можна описувати атрибути, наступним чином:

from zope.interface import Attribute

class IPerson(Interface):
    name = Attribute('Name of the person')

Attribute – об’єкт який нічого корисного не робить. Окрім того що містить в собі стрічку документації, яка передається йому в конструкторі. В нашому прикладі інтерфейси з атрибутами не потрібні, тому просто знайте що їх описують так.

Тепер ми можемо описати кілька компонентів що реалізують цей інтерфейс:

from zope.interface import implements

class Plus(object):
    implements(IOperation)
    def __call__(self, a, b):
        return a + b

class Minus(object):
    implements(IOperation)
    def __call__(self, a, b):
        return a - b

Для оголошення того що клас реалізує інтерфейс, використовують функцію implements. Так, в Python функції які викликаються під час створення класу, можуть впливати на клас що створюється, і їх називають class advisor-ами.

Окрім виклику implements всередині класу, можна вказати що клас реалізує інтерфейс альтернативним чином, ззовні:

from zope.interface import classImplements
classImplements(Host, IHost)

Яким чином те що клас реалізує інтерфейс змінює поведінку класу? Ніяким. Інтерфейс це лише документація. Хоча, не лише. 😉 Це хитра документація, бо відповідність об’єктів їй можна перевірити автоматично:

from zope.interface.verify import verifyObject, verifyClass

verifyClass(IOperation, Plus) # дасть true
verifyObject(IOperation, Plus()) # дасть true

Якщо ж ми наприклад викличемо verifyClass чи verifyObject для об’єкта для якого не вказано що він реалізує інтерфейс, отримаємо такий виняток:

zope.interface.exceptions.DoesNotImplement: An object
  does not implement interface <InterfaceClass __main__.IOperation>

Якщо забудемо якийсь параметр – отримаємо:

zope.interface.exceptions.BrokenMethodImplementation: 
  The implementation of __call__ violates its contract
  because implementation doesn't allow enough arguments.

Окрім перевірок атрибутів та методів, інтерфейси мають ще одну корисну можливість. Вони можуть описувати інваріанти – умови, виконання яких вимагається для об’єктів що надають інтерфейс. Це щось на зразок прив’язки набору тестів до інтерфейсу:

from zope.interface import invariant

def binary_float_operation(obj):
    res = obj(2, 2.1)
    assert isinstance(res, float), 'Operation should return float'

class IOperation(Interface):
    def __call__(a, b):
        ''' performs operation on two operands '''
    invariant(binary_float_operation)

Тепер, щоб перевірити відповідність якогось об’єкта інваріантам інтерфейсу, ми викликаємо метод інтерфейсу, передавши йому об’єкт:

IOperation.validateInvariants(plus_operation)

Якщо виклик plus_operation(2, 2.1) поверне не float, спрацює assert що ми описали:

  File "calc.py", line 8, in binary_float_operation
    assert isinstance(res, float), 'Operation should return float'
AssertionError: Operation should return float

Також, інтерфейс не обов’язково повинен тягти на собі опис всіх атрибутів, методів та інваріанти. Інтерфейс може просто позначати клас, як якась мітка. Наприклад позначити те що наша операція належить до спецоперацій:

class ISpecialOperation(Interface):
    """Special operation"""

Утиліти

Але давайте за всіма тими інтерфейсами та інваріантами не забувати нащо нам все це треба. Ми хотіли обчислювати простенькі вирази. Для цього нам потрібно вміти отримувати потрібний об’єкт що надає IOperation. Це можна зробити за допомогою глобального реєстру компонентів, або сайт менеджера. Отримати його можна викликавши функцію:

from zope.component import getGlobalSiteManager
gsm = getGlobalSiteManager()

Єдине його призначення – зберігати компоненти, що ми й зробимо помістивши туди кілька утиліт:

gsm.registerUtility(Plus(), IOperation, '+')
gsm.registerUtility(Minus(), IOperation, '-')

Перший аргумент методу registerUtility – об’єкт що надає інтерфейс, другий – той інтерфейс який він надає, третій – необов’язкове ім’я. Якщо інтерфейс у нас реалізує лише одна утиліта, то ім’я давати необов’язково, ми все одно цю утиліту знайдемо.

Далі ми можемо отримувати утиліти, викликаючи функцію getUtility, або queryUtility. getUtility якщо не знайде нічого, згенерує ComponentLookupError, а queryUtility – поверне значення свого аргументу default (за замовчуванням None). І таким чином, ми можемо написати наш калькулятор не використовуючи словника:

from zope.component import getUtility
def calc(expr):
    a, op, b = expr.split()
    res = getUtility(IOperation, op)(eval(a), eval(b))
    return res

Тепер, якщо знову виконати тести

assert calc('2 + 2') == 4
assert calc('2 - 2') == 0
assert calc('2 - 0.2') == 1.8
assert calc('2 * 2') == 4

То отримаємо те саме що мали зі словником, але код буде втричі довшим, і замість KeyError буде ComponentLookupError. Залишається питання – а воно нам було треба? 🙂 Далі буде видно.

ZCML

По перше, ми всю цю реєстрацію утиліт за іменами можемо винести в окремий файл, назвавши його наприклад configure.zcml.

<configure xmlns="http://namespaces.zope.org/zope">

<include package="zope.component" file="meta.zcml" />
<utility
    component="calc.plus"
    provides="calc.IOperation"
    name="+"
    />

<utility
    component="calc.minus"
    provides="calc.IOperation"
    name="-"
    />

</configure>

Ми бачимо в ньому реєстрацію наших двох утиліт, і ще один елемент include перед ними. Цей елемент важливий, бо якщо його не включити, то Zope буде скаржитись що воно взагалі без поняття що таке утиліта:

zope.configuration.xmlconfig.ZopeXMLConfigurationError: File "configure.zcml", line 3.0
    ConfigurationError: ('Unknown directive', u'http://namespaces.zope.org/zope', u'utility')

Дозволю собі трохи нагадати нам як виглядає наш calc.py без всього зайвого (перевірок інваріантів, gsm.registerUtility і т.п.):

from zope.interface import Interface, implements
from zope.component import getUtility

class IOperation(Interface):
    def __call__(a, b):
        ''' performs operation on two operands '''

class Plus(object):
    implements(IOperation)
    def __call__(self, a, b):
        return a + b
plus = Plus()

class Minus(object):
    implements(IOperation)
    def __call__(self, a, b):
        return a - b
minus = Minus()

def eval(expr):
    a, op, b = expr.split()
    return getUtility(IOperation, op)(float(a), float(b))

Як тепер нам зареєструвати всі ці утиліти? Доведеться встановити ще один модуль – zope.configuration.

Далі, з файлу main.py виконати наступне:

from zope.configuration import xmlconfig
 
from calc import eval
 
def main():
    xmlconfig.file('configure.zcml')
 
    assert eval('2 + 2') == 4
    assert eval('2 - 2') == 0
 
if __name__ == '__main__':
    main()

Тут найважливіший рядок – xmlconfig.file('configure.zcml'), тому що без нього eval не зможе знайти потрібну утиліту і викине ComponentLookupError.

Чому ми не могли зробити це прямо в calc.py? Я щось три години намагався, думав що проблема в модулі xmlconfig, пробував різні методи з нього, пробував всякі контексти (досі не до кінця розумію що то таке), а воно все казало ComponentLookupError. Тобто не хотіло реєструвати мої утиліти. Як виявилось, при завантаженні configure.zcml згадані в ньому компоненти імпортуються, і саме тому воно не хоче імпортувати ZCML з того самого файлу який той ZCML конфігурує.

Також, замість атрибуту component в елементі utility ми можемо написати factory і вказати там конструктор (фабрику) для наших утиліт:

<utility
    factory="calc.Plus"
    provides="calc.IOperation"
    name="+"
    />

Після цього з calc.py можна буде видалити рядочок:

plus = Plus()

Бо zope сама створить екземпляр утиліти коли парситиме ZCML.

Фабрика

Ви помітили слово фабрика в останньому куску ZCML? Якщо ви уважно читаєте оцю мою писанину, то можливо пам’ятаєте, що про фабрики згадувалось в словничку на початку. В zope існують ще такі штуки як zope.component.factory.Factory та zope.component.interfaces.IFactory. І я зеленого поняття не маю, що воно таке, хоча з Zope працюю майже рік, і вже майже 10 годин вивчаю документацію і пишу оцей трактат (що не означає що я не вивчав документацію до того). Єдине що чув – що їх не варто плутати з атрибутом елемента utility, бо то зовсім різні речі. Але яким би твердим не був граніт науки, спробуймо прогризтись далі.

Ми забули сказати про утиліти те, що коли утиліта реєструється, її екземпляр створюється (чи береться готовий, залежно від того як реєструється), і далі на всі виклики getUtility повертається один і той самий. На відміну від цього, фабрика створює багато різних екземплярів. Спробуємо написати фабрику що створює випадкові цифри? Бо це ніби-то має бути нескладно:

import random

def get_digit():
    return random.randint(1, 9)

Так просто? Ні, це ще не все. Справжня фабрика реалізує інтерфейс IFactory пам’ятаєте? І тому її треба створити власним конструктором:

from zope.component.factory import Factory
factory = Factory(get_digit, 'random_digit', 'Gives a random digit')

Також, в списку термінів, ми писали, що фабрика – це така утиліта. Значить її можна зареєструвати:

from zope.component import getGlobalSiteManager
gsm = getGlobalSiteManager()

from zope.component.interfaces import IFactory
gsm.registerUtility(factory, IFactory, 'random_digit')

І тепер можна діставати її з глобального реєстру компонентів і викликати, аби ці компоненти створювались:

from zope.component import getUtility
print getUtility(IFactory, 'random_digit')() # creates digit

Для таких запитів на запуск фабрики, zope має функцію-скорочення:

from zope.component import createObject
print createObject('random_digit') # also creates a digit

createObject завжди шукає утиліти що надають інтерфейс IFactory, бо коли ми сховаємо в реєстрі щось інше, наприклад:

from zope.interface import Interface

class IInt(Interface):
    ''' Interface for integer numbers '''

gsm.registerUtility(2, IInt, 'two')

І спробуємо запустити це як фабрику

createObject('two')

отримаємо вже знайому нам помилку: zope.interface.interfaces.ComponentLookupError: (, 'two')

Тепер покажу ще одну цікаву штуку з фабриками і на тому закінчимо. Створимо наприклад функцію яка повертає поточний рік:

from datetime import datetime
def get_year():
    return datetime.now().year

Це, як і get_digit по-суті, фабрика цілих чисел. Давайте їх так і зареєструємо:

get_year.__bases__ = [] # hack to make Zope think this is actually a classes
get_digit.__bases__ = []

from zope.interface import classImplements
classImplements(get_year, IInt) # declare that our "constructor" implements an interface
classImplements(get_digit, IInt)

factory1 = Factory(get_digit, 'random_digit', 'Gives a random digit')
factory2 = Factory(get_year, 'year', 'Gives a current year')

gsm.registerUtility(factory1, IFactory, 'random_digit')
gsm.registerUtility(factory2, IFactory, 'year')

Тепер, якщо ми раптом захочемо створити об’єкт що надає IInt Zope може підказати нам які фабрики для цього можна обрати:

from zope.component import getFactoriesFor
print list(getFactoriesFor(IInt)) 

Я правда не знаю де таку функцію можна використати, але мені цікаво було про неї дізнатись. А тепер пора рухатись до наступного поняття.

Адаптер

Якщо ви вже думаєте що знаєте що таке адаптер – то так воно напевне і є. Тому що це всім відомий шаблон проектування, а якщо він вам невідомий, раджу прочитати мою статтю про нього і два інші подібні шаблони. Якщо коротко, адаптер – це об’єкт що містить в собі інший об’єкт, і надає до нього якийсь інший інтерфейс, тобто адаптує його. От наприклад мій комп’ютер хоче 20 вольт постійного струму, а в розетці 220B, 50Гц. Тому мій комп’ютер приєднується до неї через адаптер.

Ну ок, адаптери корисні, але нащо їх в архітектурі компонентів Zope? Ну, за допомогою інтерфейсів можна організувати перевірку типів, і щось що віддалено нагадує строгу типізацію (скоро ми побачимо як зробити перевірку типів вхідних даних), то за допомогою адаптерів – щось що нагададує слабку типізацію (неявне приведення типів до бажаних).

Нехай в нас з’явилась ще якась бібліотека з операціями, але вони мають трохи інший інтерфейс. Наприклад як у Scala:

class IScalaOperation(Interface):
    def apply(a, b):
        ''' performs operation on two operands '''

class Division(object):
    implements(IScalaOperation)
    def apply(self, x, y):
        return x / y

Так, у Scala символ “/” справді позначає об’єкт з методом apply тому що в Scala теж все – об’єкт. Це як метод __call__ в Python. Зареєструємо цю утиліту:

<utility
    factory="calc.Division"
    provides="calc.IScalaOperation"
    name="/"
    />

І тепер, нехай в нас є метод що приймає операцію і щось з нею робить, наприклад foldl (інша назва reduce). Як цьому методу вказати що на вхід він хоче саме IOperation? Перший спосіб – verifyObject(IOperation, op). Але є кращий спосіб:

def foldl(op, seq):
    op = IOperation(op) 
    acc = seq[0]
    for n in seq[1:]:
        acc = op(acc, n)
    return acc

Якщо протестувати цей код в main.py отакими умовами:

    assert foldl(getUtility(IOperation, '+'), range(11)) == 55
    assert foldl(getUtility(IScalaOperation, '/'), [64, 2, 2, 2, 2, 2]) == 2

То перше припущення виконається, тому що якщо інтерфейс викликати передавши йому об’єкт, то він поверне цей об’єкт, а друге поламається з отаким винятком:

  File "/home/bunyk/zca/calc.py", line 44, in foldl
    op = IOperation(op)
TypeError: ('Could not adapt', <calc.Division object at 0xb70f33ac>, <InterfaceClass calc.IOperation>)

Чим це краще за перевірку verifyObject, запитаєте ви? А тим, що інтерфейс намагався привести об’єкт у відповідність собі, але не зміг. Щоб він зміг таке робити, ми повинні описати відповідний адаптер:

from zope.component import adapts
class ScalaOperationAdapter(object):
    implements(IOperation)
    adapts(IScalaOperation)

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

    def __call__(self, a, b):
        return self.scala_op.apply(a, b)

І не забути його зареєструвати в ZCML:

<adapter
    factory="calc.ScalaOperationAdapter"
    provides="calc.IOperation"
    for="calc.IScalaOperation"
    />

Чи з коду:

    gsm.registerAdapter(ScalaOperationAdapter)

Інтерфейси які цей клас надає та адаптує можна не вказувати, бо вони вже й так описані в класі адвайзорами implements та adapts. Неіменовану утиліту можна реєструвати аналогічно, просто передавши об’єкт в registerUtility.

Після такої реєстрації адаптера, zope сам при виконанні op = IOperation(op) знайде адаптер і приведе інтерфейс IScalaOperation до IOperation.

Існують ще мульти-адаптери (щось на зразок фасаду), subscriber-и, handler-и і купу всіляких інших речей, але так глибоко я ще не копав. Розкажу вам натомість про інший механізм зменшення зв’язності.

Події

Події – це можливість зробити так, щоб компоненти давали одне одному команди, не знаючи про існування одне одного.

Наприклад ми маємо фігуру – трикутник. Вона складається з трьох відрізків. Кожен відрізок задається двома точками на кінцях. Якщо ми пересуваємо якусь точку (викликаємо на ній наприклад метод move), то варто було б повідомити обидва відрізки які вона в трикутнику описує що їм теж варто пересунутись і перемалюватись. Але як зробити так щоб точці не треба було знати про відрізки, аби лише відрізки знали про точку? Якщо прямо з коду move точки викликати move відрізків, то ми не зможемо створити точку яка не приєднана до жодного відрізка, чи приєднати додатковий відрізок до точки. Нам доведеться використати обмін повідомленнями.

zope.event – дуже простий. Він містить два об’єкти – список subscribers та функцію notify. В subscribers ми додаємо обробники подій, а notify ці події створює. Ось так:

from zope.event import notify, subscribers

def on_event(evt):
    print 'Event:', evt

subscribers.append(on_event)

notify('Hello!')
# Виведе нам Event: Hello!

Виглядає немовірно просто, особливо порівняно з усім попереднім. Але через те що цей модуль такий простий, кожному обробнику доведеться самостійно вирішувати чи та подія яка до нього прийшла його цікавить. Ось вам код що ілюструє приклад з трикутником:

class Point(object):
    def __init__(self, x, y):
        self.move(x, y)

    def move(self, x, y):
        self.x = x
        self.y = y
        notify(('moved', self))

    def __str__(self):
        return '(%s, %s)' % (self.x, self.y)

class Line(object):
    def __init__(self, a, b):
        self.a = a
        self.b = b
        subscribers.append(self.on_event)

    def on_event(self, evt):
        if evt[0] == 'moved':
            self.on_move(evt[1])

    def on_move(self, point):
        if (point == self.a) or (point == self.b):
            print 'moving', self

    def __str__(self):
        return '%s -- %s' % (self.a, self.b)

A = Point(0, 0)
B = Point(1, 0)
C = Point(0, 1)

AB = Line(A, B)
BC = Line(B, C)
CA = Line(C, A)

A.move(-1, -1)

Ми переміщуємо одну точку, а на екрані побачимо що на це відреагувало два відрізки:

moving (-1, -1) -- (1, 0)
moving (0, 1) -- (-1, -1)

І на цьому думаю варто завершити. Якщо ви дочитали до цього місця – молодці, в мене самого це не вийшло б з першого разу. Успіхів вам з тим для чого вам оця ZCA виявилась потрібною. Сподіваюсь вона тепер буде не настільки заплутана й незрозуміла, як була спочатку для мене.

Посилання

  1. Коротка теоретична стаття про Zope Component Architecture, написана Деніелом Роббінсом (творцем Gentoo!)
  2. A Python component architecture – коротка практична стаття написана Леннардом Легебро (автором книжки Porting to Python3)
  3. A Comprehensive Guide to Zope Component Architecture – книжка Baiju Muthukadan
  4. Документація zope.component
  5. Документація zope.interface
  6. Документація zope.event
  7. Python class advisors – James Henstridge
  8. stackoverflow.com – Purpose of Zope Interfaces?
  9. stackoverflow.com – Python 3 and static typing – Відповідь про “статичну типізацію” за допомогою інтерфейсів.
  10. stackoverflow.com – Can zope.interface define how a class’ __init__ method should look? – пояснення про те як фабрикою описати інтерфейс методу __init__.
  11. stackoverflow.com – How to make zope load my ZCML? – мої намагання розібратись з ZCML

Written by bunyk

13 Грудня, 2014 at 19:08

Опубліковано в Кодерство

Tagged with

Відповідей: 2

Subscribe to comments with RSS.

  1. […] Вивчення ZODB та ZCA. […]

  2. […] словом те що ви могли почитати в трактаті про ZCA, тільки тепер в авторській озвучці, + можна буде […]


Залишити коментар

Цей сайт використовує Akismet для зменшення спаму. Дізнайтеся, як обробляються ваші дані коментарів.