Zope接口的目的是什么?

37 投票
4 回答
15866 浏览
提问于 2025-04-15 20:52

我开始在代码中使用Zope接口,目前它们对我来说主要是用来做文档。我用它们来说明一个类应该具备哪些属性,然后在合适的类中明确实现这些属性,并在我预期会用到这些属性的地方进行检查。这种做法还不错,但我希望它们能做得更多,比如实际验证这个类是否真的实现了接口,而不仅仅是检查我是否声明了这个类实现了接口。我看过几次Zope的维基,但还是没能找到比我现在使用的方式更多的接口用法。所以,我想问问,这些接口还能用来做什么,怎么才能用得更好。

4 个回答

20

Zope接口可以帮助我们把两段代码分开,这样它们就不需要互相依赖了。

假设我们有一个组件,它在a.py模块里负责打印问候语:

>>> class Greeter(object):
...     def greet(self):
...         print 'Hello'

还有一些代码需要在b.py模块里打印问候语:

>>> Greeter().greet()
'Hello'

这种安排让我们很难在不修改b.py的情况下更换处理问候语的代码,因为b.py可能是单独打包分发的。为了避免这个问题,我们可以引入一个新的模块c.py,里面定义一个IGreeter接口:

>>> from zope.interface import Interface
>>> class IGreeter(Interface):
...     def greet():
...         """ Gives a greeting. """

现在我们可以用这个接口来解耦a.py和b.py。b.py不再直接创建一个Greeter类的实例,而是会请求一个提供IGreeter接口的工具。而a.py会声明Greeter类实现了这个接口:

(a.py)
>>> from zope.interface import implementer
>>> from zope.component import provideUtility
>>> from c import IGreeter

>>> @implementer(IGreeter)
... class Greeter(object):
...     def greet(self):
...         print 'Hello'
>>> provideUtility(Greeter(), IGreeter)

(b.py)
>>> from zope.component import getUtility
>>> from c import IGreeter

>>> greeter = getUtility(IGreeter)
>>> greeter.greet()
'Hello'
52

在我工作的地方,我们使用接口(Interfaces)来利用ZCA,也就是Zope组件架构。这个架构是为了创建可以互换和插拔的组件而设计的,使用接口可以让我们更灵活地处理每个客户的定制需求,而不需要修改我们的软件主干。可惜的是,Zope的维基文档常常不够完整。不过在它的pypi页面上,有对ZCA大部分功能的简要说明。

我并不使用接口来检查一个类是否实现了某个接口的所有方法。理论上,这在你给接口添加新方法时可能有用,可以检查你是否记得把新方法添加到所有实现该接口的类中。个人而言,我更喜欢创建一个新的接口,而不是修改旧的接口。一旦旧的接口已经发布到pypi或你的组织中,修改它通常是个坏主意。

关于术语的小提示:类是“实现”(implement)接口的,而对象(类的实例)是“提供”(provide)接口的。如果你想检查某个接口,可以写 ISomething.implementedBy(SomeClass)ISomething.providedBy(some_object)

接下来,我们来看看ZCA在哪些方面有用。假设我们正在写一个博客,使用ZCA来使其模块化。我们会为每篇文章创建一个 BlogPost 对象,它会提供一个 IBlogPost 接口,所有这些都定义在我们的 my.blog 包中。我们还会把博客的配置存储在提供 IBlogConfigurationBlogConfiguration 对象中。以此为起点,我们可以实现新功能,而不必触碰 my.blog

以下是一些使用ZCA而不需要修改基础 my.blog 包的例子。我和我的同事在实际的客户项目中做过这些事情(并且觉得很有用),尽管当时我们并没有在实现博客。:) 这里的一些用例可以通过其他方式更好地解决,比如使用打印CSS文件。

  1. 为所有提供 IBlogPost 的对象添加额外的视图(BrowserView,通常在ZCML中用 browser:page 指令注册)。我可以创建一个 my.blog.printable 包。这个包会注册一个名为 print 的浏览器视图,专门为 IBlogPost 渲染博客文章,使用一个Zope页面模板,生成适合打印的HTML。这个 BrowserView 将出现在URL /path/to/blogpost/@@print

  2. Zope中的事件订阅机制。假设我想发布RSS订阅,并且希望提前生成它们,而不是在请求时生成。我可以创建一个 my.blog.rss 包。在这个包中,我会为提供 IObjectModifiedzope.lifecycleevent.interfaces.IObjectModified)的事件注册一个订阅者,针对提供 IBlogPost 的对象。每当提供 IBlogPost 的对象的某个属性发生变化时,这个订阅者就会被调用,我可以用它来更新所有应该包含该博客文章的RSS订阅。

    在这种情况下,可能更好的是在每个修改博客文章的 BrowserView 结束时发送一个 IBlogPostModified 事件,因为 IObjectModified 在每次属性变化时都会发送一次,这可能会影响性能。

  3. 适配器(Adapters)。适配器实际上是将一个接口转换为另一个接口的“桥梁”。对于编程语言爱好者来说:Zope适配器在Python中实现了“开放”的多重分发(“开放”是指“你可以从任何包添加更多情况”),更具体的接口匹配优先于不太具体的匹配(Interface类可以是彼此的子类,这正是你所希望的效果)。

    从一个 Interface 调用适配器时,可以使用非常简洁的语法 ISomething(object_to_adapt),或者通过函数 zope.component.getAdapter 查找。多个 Interface 的适配器必须通过函数 zope.component.getMultiAdapter 查找,这样稍微复杂一些。

    你可以为一组给定的 Interface 有多个适配器,通过在注册适配器时提供的字符串 name 来区分。默认情况下,名称是 ""。例如,BrowserView 实际上是适配器,它们适配于它们注册的接口和HTTP请求类实现的接口。你还可以使用 zope.component.getAdapters( (IAdaptFrom,), IAdaptTo ) 查找所有从一个接口序列到另一个接口的注册适配器,这会返回一对对的(名称,适配器)。这可以作为一个很好的方式,提供插件的挂钩。

    假设我想把我所有的博客文章和配置保存为一个大的XML文件。我创建一个 my.blog.xmldump 包,定义一个 IXMLSegment,并注册一个从 IBlogPostIXMLSegment 的适配器,以及一个从 IBlogConfigurationIXMLSegment 的适配器。现在我可以通过写 IXMLSegment(object_to_serialize) 来调用适合某个对象的适配器。

    我甚至可以从其他包添加更多适配器到 IXMLSegment。ZCML有一个特性,可以在某个包安装时运行特定的指令。我可以利用这个特性,让 my.blog.rssmy.blog.xmldump 安装时注册一个从 IRSSFeedIXMLSegment 的适配器,而不需要让 my.blog.rss 依赖于 my.blog.xmldump

  4. Viewlet就像小型的 BrowserView,可以“订阅”页面中的特定位置。我现在记不清所有细节了,但这些非常适合像侧边栏这样的插件。

    我不记得它们是否是基础Zope或Plone的一部分。我建议除非你要解决的问题确实需要一个真正的CMS,否则不要使用Plone,因为它是一个庞大而复杂的软件,通常运行较慢。

    不过你不一定需要 Viewlet,因为 BrowserView 可以相互调用,使用'TAL表达式中的 'object/@@some_browser_view',或者使用 queryMultiAdapter( (ISomething, IHttpRequest), name='some_browser_view' ),但无论如何它们都很不错。

  5. 标记接口(Marker Interface)。标记接口是一个不提供任何方法和属性的接口。你可以在运行时使用 ISomething.alsoProvidedBy 将标记接口添加到任何对象。这允许你,例如,改变在特定对象上使用的适配器和定义的 BrowserView

抱歉我没有详细说明每个例子的实现方法,但每个例子大概都需要一篇博客文章来讲解。

24

其实你可以测试一下你的对象或者类是否实现了你定义的接口。为此,你可以使用 verify 模块(通常在测试时会用到它):

>>> from zope.interface import Interface, Attribute, implements
>>> class IFoo(Interface):
...     x = Attribute("The X attribute")
...     y = Attribute("The Y attribute")

>>> class Foo(object):
...     implements(IFoo)
...     x = 1
...     def __init__(self):
...         self.y = 2

>>> from zope.interface.verify import verifyObject
>>> verifyObject(IFoo, Foo())
True

>>> from zope.interface.verify import verifyClass
>>> verifyClass(IFoo, Foo)
True

接口还可以用来设置和测试不变条件。你可以在这里找到更多信息:

http://www.muthukadan.net/docs/zca.html#interfaces

撰写回答