Zope3和five技术探索

Zope3是对zope2的一次完全的革新,很多设计理念都有根本上的变化。

先来看看zope2中所暴露的缺点:
为了尽可能简单的提供丰富的功能(特别是对于非技术用户而言),Zope做了许多自动化的工作。
这对于简单的应用是非常好的,但是对于那些更复杂些的应用,反而是一种障碍了,如隐式获取(Implict Acquisition)。
Plone的皮肤功能继承自CMF,它依赖获取机制,这意味着所有的模板和脚本都共享着一个共同的命名空间(namespace),而不管内容的类型是什么。名字最好要包含类型信息,并且要小心谨慎的获取(否则就可能重名)。

在传统的Zope产品中,一个对象的所有功能都是它自己的类或者基类提供的。改变这些产品行为的唯一方式,是去修改或者子类化它们。
修改产品通常会使维护升级变得很困难。子类化也是令人讨厌的,因为它会让基类和子类之间耦合性得过于紧密了。

Zope3做了什么呢?
对于Zope2中那些易混淆的概念则予以剔除或改造,如Zope3去掉了隐式获取的支持,只允许显式的获取。
组件装配基于对象的连结,通过接口(interfaces)对组件进行组装。并且使用视图将业务逻辑与呈现相分离。

在Zope3中每一个页面模板可以声明为只用于某一种接口,只有那些实现了这个接口的对象才能使用这个页面模板,这样页面模板的名字就不再是全局的了,因此很好地解决了命名冲突的问题。

但是目前的Plone2.5以及即将发行的Plone3.0都是运行于Zope2的,因为它仍然依赖于Zope2的隐式获取机制。
对于一个很容易定制开发的Plone而言,隐式获取机制并非全无用处, Plone中有一些关键的特性还得依赖于它。
只是Zope2中缺少一些Zope3所拥有的新的特性,如全局模板命名冲突等问题,因此需要一种简单的方案在Zope2引入Zope3的解决方式,这就是five。

上面讲到,Zope2通过继承类增加功能,会造成代码难以重用等缺点。 Zope3的原则是将代码按照功能单元分解成尽量小而精简,那么这么多分散的小的功能需要一种组合的方式。

Zope3采用配置标记语言(ZCML)来完成这个工作。 ZCML采用XML语言作为基础,通过描述的方式进行各种类与接口的适配、声明,将扩展功能类与基类进行组合。

Zope中默认加载的ZCML文件名为configure.zcml, 基本的框架为:
< configure  xmlns ="http://namespaces.zope.org/zope"
           xmlns:browser
="http://namespaces.zope.org/browser"
           xmlns:five
="http://namespaces.zope.org/five" >

           
< include  package =".browser"   />
           
<!--   ... 在这里添加ZCML语句  -->

</ configure >
这里的include指令实质就是把当前目录中的browser文件夹下的configure.zcml文件引入这个外层的configure.zcml文件。

接口是Zope所开创的一项Python基础技术。它借用标准的Python类(class)来定义。按照习惯约定,所有的接口命名都应该加前缀 I 。
它和类很相似,但是类中方法不包含任何实现,且方法定义中没有self参数。(旧的zope2接口只用来做文档化作用,如:__implements__ = (WriteLockInterface,))

有两种方式实现一个接口:其一就是通过interface.implements(interfaceName) 方法;其二就是通过上面提到的zcml技术去配置,如
<five:implements class=".ATAudio.ATAudio" interface=".interfaces.IATAudio" />
这种方法的好处就是对于不是自己维护的代码不能直接修改它的类定义,这时候使用ZCML来实现接口也是唯一的让它实现接口的方式。
有人会想,这里有什么用,如果不是自己维护的代码,就算声明了它实现哪个接口,也无法真的去在这个类中修改代码。其实这里有些这样的好处:1是为了适配器而用的,如果某个类实现了适配器中指定的目标(为实现这个目标接口的类的实例而新增功能),就能得到新增的功能;2是可以只做一个标记,而不去写新的适配器类,这样可以用来判断是不是这个不能修改的类实例是不是可以被强制转换为这个标记的接口,通过这个boolean值去做些事件处理或者什么别的逻辑处理。

Zope3组件:实现了接口的类,这个类经过实例化所生成的对象也就称为一个Zope3组件。
组件是Zope3的基础,它不仅将系统中已有对象都强制为一个个组件,对于新增加的功能也是如此。

想一想在Zope2中增加一个功能是怎样的方式,在Zope2中通常使用子类化的方式去扩展一个对象的功能,但子类化也就意味着扩展的功能只能在此对象上使用,如果还想将功能使用于另一种类型对象,还必须得子类化另一个类,再添加同样的功能,聪明一点的办法是在一个公共的utils中定义功能,再在每一个子类对象中分别使用公共的功能,但这样的解决办法比起Zope3的解决方案来实在是相形见拙。

适配器用于为对象扩展新的功能,通过适配器,可以为一个接口增加另外一个接口的功能,实现2个接口之间的适配。
它是一个Python类,并且实现了新接口中定义的所有方法和属性。

具体的重构过程是在一个新的接口中定义这些功能,并在新的适配器类中实这个新接口的功能,以合适的ZCML将它们组装起来,这样让所有原始对象都有新适配器对象所提供的功能。

说了这么多,看一下一个真实的适配器用法的例子:
#  obj is ATAudio object
from  interfaces  import  IAudio

adapted_obj 
=  IAudio(obj)
title 
=  adapted_obj.title
title 
=  title.replace( ' Blue ' ' Country ' )
adapted_obj.title 
=  title
首先假设有obj是一个ATAudio对象,它实现了IATAudio接口。适配器ATAudioAudio就正是为IATAudio接口的对象提供IAudio接口的功能。
因此下面直接使用接口对对象进行强制转换,就是 IAudio(obj) 。强制转换的结果是返回一个适配过的对象,这个对象上可以调用所有适配器中所定义的方法。
如这个例子中适配器ATAudioAudio上定义了title设置和获取,则可以从adapted_obj上直接调用。

这里也就反应出了一个适配器的优点:只需要调用功能接口,而不需要考虑具体功能实现。

其中的适配器定义部分代码如下:
from  zope  import  component, interface
from  Products.ATAudio  import  interfaces

class  ATAudioAudio(object):
    
""" An IAudio adapter for IATAudio.
    
"""

    interface.implements(interfaces.IAudio)
    component.adapts(interfaces.IATAudio)

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

    
def  _get_title(self):
        
return  self.context.Title()
    
def  _set_title(self, v):
        self.context.setTitle(v)
    title 
=  property(_get_title, _set_title)

    
#  ...
这只是其中开始一段,可以看到它基于object类,并使用implements实现了其功能定义接口。

这次不同的是多了一个component.adapts语句,这一句的意思是将这个类的功能适配到实现了IATAudio接口的对象上。ATAudio实例对象已实现了IATAudio接口,因此这个适配器可以作用于ATAudio实例对象。

它的构造方法中只有一个参数,按照习惯它被命名为context。这个参数表示被适配的对象。

上面的操作只是在代码中写到可以适配于IATAudio接口的对象上,但是还没有经过装配,怎么做呢?现在?
还是用到了zcml技术。
Zope3使用ZCML来将它们适配起来。在configure.zcml中可以看到这样一句:
<adapter factory=".audio.ATAudioAudio" />
这就是装配语句,在初始化读到这一句时会找到audio中的ATAudioAudio类,读取其中的实现接口信息和适配对象信息来进行装配。

这种只有一行factory声明的是一种简化式写法,因为在适配器中已经声明了所实现的接口和所适配到的对象类型。
Five在初始化产品的过程中会找到这一句,对应到audio.py中的ATAudioAudio类上去找,已声明实现了IAudio接口(interface.implements(interfaces.IAudio)),适配到提供IATAudio接口的对象上(component.adapts(interfaces.IATAudio))。

其实就如同定义某个类实现哪个接口一样,还可以不在代码中硬编码,而用zcml配置如:
< adapter
   
for =".interfaces.IATAudio"
   provides
=".interfaces.IAudio"
   factory
=".audio.ATAudioAudio"   />
这一段ZCML的意思是针对任何实现了".interfaces.IATAudio"接口的对象,为其提供".interfaces.IAudio"接口所定义的功能,功能的具体实现在".audio.ATAudioAudio"类中。

注意到这种写在ZCML声明中的装配语句可能要比写在代码中的语句更难理解一些,因此在自己的产品中已确定适配类型的应该像ATAudio产品一样只留下适配语句写在ZCML中,在Zope3的开发实践中已经证明将太多的功能代码转移到配置文件中也不是好的方式,因此应该在功能代码中声明接口和所适配对象类型,而在ZCML中只使用简单的:<adapter factory=".audio.ATAudioAudio" />

前面提到Zope2皮肤存在一个很大的问题,就是所有皮肤元素都共享一个全局的命名空间,当产品有很多的皮肤元素时,必须十分谨慎地给皮肤元素命名以避免命名冲突。 Zope3使用视图技术很好地解决了这个问题。

视图就是一个Python类,它可以选择与一个页面模板相关联。通过在类中编写所有的逻辑处理部分,在页面模板中调用这个视图类所提供的功能,保持了页面模板作为呈现单元的作用的清晰性。

视图正是这样一种适配器,但它与普通适配器不同的是它不仅要适配目标对象,还适配着一个用户请求对象request,因为视图是与这两者都有关系的,因此从Python程序的角度来说,视图就是一种二参数适配器。
对于自动测试而言,视图所带来的额外的优点是适配器都是单独的代码段落,这是很容易写测试案例的。
实际上所有的视图都可以在URL上调用,或者在页面模板中调用。

编写视图适配器类和普通适配器类一样,只是__init__方法中多了一个request参数,但zcml装配这个视图则有点不同。
< browser:page
    
name ="view-with-z3.html"
    for
=".interfaces.IATAudio"
    permission
="zope2.View"
    template
="audio.pt"
    class
=".browser.AudioView"
    
/>

< browser:page
    
name ="edit-with-z3.html"
    for
=".interfaces.IATAudio"
    permission
="cmf.ModifyPortalContent"
    class
=".browser.AudioEditForm"
    
/>

第一个是查看视图,命名为"view-with-z3.html",其中的for属性是说明它只能使用在实现了interfaces中定义的IATAudio接口的对象上, permission说明调用权限是zope2的View权限, template说明它使用页面模板audio.pt为模板, class说明使用browser中定义的AudioView类的功能。
第二个是编辑视图,同理可知它命名为"edit-with-z3.html",使用于IATAudio接口的对象上,权限是"cmf.ModifyPortalContent",使用了browser中的类AudioEditForm的功能。

注册的视图可以直接在URL上调用,如:http://localhost:8080/path/to/object/@@view_name。但要注意加上@@前缀
@@ 表示后面跟的是一个视图,而不是普通的内容,与脚本或模板可以用在任意的context对象上不同的是,视图是注册在某一类接口上,因此context所代表的对象必须是实现了这个接口的对象,否则会报出适配器不匹配的错误。

视图用在TALES中的通用形式是以 @@ 来引用视图,如:context/@@view_name。

除此之外,Zope3中还有一些特殊的接口,为这些接口添加视图可以实现一些特殊的效果。如皮肤中常用的图片,这种类型相比其它皮肤元素而言完全是静态的, Zope3的方式是将它分离开使用单独的命名空间以减少可能的命名冲突。

这在Zope3中称为资源视图,用于添加图片等静态对象,它使用一个单独的 browser:resource 指令:<browser:resource name="myphoto.jpg" image="myphoto.jpg" />

这个语句中声明了一个名称为"myphoto.jpg"的资源对象,在TALES中的访问方法是使用 ++resource++:

context/++resource++myphoto.jpg

直接在URL中访问就是:http://localhost:8080/++resource++myphoto.jpg

视图本是Zope3使用的一项技术,有了Five产品能让它使用在Zope2项目中,其实只要满足两个条件就好:一是对象必须是一个Zope3组件,也就是对象是实现了某个接口的类的实例;二是对象可以用zope3的方式去漫游,而不是zope2的老方式。

要让已存在的类实现一个接口,由上面我说的,是很容易的事,这里不再累赘。至于改变漫游方式利用five指令<five:traversable class=".mymodule.MyClass" />就可以了。它的本质是在底层重载了 __bobo_traverse__ 方法实现。这先使用Zope 3的方式漫游(traverse),查找视图和其他的东西,如果找不到,就回到标准的Zope 2的漫游方式。

对于事件处理,Zope3的解决方式是把事件的注册和处理与基本的程序部分相分离,将事件处理代码定义在单独的函数中,使用ZCML将它们装配起来工作。这和适配器、视图的解决思路其实是一致的:就是"保持基类尽量小"。
< subscriber
    
for =".interfaces.IATAudio
         zope.app.event.interfaces.IObjectModifiedEvent"

    handler
=".audio.update_catalog"
    
/>

这就是zope3在zcml中配置订阅事件的方法,十分灵活。它的意思是针对实现了".interfaces.IATAudio"接口的对象,当"zope.app.event.interfaces.IObjectModifiedEvent"事件发生时,调用handler所说明的函数。

最后来谈谈工具这个话题。对于有用的工具函数,Zope3提供了一个utility指令用于注册工具。采用这种方式能够实现系统的松散耦合,降低维护难度。先定义一个接口,再实现它,这和开发适配器的流程一样。
然后是在ZCML中使用 utility 指令声明它是一个工具,比如:
< utility
    
provides =".interfaces.IATAudioMigrator"
    factory
=".migration.ATAudioMigrator"
    
/>
声明为工具之后,在所有需要使用到的地方都可以用它。使用component.getUtility去获取工具。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值