Zope组件架构和接口

来自Ubuntu中文
Oneleaf留言 | 贡献2007年5月31日 (四) 09:26的版本 (新页面: 原文出处:[http://griddlenoise.blogspot.com/2005/12/zope-component-architecture-interfaces.html The Zope Component Architecture - Interfaces, Adaptation, and Duck Typing] 原文...)
(差异) ←上一版本 | 最后版本 (差异) | 下一版本→ (差异)
跳到导航跳到搜索


原文出处:The Zope Component Architecture - Interfaces, Adaptation, and Duck Typing

原文作者: Griddle Noise

授权许可:创作共用协议

翻译人员:FireHare

校对人员:Leal

适用版本:Zope 3

文章状态:草译阶段


(使用猎人、鸭子、猎狗的故事,来解释Zope的组件架构各种概念,同时避免了对ZCML的引入,并模拟了zope的对象发布机制,文章简单、有趣,是很好的zope3入门基础教程)

We've done amazing work in the past few months with Zope 3. Compared to the black magic of Zope 2 and so many other frameworks I look at, it's quite refreshing. It's not always easy, but it's been fun while also being quite trustworthy. Even without a deep understanding of all that's available, my very small company and our even smaller team of developers have achieved incredible results: a from scratch rewrite of one of our oldest and most heavily used customer's web sites, three other customers with solutions built on top of the same stack written and extracted for that earlier customer; a one week turnaround (including graphic design) of one of those other customer's web sites; sharing of benefits of refactoring of code from the customer specific layers of the stack to more general layers; an 'admin' user interface far more capable and complete than just about anything we've delivered in the past; all developed faster than ever before; and with a clean architecture despite only the lightest of planning.

在过去的几个月中使用 Zope3 进行的工作是令人惊奇的。相对于 Zope2 的黑魔法和我所见过的其它框架来说,它的表现是非常令人振奋。它并不总是很容易,但它虽然非常可靠但也很有趣。甚至在还没对它的全部功能进行深入了解的时候,我们那很小的公司和甚至更小的开发团队就取得了令人难以置信的成果:重写了我们最旧也是负载最重的一个客户网站和三个客户的解决方案,这些方案就是建立在对先前那个客户的相同的编写层次和总结基础之上的;其他客户网站之一的一周回顾(包括图形设计);从通用层次结构中的客户指定层次代码的 refactoring 中得到好处;比任何过去我们所交付的功能更强更完善的“admin”用户界面;所有开发都比以前更快;并且就算是最轻量级的规划也有一个清晰的体系架构。


What makes all this possible? The Zope Component Architecture. The Zope Component Architecture is a pretty lightweight system, especially since Zope 3.1 which simplified the component architecture that shipped with Zope 3.0 into a simple vocabulary. There are only a couple of core Python packages that provide the heart of the component architecture. And everything essentially flow through the heart.

是什么使这一切成为可能呢?Zope 的组件架构。Zope 的组件架构是一个优雅的轻量级系统,特别是在 Zope 3.1 中将 Zope 3.0 的组件架构简化到一个简单的词汇表之后。它只用两个 Python 核心包提供组件架构的核心部分,每个部分从本质上讲都要流经核心部分。


The core packages that provide the Zope Component Architecture are zope.interface and zope.component. zope.interface provides a way of describing and querying specifications. Similar in some ways to Interface objects in Java or in systems like CORBA and ILU, zope.interface Interfaces can be used to describe objects, policies, and APIs separate from their implementation. By writing against the interface, Zope 3 takes advantage of one of the key advantages of dynamic languages like Python - Duck Typing. Duck Typing is a way of saying "if it quacks like a duck, then that's good enough for me!" Just putting the zope libraries on the Python path - configuring and setting up nothing else - here's how this works:

Zope 组件架构的核心包是 zope.interface 和 zope.component。zope.interface 提供描述和查询规范的方法,这在某些程度上跟 Java 和象 CORBA 、ILU 系统中的接口对象有点类似。zope.interface 接口可以用于描述对象和策略,并且可能从它们的实现中分离 API。By writing against the interface, Zope 3 利用了类似 Python 的动态语言中的关键特性之一 -- Duck Typing。Duck Typing 的意思类似于“if it quacks like a duck, then that's good enough for me!” 只需将 zope 的库文件放在 python 目录上 - 甚至不需要配置任何东西 - 下面说明它是能工作的:

#!python
>>> from zope.interface import implements, providedBy, Interface
>>> class IQuacksLikeADuck(Interface):
...     def quack():
...         """ Returns a quacking sound. """
... 
>>> class Platypus(object):
...     implements(IQuacksLikeADuck)
...  
...     def quack(self):
...         return "In my world, a platypus goes 'quack!'"
... 
>>> ben = Platypus()
>>> IQuacksLikeADuck.providedBy(ben)
True
>>> print ben.quack()
In my world, a platypus goes 'quack!'

Now, the normal and generally allowable Python way of Duck Typing is to do something like if hasattr(ben, 'quack'): ben.quack(). But that sort of 'hasattr' code only goes so far. Zope 2 is littered with skeletons like if getattr(obj, '_isPrincipiaFolderish', 0):.... As systems grow, developers forget those old names. Or objects just grow heavy with odd little attributes that say they have not only one thing about them that makes them folderish, but many. And if its folderish, then maybe we'll draw a tree for it. But if '_isPrincipiaFolderish' is really an important attribute - where does that get documented? Who knows it's there? Who knows what its values may be. Can it be callable? Zope 2 actually has attributes like this - elements that started out as simple attributes, and then grew to be callable for situations that needed to do computation. And then grew to be callable with parameters for really specific situations. But finding out about all of these combinations and their meaning was not always easy to document or even infer. Recognizing this, an Interfaces package was made for Zope 2 but was initially used only for documentation. But Interfaces were not used as core elements, and there was little impetus to keep an interface spec up to date when it wasn't actually used in code. But in Zope 3 and in new Zope 2 work, that changes. Interfaces are core members. It means that not only are most important things documented, but that it's really more important for an object providing a certain interface to just provide that interface - and not how its implemented. I don't care how it quacks, as long as it quacks. Quacking is all that I require. Interfaces allow one to formalize duck typing, without really adding too much weight to their program. In fact, with interfaces being a sort of public interface, I find it easiest now to look at a packages 'interfaces.py' module first when looking at new code. How a specific implementation of a security policy doesn't really matter to me at first - I want to know how I interact with the security policy. So interfaces become useful for documentation without having to generate or wade through documentation generated for every single function and class and method in a particular module.

现在,在正常情况下 Duck Typing 通常允许使用的是类似 if hasattr(ben, 'quack'): ben.quack() 这样的 Python 用法,But that sort of 'hasattr' code only goes so far。Zope 2 被类似 if getattr(obj, '_isPrincipiaFolderish', 0):.... 的结构给搞乱了。当系统改变时,开发人员就会忘记那些老的名字。或者有着少量古怪属性的对象变得不仅要生成它们自己的 Folderish,还要做更多的事。如果只是它自己的 Folderish,我们还可以为它画个树来表示。但如果'_isPrincipiaFolderish'真是一个重要的属性的话,那又从哪里得到文档呢?谁知道它在哪里呢?又有谁知道它的值可能是什么,能否被调用呢?事实上 Zope 2 就存在这样的属性 - 元素一开始只是一个简单的属性,然后就变成在需要计算的环境下被调用,然后变成在特定的环境下被带着参数调用。但是查明所有这些组件及其含义并进行归档或者甚至是作个结论都并不总是很容易做到的。正是认识到这一点,在 Zope 2 中生成一个 Interfaces 包并只将之用于归档,但 Interfaces 并不用做核心元素,这样就可以产生一个小小的动力,当接口没有被实际用在代码中时可以保持接口说明是最新的。但在 Zope 3 和新的 Zope 2 的产品中,这种情形改变了,Interfaces 就是核心成员。这就意味着不仅大部分重要的事件被归档,而且使得对象提供一个确定的接口,只是提供接口 - 而非如何实现,真正更为重要起来。我并不关心它在 quacks 的时候如何 quacks,Quacking 就是我所要的全部。Interfaces 允许其中之一去格式化 duck typing,而不是给他们的程序添加大量的负荷。实际上,随着 interfaces 成为一种公共接口,我发现现在在看新代码时查看'interfaces.py'模块包相当容易。案例策略的特定实现起始对我来并不真有关系 - 我想知道的是我如何同安全策略交互。于是 interfaces 对于无须费力地对特定模块的每个单独的函数、类和方法生成文档就变成很有用了。

Of course, interfaces on their own are still not all that great. The second part offered by zope.interface is adaptation. Sticking with the quacking example, let's add a hunter into the mix. He wants to go duck hunting. He's going to need to attract ducks by quacking like them.

当然,interfaces 也不全然都好。 zope.interface 提供的第二部分就是适配。还是鸭叫的例子,让我们在里面加一个猎人,他想猎捕鸭子,并打算通过模仿鸭叫来引诱它们。

#!python
>>> class Hunter(object):
...     def <u>init</u>(self, name):
...             self.name = name
... 
>>> tom = Hunter('Tom')
>>> IQuacksLikeADuck.providedBy(tom)
False

What we need here is an adapter. This is where zope.component and zope.interface really collaborate. By default, most adapters in Zope adapt from one interface to another. But you can also adapt a class to an interface, which is what we'll do here since we didn't make an 'IHunter' or 'IPerson' or 'IQuackless' interface.

在这里我们需要一个适配器。它可以让 zope.component 和 zope.interface 真正合作。缺省时,Zope中的大多数适配器是从一个接口适配到另一个的。但你也可以将一个类适配到接口的,正如我们接下来要在这里做的那样,因此我们没有生成 'IHunter' 或 'IPerson' 或 'IQuackless' 接口。

#!python
>>> from zope.component import adapts
>>> class DuckCall(object):
...     implements(IQuacksLikeADuck)
...     adapts(Hunter)
...  
...     def <u>init</u>(self, hunter):
...         # Adapters are passed in their adaptee as first argument
...         self.hunter = hunter                                 
...  
...     def quack(self):
...         return self.hunter.name + ' quacks with a duck call'
... 
>>> zope.component.provideAdapter(DuckCall)

So here, we say that instances of DuckCall implement the IQuackLikeADuck interface, and that this particular class adapts Hunter objects. The next line registers the adapter with the global component architecture adapter registry. This is the easiest version of the call - a factory (in this case a class) is passed in which declares what it adapts and provides directly. Now there's a duck call available for hunters. How do we use it? There are a couple of ways. One is to use zope.component.queryAdapter or getAdapter - the only difference being that queryAdapter returns 'None' or a user supplied default if the adaptation can't happen, while getAdapter raises an exception. But the interesting way is to use the interface directly, passing in the object:

因此在这里,我们说 DuckCall 的实例实现了 IQuackLikeADuck 接口,这个特定的类适配 Hunter 对象。下一行注册了带有全局组件架构适配器注册库的适配器。这是调用最容易的一种描述 - 一个声明在其中可以直接适配和提供什么的 factory (在本例中是一个类)被通过。现在猎人可以做鸭叫了,那么我们怎么来用它呢?有两种方法,一种是使用 zope.component.queryAdapter 或 getAdapter - 它们之间唯一的不同在于当适配没有发生时 queryAdapter 返回 ‘None’或用户提供的缺省值,而 getAdapter 抛出一个异常。但有趣的方法是直接使用接口,可以是对象:

#!python
>>> IQuacksLikeADuck(tom)
<<u>main</u>.DuckCall object at 0x67cad0>

Like with 'queryAdapter', a second argument can be passed in as a default if the object cannot be adapted, otherwise an exception is raised. What this form allows for is code that works with objects by interface, and it allows for other code to provide expected interfaces for existing objects. Lets add to the wildlife. First, there's the hunters dog:

象 'queryAdapter' 一样,当对象没有适配时,第二个参数将被作为缺省值返回,否则就抛出异常。这种形式考虑的是 那样通过接口同对象一起工作的代码,它也考虑到其他为已有对象提供异常接口的代码。让我们添加动物。首先是猎狗:

#!python
>>> class IrishWolfhound(object):
...     size = 'HUGE'
...     def <u>init</u>(self, name):
...             self.name = name
... 
>>> squeekers = IrishWolfhound('Squeekers')

And a duck. Since the duck provides 'quack' directly, we can separately say that instances of the class provide the IQuacksLikeADuck interface, or we could even say that a particular instance provides it. This shows how we can separately say that the class implements it.

还有鸭子。因为鸭子直接提供 ‘quack’, 我们可以单独说类的实例提供了 IQuacksLikeADuck 接口,甚至我们可以说一个特殊的实例提供了它。这表明我们可以单独说类实现了它。

#!python
>>> class Duck(object):
...     def quack(self):
...             return "The best quack is a duck quack."
... 
>>> hunted = Duck()
>>> IQuacksLikeADuck.providedBy(hunted)
False
>>> from zope.interface import classImplements
>>> classImplements(Duck, IQuacksLikeADuck)
>>> IQuacksLikeADuck.providedBy(hunted)
True

And it should be noted that if an object provides an interface itself, then the adapter lookup will return the object itself. So now that we have quite a little menagerie. Lets see what some simple tickler code might look like that would take a list of objects and try to make them quack.

请注意当对象自己提供接口时,适配器查找将会返回对象自身。因为现在我们有个非常小的动物园,Lets see what some simple tickler code might look like that would take a list of objects 并让他们做鸭叫。

#!python
>>> def tickler(*args):
...     " Goes through the provided arguments and tries to make them quack "   
...     for potentialQuacker in args:
...         quacker = IQuacksLikeADuck(potentialQuacker, None)
...         if quacker is None:
...             print "Could not quack: %r" % potentialQuacker
...         else:
...             print quacker.quack()
... 
>>> tickler(ben, tom, squeekers, hunted)
In my world, a platypus goes 'quack!'
Tom quacks with a duck call
Could not quack: <<u>main</u>.IrishWolfhound object at 0x698bd0>

The best quack is a duck quack.Of course, this is a fiendishly simple showcase. But it's a powerful building block, and is one of the core concepts of Zope 3. Just about everything flows through interfaces and adapter registries. A powerful extension of the 'adapter' concept is the 'multi-adapter'. The multi-adapter situation most commonly used is the Browser View. A Browser View is made by adapting an object, like the Duck, and another object, like an HTTP Request, to something that gets rendered to a web browser via HTTP. Fundamentally, the Component Architecture merely provides the registries for loosely-coupled objects to work together, and it removes a lot of the magic.

最好的 quack 是鸭的 quack。当然,这是一个非常简单的示例。但是它是一个强大的构建块,也是 Zope 3 核心概念之一。几乎每个东西都要流经接口和适配器注册库。一个对“适配器”强有力的扩展就是“多适配器”。大多数多适配器通常被用于浏览器视图。一个浏览器视图被用来适配一个对象,如 Duck 或另一个对象,就象通过 HTTP 获得代码去渲染 WEB 浏览器的 HTTP 请求一样。从本质上来说,组件架构主要是为松耦合对象一起工作提供注册库,并去除了许多 magic。

In something purely theoretical, lets remove all of Zope as people know it (the object database, the publisher that traverses URLs to return objects, and so on and so on. Using the component architecture, lets do an extremely lightweight 'rails'-ish thing. This code makes a fake little database with just tuples of data, as might be returned by a basic dbapi adapter. It defines a Utility to find the 'model' from this database. A utility is a new concept. In quick, utilities are other abstract notions, used loosely. One can define and install different utilities, and the code that uses the utility has no hard dependencies on the utility object. Examples include RDB connections - 'get me an IRDBConnection with the name "racers"'; caches; and more. Sometimes all utilities for a particular interface are queried, sometimes you just want an individual one. The zope.component.provideUtility call establishes the utility. In this code we have only one and we name it 'greyhound'. That name is used later in the 'simplePublish()' function to find the utility by combination of its name and published interface. Again - this code was tested by ensuring that my Zope 3.1 installations lib/python directory was in the Python path and that's it.

从纯理论的角度,让我们去除所有 Zope 中众所周知的部分(对象数据库,通过 URL 返回对象的发布器等等,使用组件架构,让我们做一个非常轻量级的 'rails'-ish 东西。该代码用数据元组的形式生成一个假的小数据库,也许会通过一个基本的 dbapi 适配器返回。它定义了一个工具以从该数据库中找出“模块”。工具是一个新概念。简单地说,工具是其他不精确的抽象概念。一个可以定义和安装不同的工具,工具所用的代码并不严格依赖工具对象。就象包含 RDB 连接一样 - “给我一个叫'racers'的 IRDBConnection 接口”;缓冲等等。有时要查询对于某特定接口的所有工具,有时你只想要其中的一个。zope.component.provideUtility 调用确认工具。在该代码中我们只有一个,我们将其命名为 'greyhound'。该名字在稍后的 'simplePublish()' 函数中被用于通过它的名字和发布接口来再次找到工具。 - 该代码被测试通过,请确保我的 Zope 3.1 安装库/Python 目录被包含在 Python 路径中,就是这样。

#!python
import zope.component
import zope.interface
import zope.schema
class IGreyhoundData(zope.interface.Interface):
"""
zope.schema provides a way of defining the properties and their bounderies
as they make up a component. It integrates with the interface system and
can be used for documentation, validation, data retrieval and setting, 
and more. The fields define 'get' and 'set' methods, among others, useful
for querying and setting data on an object without having to have intimate
knowledge of the object's structure. Useful for web forms, but also for
database storage and retrieval.
"""
name = zope.schema.TextLine(title=u"The Greyhound's Name")
raceName = zope.schema.TextLine(title=u"The name on the race card.")
fastestTime = zope.schema.Float(title=u"Fastest race time (in seconds).")
trackLength = zope.schema.Int(title=u"The length of the track, in yards, of the fastest time.")
retired = zope.schema.Bool(title=u"False if the greyhound is still racing.")
adopted = zope.schema.Bool(title=u"True if the greyhound has a home after the track.")

class IDataFinder(zope.interface.Interface):
def find(oid):
""" Returns an object that matches this object id. Raises KeyError if not found. oid should be an integer. """

# Define a fake database
greyhounddb = {
1: ('Betty Joan', 'Beauty', 31.22, 503, True, True),
2: ('Dazzling', 'Dazzling Pet', 32.00, 503, True, False),
}

class Phantom(object): 
""" Phantoms can be anything... """
pass

class DataFinder(object):
zope.interface.implements(IDataFinder)
schema = None
factory = None
db = None

def find(self, oid):
""" 
Calls the fake database and uses the schema system to map fields
from the tuple returned to a new Phantom object, and then ensures that 
the phantom is marked to provide the schema in 'self.schema'
"""
raw = self.db.get(oid)
newobj = self.factory()
for idx, name in enumerate(zope.schema.getFieldNamesInOrder(self.schema)):
field = self.schema[name]
value = raw[idx]
field.set(newobj, value)
zope.interface.directlyProvides(newobj, self.schema)
return newobj

# Define a basic data finder
class GreyhoundFinder(DataFinder):
schema = IGreyhoundData
factory = Phantom
db = greyhounddb

greyhounds = GreyhoundFinder()
zope.component.provideUtility(greyhounds, name='greyhound')

class ISimpleRequest(zope.interface.Interface):
headers = zope.interface.Attribute('Mapping of request headers')

class SimpleRequest(object):
zope.interface.implements(ISimpleRequest)
def <u>init</u>(self, **headers):
self.headers = {}
self.headers.update(headers)

class IView(zope.interface.Interface):
""" Simple view interface. """
def render():
""" Returns a rendered string of this view. """

class RacerView(object):
zope.interface.implements(IView)

def <u>init</u>(self, context, request):
self.context = context   # aka, the racer
self.request = request

def render(self):
return "Name: %s; Fastest Time: %0.2f" % (self.context.name, self.context.fastestTime)
zope.component.provideAdapter(RacerView, (IGreyhoundData, ISimpleRequest), name='view')

detail_template = """Name: %(name)s
Racing Name: %(raceName)s
Fastest Time: %(fastestTime)0.2f for length %(trackLength)d
Retired: %(retired)s
Adopted: %(adopted)s
"""

class DetailsView(object):
zope.interface.implements(IView)

def <u>init</u>(self, context, request):
self.context = context   # aka, the racer
self.request = request

def render(self):
racer = self.context
mapping = dict([ 
(name, field.get(racer)) 
for name, field in zope.schema.getFieldsInOrder(IGreyhoundData) 
])
return detail_template % mapping
zope.component.provideAdapter(DetailsView, (IGreyhoundData, ISimpleRequest), name='detail')

def simplePublish(path):
""" 
Breaks 'path' up into three components:

- ``name``: used to look up a data finder utility
- ``viewname``: used to query a multi-adapter for the found object and simple
http request
- ``oid``: used to look up an object out of the finder

from ``name/viewname/oid``

returns the rendered view.

A very very simple showcase of how the zope component architecture might
be used to register and find model accessors and views in a web publishing
system besides the traditional Zope and 'zope.app' setup.
"""
request = SimpleRequest(ignored='hi')
getter, viewname, oid = path.split('/')
oid = int(oid)

# This gets an 'IDataFinder' utility with the name in the 'getter' value.
# A greyhound getter that queries a real database could have been installed
# instead - it doesn't impact this code.
model = zope.component.getUtility(IDataFinder, name=getter).find(oid)

# Now that we have the model object and the fake request, find something
# that adapts both of them to a single object. Since the request is fake
# and the views are simple, we don't really show how the request object
# is used here. But this is basically where the web environment and model
# environment would actually meet - only in the multi-adapter. Other than
# that, both environments are oblivious to each other. It's only the zope
# component architecture registries that know what's in them, and they're
# pretty agnostic.
view = zope.component.getMultiAdapter((model, request), name=viewname)
return view.render()

def test():
print "Publishing ``greyhound/view/1``"
print simplePublish('greyhound/view/1')
print
print "Publishing ``greyhound/view/2``"
print simplePublish('greyhound/view/2')
print
print "Publishing ``greyhound/detail/1``"
print simplePublish('greyhound/detail/1')
print
print "Publishing ``greyhound/detail/2``"
print simplePublish('greyhound/detail/2')

if <u>name</u> == '<u>main</u>':
test()

And when run:

Desktop> python zopetest.py
Publishing ``greyhound/view/1``
Name: Betty Joan; Fastest Time: 31.22

Publishing ``greyhound/view/2``
Name: Dazzling; Fastest Time: 32.00

Publishing ``greyhound/detail/1``
Name: Betty Joan
Racing Name: Beauty
Fastest Time: 31.22 for length 503
Retired: True
Adopted: True


Publishing ``greyhound/detail/2``
Name: Dazzling
Racing Name: Dazzling Pet
Fastest Time: 32.00 for length 503
Retired: True
Adopted: False

支持所有版本类