组件架构-介绍 难度:新手 技能: 熟悉面向对象编程。 有接口的相关知识,例如通过学习前面的章节。 如果有面向组件编程的知识将更好。(可选) 问题/任务: 当组件架构被第一次思考的时候,它是作为一个Zope2的扩展,并不是像现在这样替代者。问题是由于增加持久性的特性、修正错误和代码矛盾,导致Zope2的API非常臃肿和不协调。 另一个动机是组合从咨询工作中学到的课程和建立大型的Web应用。这些证明了单对象继承所带来的实际限制。 对松散连接架构越来越多的需求也引发了使用微型接口的对象而不是Zope2中又少又巨大的对象。这种类型的框架也将彻底降低学习曲线,因为开发者只需要学习很少的APi就可以完成给与的任务。 所有的这些需求都指向一个面向组件的框架,在Zope3里叫做"组件架构(Component Architecture)"。很多大型软件项目都已经转型为面向组件的系统了。 下面是一些有名的项目: COM (Microsoft's Object Model) Corba (Open source object communication protocol) 开源的对象通信协议 KParts (from the KDE project) 来自KDE项目 Mozilla API (it is some ways very similar to Zope 3's CA) 在某些地方与Zope3的CA非常相像 JMX (Sun's Java Management Extensions manages Beans) 然而,虽然Zope3与上面的架构有很多相似之处,还是要感谢Python提供了这样强大的灵活性来使它变得可能,而其他的编译型的语言是不允许的。 解决方案: 在这一章,我将向你进行一个对所有组件类型的高级的介绍。在本书中将有大部分使用这里介绍过的组件类型来开发的具体例子。 服务(Services) 服务是在应用服务器没有失去作用的时候提供的基础的功能性。他们相当于CMF(Zope 2)中符合"工具",并从中得到些许的语义上的灵感。 服务完全不依赖于其他组件。你只需要向其他组件的构造函数或者服务方法传递参数来与它们交互。任何应用都应该只有少数的服务涉及到最基础的功能。当处理本地的请求时,如果不能在本地得到响应,那么服务应该永远将请求向上委派,直到服务的全局版本。 最基本的服务是组件注册他们自己。例如,每当你利用ZCML将一个类注册为一个utility的时候,那个类是被注册在"Utility Service"里的,并且可以随后利用服务检索出来。举例来说,每当你使用ZCML将类注册为一个实用工具的时候,这个类就注册到在"Utility Service"里,并且可以随后通过使用该服务来检索到它。是的,我们还有一个 "服务的服务(Service Service)"来管理所有的注册服务。 另外的一个服务是错误报告(Error Reporting)服务, 用来记录所有的在页面发布过程中产生的错误。当错误发生的时候,它允许开发者检讨错误的细节和检讨系统/请求的状态。
错误报告服务的 UML 图表
服务接口的一个习惯性用法是他们只包含"访问者(accessor)"方法。整流器(Mutator)方法通常是特别实现的并且是由附加的接口提供的。这种模式的结果是一旦系统在运行中,那么服务通常不能被修改。请注意我们强烈地劝诫开发者放弃为应用编写服务。请改为使用实用工具(utilities)。 如果请求在本地不能得到回答,那么服务就藉由委派请求的方式来获得。举例来说,如果我想找一个名叫 "hello1" 的实用工具,它提供了IHello接口并且在本地站点没有找到,那么实用工具服务(Utility Service)就将该请求委派到父站点里。这些最终都会到达全局实用工具服务(Utility Service)。只有当全局服务也不能提供结果时,那么一个错误将被返回。更多关于本地和全局组件的内容请参考下面的"全局vs本地"章节。
适配器(Adapters) 适配器可以被考虑为架构中的"胶合组件",他们将一个接口连接到另外一个接口而且允许各种各样的高级编程技术 , 像面向方面编程(Aspect-Oriented Programming)。一个适配器携带一个实现一个接口的组件,并使用这个对象提供另外一个接口。
一个适配器的UML图表,将IExample适配为IReadFile和IWriteFile
这允许开发者将功能分割成小的API,并且可以保持这些功能的可维护性。举例来说,一个人可以写一个允许将IExample内容组件表现为FTP中的一个文件的适配器(见上面图表)。 这可以通过为内容组件实现 IReadFile 和 IWriteFile 接口来做到。除了直接在SimpleExample类里面加入接口实现来达到目的以外,我们还可以创建一个适配器来将IExample适配到IReadFile和IWriteFile。一但适配器被两个接口注册了 ( 通常经过 ZCML),它可以像下列这样被使用: 1 read_file = zapi.getAdapter(example, IReadFile) 2 write_file = zapi.getAdapter(example, IWriteFile) getAdapter() 方法找一个IReadFile的适配器,它可以映射任何一个在本例中(SimpleExample的实例)实现的接口。一个可选的参数名叫context,可以作为一个关键字参数传递,用来指定查找适配器的地点。如果没有限定条件,则系统只查找全局适配器。默认为发出请求的站点。 在这个特别的情形中我们从一个接口适配到另外一个接口。但是适配器也能从一些接口适配到另外一个接口。这些即是多重适配器。虽然多重适配器在开始时被看作是多余的,但是它们现在有着广泛的应用。 适配器最好的副作用就是你根本不需要碰SimpleExample的原始实现。这就意味着我可以通过使用适配器和ZCML来在Zope3中集成任何Python产品。
实用工具(Utilities) 实用工具和服务很相像,但是不提供重要的功能,所以当使用工具丢失的时候应用程序应该不会被破坏。这句话应该用一个例子来阐述。 在Zope3的前阿尔发开发中,到不同关系数据库的SQL连接是由一个服务管理的。SQL 连接服务会处理 SQL 连接,而且使用者然后可以为名字的 SQL 连接服务。如果一个连接是无效的,那么服务会给一个否定的回答。这时我们了解了实用工具的角色,而且我们能够摆脱SQL连接服务(SQL Connection Service)并将SQL连接作为实用工具来实现。现在我们能要求实用工具服务(Utility Service)给我们一个实现ISQLConnection的对象,而且还有一个被指定的名字。我们意识到许多担当注册用途的服务是可以被丢弃的,而由它们管理的对象则变成了实用工具。这大大减少了服务的数量而且降低了系统的复杂度。本节的课程是,当你开发一个服务之前,应该先评估它是否仅仅担当一个容器,在这种情况下采用实用工具来实现这些功能将是更好的选择。 工厂(对象类/属性) 工厂,如名字所说的那样,是为了创建其他的组件和对象而存在的。工厂可以是方法,类或甚至是可调用的实例。如果你指定了工厂指令,开发者就可以在处理内容对象的时候直接遇到它们了(因为ZCML可以自动为你创建工厂)。当处理满足的物体 (自 ZCML 以后为你自动地产生工厂) 如果你叙述工厂指令的时候 , 开发者直接地只遇到他们。工厂的功能和可用性可以通过一个例子来很好的描述出来。 让我们再一次考虑我们的SimpleExample内容组件。一个工厂必须提供二个方法。明显的一个是__call__()方法,它创建并返回一个SimpleExample实例。第二个方法叫做getInterfaces(),它返回一个这个使用__call__()方法创建的对象可提供的接口的列表。 工厂被简单的命名为提供IFactory的utilities。通过使用一个特别的子指令的,你能使用一个id来注册一个工厂, 就像例子那样。SimpleExample。一旦工厂被注册了,你就能使用组建架构的createObject()的方法隐式的使用工厂来创建Simple Examples。 1 ex = zapi.createObject('example.SimpleExample') 参数只是工厂的id。顺便一提,一个工厂id在整个的系统中必须是唯一的并且工厂的低级功能大部分被高级的配置所隐藏起来了。你可以指定一个可选性的上下文参数,它指定了你正在访问的位置。默认情况下,上下文是发起请求的站点;如果你指定为None,则只有全局的工厂被考虑。 显示组件--视图、资源、皮肤、层 显示组件,尤其是视图,很类似于适配器的,除了他们似层皮肤那样使用附加参数之外。事实上,今后版本的zope3,此显示服务将被移除并且显示组件将变成适配器。显示组件用于为其它的组件提供多样的输出格式(显示类型),比如HTML,FTP,XML-RPC等等。 为了使得一个视图工作,二条信息必须被提供。首先,此视图必须知道它为哪些对象提供视图。这个对象通常被认为是视图的上下文。其次,我们需要知道一些协议特指的信息,它们存储在请求对象中,该对象在视图中可永远通过此请求的变量名来访问。对于HTML,例如,请求包含所有的cookies,表单变量和HTTP头信息值,也包含授权用户和可应用的场所。一个视图的方法的返回值依赖于显示的类型和方法它自己。视图方法的返回值取决于表现类型和方法本身。例如,HTTP 视图通常的返回HTML体,而FTP 可能返回"文件名"的列表。 资源是显示组件在他们自己的权力。与视图相比,他们没有提供显示另一个组件,但是提供一些独立于任何的其它的组件的显示输出。HTML是一个主要的例子。样式表和图像(布局)经常不被认为是内容,并且同样没有依赖于任何的内容对象,但是他们是HTTP显示类型。然而,不是所有的显示类型都要求资源;FTP和XML-RPC两者都没有这样的独立的显示组件。 然后,视图和资源通过层来分组,层通常的被用来分组相似的外观和感觉。在Zope2的CMF 框架,层是被包含在门户皮肤工具里面的文件夹。一个层的例子是debug,它简单插入Python的tracebacks到异常的HTML页面中。 多重的层可以被堆叠为皮肤。当前我们在Zope3核心中有几个皮肤:"rotterdam(默认)","Basic ","Debug "和"ZopeTop(在3.0分发版中被剔除)"。皮肤可以简单通过在你的根URL的后面键入++skin ++SKINNAME来改变,例如: http://localhost:8080/++skin++ZopeTop/folder1 当你开发一个最终用户网站,你明确的想创建你们自己的层并且合并它为新皮肤。你想避免为你的站点编写视图并分别执行此外观和感觉。你可以使用皮肤来创建一个外观,它将可用于所有新的和已经存在的模板。 全局vs局部 Zope3有意识的隔离本地和全局的组件。全局的组件没有关联它们的地方,并且因此它们将永远可以被达到和表现。 他们永远在Zope3启动的时候经由Zope配置标记语言(ZCML)来初始化和注册,就像我之前提到的那样。因此,全局的组件不是持久的,这意味着他们根本不是存储在ZODB里。 他们的状态在服务器关闭的时候被销毁(也应该被销毁)。 另一方面,本地组件是存储在ZODB里,位置在他们被定义的对象树里面。本地组件永远仅仅增加和重写先前的设置;他们永远不能删除现有的设置。可以利用点击Web界面上的"Manage Site"来创建新本地组件。这将领引你进入到配置名字空间之内并且永远使用URL里的++etc++site来标记。 每个文件夹都可以被晋升为一个站点。 当通过点击"Make Site"来声明一个文件夹为一个本地站点管理器时,我们称这个对象/文件夹为一个站点。 象之前提到的那样,本地组件当查询信息时使用显式的采集处理。 例如,我想为SimpleExample得到工厂。当查询组件时,发布所使用的原始站点将被选中。 然而,有时要求从一个不同的站点来启动查询组件。在这种情况下,你简单在调用里指定上下文即可。 1 factory = zapi.getFactory('example.SimpleExample', context=other_site) 如果一个本地Utility Service存在并且找到一个名称为example.SimpleExample的IFactory实用工具,那么它将被返回。否则,本地Utility Service委派这个请求到下一个站点。请求可以一直被委派到全局的Utility Service,在那里必须给出一个回答。如果全局的Utility Service也不知道答案,那么一个ComponentLookupError将被抛出。 我们可以看见在一个服务的全局和本地实现之间有轻微语义上的差异,除了在数据存储和可访问性上的差异以外。全局服务从不担心请求的地方或者委派。净效应是全局的组件相对于他们的同等本地组件来说经常更加容易的实现。此外本地组件除了可读的APIs之外通常有可写的APIs,因为他们必须提供运行时的管理。