Mach-II DevGuide 系列教程译文: 模型的设计

 

 

本节关注设计问题中的程序模型部分.model包含了所有事务逻辑,并作为ColdFusion组件集合被实现.

 

事务逻辑与Mach-II的分离

 

Mach-II使用继承MachII.framework.Listener的组件与你的应用程序的Model部分相结合.在简单程序里,所有事务逻辑被实现在这种listener,但是这种途径不能够缩放自如来应付程序复杂性的增加.这是因为在其他的事物之间,它导致了事务逻辑与框架之间的高耦合性.如果我参考前几章的指导来设计你的事务组件的话,会清楚发现这些组件中只有很少一部分使用了原始的listener.很可能,你的几本事务组件中根本没有原始的listener.这绝对没问题!

 

     你能更好地确保框架和你的程序之间只用很少的耦合如果可能的话,创建继承于Mach-II listener组件的新组件吧.它们在你的事务模型中沟通事件的用途比较单一.换句话说,试着将你的事务组件尽可能地隔离出框架组件:只有listener组件知道框架;只有listener组件可以访问Mach-II变量;只有listener组件可以通告事件.核心事务组件可以不知道Mach-II框架并完全不依赖于框架.

 

Listener的剖析

 

使用CFCInvoker_Event类型invoker的最简单的listener组件如下:

 

          <cfcomponent extends="MachII.framework.Listener">
   
   
                <cffunction name="configure" returntype="void" access="public" output="false">
   
   
                              <!--- perform any initialization --->
   
   
                    </cffunction>
   
   
                <cffunction name="someMethod" returntype="someType" access="public" output="false">
   
   
                          <cfargument name="event" type="MachII.framework.Event" required="yes" />
   
   
                              <!--- perform some task --->
   
   
                              <cfreturn someValue />
   
   
                    </cffunction>
   
   
          </cfcomponent>
   
   

 

方法someMethod可以被mach-ii.xml里的事件句柄使用<notify>命令来调用.返回值someValue的类型必须是someType.注意如果方法不需要返回renhe  ,someType可以是void—它可以不包含<cfreturn/>标签,也可以在<cfreturn/>里不定义任何异常.

        

         mach-ii.xml定义Listener的方法如下:

          <listener name="listenerName" type="Path.To.YourListener">
   
   
                    <invoker type="MachII.framework.invokers.CFCInvoker_Event" />
   
   
          </listener>
   
   

 

<invoker>标签定义如何在listener里调用方法.一般情况下,你可以像上面所示定义为CFCInvoker_Event,这样使得当前的事件对象作为简单变量被传递给被调用的方法.你也可以定义它为CFCInvoker_EventArgs,这样会使当前的事件的变量逐个对应于方法的变量.更多信息可以参考后面的Invokers & Listeners那一节.

 

你可以为listener确定几个参数:

          <listener name="listenerName" type="Path.To.YourListener">
   
   
                    <invoker type="MachII.framework.invokers.CFCInvoker_Event" />
   
   
                    <parameters>
   
   
                              <parameter name="param1" value="value1" />
   
   
                              <parameter name="param2" value="value2" />
   
   
                    </parameters>
   
   
          </listener>
   
   

 

这样为param1 param2提供了默认参数值(分别是value1 value2).listener可以用getParameter()方法访问这些参数,例如: getParameter("param1")(这个listenerlistener基类继承了这个方法).

 

Listener组件的方法可以在句柄中调用,如下:

 

<notify listener="listenerName" method="someMethod" resultKey="someVariable" />

 

这是用当前事件来调用someMethod()方法,并将值存放在someVarisable返回,例如request.result.这个resultKey属性是可选的(当方法的returntype=”void”时无需定义它).

 

虽然listener可以通过announceEvent()通告附加事件加入队列(并且在当前事件触发后的某个时候执行它们),但是listener值可以访问当前事件.一个典型的listener方法的任务是用来对一个和多个事务模型对象调用一个和多个方法.这个事务模型对象可以创建成闲置状态的,可以在session域里管理它或者在listenerconfigure()里创建它(并存放在listener的变量域里).

 

更多关于编写listener的信息,可查阅Mahc-II教程<<如何开发listener>>(Ben Edwards).

 

调用器与监听器(Invokers & Listeners)

mach-ii.xml里定义一个listener,你需要确定invoker类型.Mach-II提供了两种默认类型:

  • CFCInvoker_EventArgs
  • CFCInvoker_Event

<notify>标签里的Invoker决定变量以何种形式被传递个listener方法,以及结果如何表现.如上面一节所说的, CFCInvoker_EventArgs使当前的事件的变量逐个对应于方法的变量. CFCInvoker_Event使当前的事件对象作为简单变量(类型为MachII.framework.Event)被传递给被调用的方法.这两种invoker都将结果存放在特定的变量,这意味着你需要使用一个request域的变量.某种程度上,用哪种类型的invoker是设计风格的事,但有一些实际的因素要考虑:

  • CFCInvoker_EventArgs 可能更加安全,因为你可以确定每个变量的类型(以及它们是否必要,如果不做定义,它也会有个默认的值),但是要让ColdFusion自己来验证来自你的URL和表单的数据是什么,这是很费力的. CFCInvoker_Event 提供了更加灵活的机制,方便你向你的listener方法添加扩展代码来访问和验证事件变量.
  • 总的来说,使用CFCInvoker_Event,并且考虑使用事件过滤器来提高其安全性是比较好的做法.

如果你选择了CFCInvoker_Event,你的listener可以以如下方式来传递简单变量:

         <cfargument name="event" type="MachII.framework.Event" required="true"/>
   
   

 可以提供isArgDefined()方法来验证给定的URL或表单变量:

          <cfif arguments.event.isArgDefined("anArg")>
   
   

                    ...

</cfif>

因为URL和表单变量是固定(HTTP有关),最好在代码中明确指定事件变量的类型,而不是使用CFCInvoker_EventArgs,进而为个别URL或表单变量声明方法变量(你不希望ColdFusion因为用户误输入而抛出异常吧!)

 

你可以为特殊用途自定义invoker,例如下面这个invoker,设置事件变量的resultKey等于属性值,将结果直接放入事件对象.:

<cfcomponent extends="MachII.framework.ListenerInvoker" output="false"
   
   
                    hint="I am a custom invoker" displayName="EventInvoker">
   
   
          <cffunction name="invokeListener" access="public" returntype="void" output="false"
   
   
                               hint="I implement the method invocation for a listener">
   
   
                    <cfargument name="event" type="MachII.framework.Event" required="true" 
   
   
                                         hint="I am the current event" />
   
   
                    <cfargument name="listener" type="MachII.framework.Listener" required="true" 
   
   
                                         hint="I am the listener that is being notified" />
   
   
                    <cfargument name="method" type="string" required="true" 
   
   
                                         hint="I am the method that is being invoked" />
   
   
                    <cfargument name="resultKey" type="string" default="" 
   
   
                                         hint="I name the optional event argument in which the result is stored" />
   
   
                    <cfset var resultVar = 0 />
   
   
                    <cftry>
   
   
                               <cfinvoke component="#arguments.listener#" 
   
   
                                         method="#arguments.method#" 
   
   
                                         event="#arguments.event#" 
   
   
                                         returnVariable="resultVar" />
   
   
                               <cfif arguments.resultKey is not "">
   
   
                                         <cfset arguments.event.setArg(arguments.resultKey,resultVar) />
   
   
                               </cfif>
   
   
                    <cfcatch type="Any">
   
   
                               <cfrethrow />
   
   
                    </cfcatch>
   
   
                    </cftry>
   
   
          </cffunction>
   
   
</cfcomponent>
   
   

 

实例数据与监听器(Instance Data & Listeners)

因为Mach-II将所有框架组件实例载入application,你需要记住哪些你创建在listener里的实例数据被有效地储存在application.这要考虑以下3个问题:

l          优点:你可以通过将数据以实例形式存入listener(利用变量域)的办法,很容易地为你的程序缓存数据

l          缺点:更新实例数据会影响所有进程,需要适当地使用锁定:在变量域数据上对一些更新用<cflock type="exclusive" name="..."> .. </cflock>保护.

l          缺点:要管理per-session数据的话,你需要执行类似与Session Façade(参考下节).

总之,你的listener应该是无归属的与任何数据实例无关除非你为某种原因的确要缓存数据,例如保存变量值到变量域来保存请求里对变量的动态访问结果.因此,在使用varlistener声明本地变量时要格外小心,避免将某些东西存放到未命名的域里.同样要注意像<cfquery>这类生成变量的标签,所以你应该以如下方式定义变量:

          <cfset var userSelect = 0 />
   
   

          <cfquery name="userSelect" ..>

                    ...

</cfquery>

 

Session Façade

上面已经提到了,如果你需要管理per-session数据,你需要使用Session Façade这种设计模式.它的工作原理是只有你的 listenersession-aware,也就是说,它知道session,并且管理session域里的组件实例,但那些per-session的组件不是listener本身(它们不是继承MachII.framework.Listener),并且它们是不涉及session域的.例如,购物车listener响应'addItem', 'removeItem' ,以及'updateQuantity'等事件,但不代表对存放在session域的购物车对象的动作.购物车listener会应需求在session域创建一个购物车对象.这个购物车对象将以数据实例(因为购物车对象是per-session,所以它也是per-session)的形式存放信息,但不涉足session.

<cfcomponent extends="MachII.framework.Listener" ..>
   
   

  
  
   
    
  
  
          <cffunction name="addItem" ..>
   
   
                    <cfargument name="item" ../>
   
   
                    <cfset getCart().addItem(arguments.item) />
   
   
          </cffunction>
   
   

  
  
   
    
  
  
          ...
   
   

  
  
   
    
  
  
          <cffunction name="getCart" returntype="Cart" access="private" ..>
   
   
                    <cfif not structKeyExists(session,"cart")>
   
   
                               <cfset session.cart = createObject("component","Cart").init() />
   
   
                    </cfif>
   
   
                    <cfreturn session.cart />
   
   
          </cffunction>
   
   

  
  
   
    
  
  
</cfcomponent>
   
   

 

上面的例子示范了Session Façade技术,但没有将它完善(例如,这里listener没有configure()方法,没有hint= 或者output= 属性,他没有在创建购物车对象时没有锁定session域等等).

对象的传递

Mach-II里基本的事件周期是:

l          收到事件

l          通告需要返回数据(resultKey)listener

l          view递交数据

如果你需要多于一页的数据呈现在你的页面,你就需要多次通知listener,创建多个resultKey,并持有建立在多个请求域变量基础上的view.步骤似乎很明显,而效果呢?

l          listener方法的联合调用(性能可能会有影响)

l          再细微的界面都需要向listener发送请求(这就产生了非常多的低层次的获取器(getter))

l          View与输入变量之间的依赖性变复杂了(非常多的请求域变量被请求)

 

这些状况亮红灯了尤其是后两个,会严重破坏封装性,并提高耦合性.你希望你的系统出现这种状况吗?一个很好的例子是用来显示个人信息的view:这个view可能需要显示姓名,街道地址,城市,省份以及邮政编码等等.很明显,幼稚的做法是,需要为此在listener(在事件句柄调用每一个listener)设置很多的getter方法,并将view建立在request.firstName, request.lastName, request.streetAddress等变量的基础上.这是很难看的做法.

 

对象传递模式就是用来解决这个问题的,它将数据聚集起来,modelview之间传递.在以上的例子里,你需要单一的getPerson()方法来返回一个结构体(内含所有必要数据),这样view值依赖于resultKey.如果你希望进一步封装你的传递对象,可以使用bean—一个简单的CF组件,其中包含获取器(getter)和设置器(setter)以及构造器(init()).listener可以用它向传递对象放入所有数据然后返回给Mach-II.

Bean和表单触发器(Beans & Form Handling)

上节提到,bean是一个包含用以封装数据(变量)gettersetter的简单CF组件.bean一般被用来作为传递对象在程序不同层次传递数据.如果一个bean含有一个变量foo(私有的实例变量),并拥有getFoo()setFoo()来进行获得和设置foo值的操作.getFoo()方法需要是公有的,setFoo()则既可以公有也可以是私有的,这要看bean是只读的还是可读写的.这里有一个只读的bean:

<cfcomponent>

          <!--- "declare" properties for clarity: --->

          <cfset variables.foo = "" />

          <!--- constructor: --->

          <cffunction name="init" returntype="FooBean" access="public" output="false">

                    <cfargument name="foo" type="string" default="" />

                    <cfset setFoo(arguments.foo) />

                    <cfreturn this />

          </cffunction>

          <!--- public getters: --->

          <cffunction name="getFoo" returntype="string" access="public" output="false">

                    <cfreturn variables.foo />

          </cffunction>

          <!--- private setters: --->

          <cffunction name="setFoo" returntype="void" access="private" output="false">

                    <cfargument name="foo" type="string" required="yes" />

                    <cfset variables.foo = arguments.foo />

          </cffunction>

</cfcomponent>.

 

可读写的bean的不同之处就是setter为公有.构造器为每个变量设置了一个可选项,调用setXxx()为每个xxx变量.

Mach-II支持bean的创建和布局,用以下<event-bean>命令:

         <event-bean name="beanName" type="beanType" fields="field1,field2" />

这个命令创建了一个确定类型(beanType, 例如 my.model.FooBean)bean,并将它作为名为beanName (例如fooBean或者直接就叫foo)的事件变量存放在当前事件对象.

l          如果fields=被定义了,那么命令将调用构造器(init()),期间不带任何变量,而是对每个fields列表里的field调用setter(例如setField1(event.getArg("field1")), setField2(event.getArg("field2"))).

l          如果fields=被忽略,那么命令将一种变量名称逐个调用当前事件变量(例如., init(field1=event.getArg("field1"), field2=event.getArg("field2"))).

 

这样使得在Mach-II里处理表单提交更加容易:定义一个bean组件来呈现表单提供的数据,使用<event-bean>命令将表单数据组装进去.然后,你可以像封装好的bean一样操作这些提交的数据,执行检验(见事件过滤器一节)等操作.

关于便携和使用bean的更多信息可以参考Mach-II教程Beans, Beans, the Musical Fruit(Ben EdwardsHal Helms).

 

数据库访问对象(Database Access Objects/DAO)

Mach-II框架没有直接设计这部分知识,大多数程序需要实现数据访问(通常是数据库),也有很多人问及这类问题.

大多数程序有两种模式来访问固定数据:

  • 聚合访问(aggregated access) – 多行数据的报告,搜索以及列表
  • 逐个访问(per-object access) - 单行(单个对象)的创建,编辑以及运行

ColdFusion有很强大的内建语句来处理第一种访问方式—query语句它能够很有效的处理来自数据库(或者其他数据源,因为你可以很其轻松地创建一个query对象,并组装进自己的数据)的多行数据集.当你处理聚合访问时,很难将每行数据返回给完全封装的对象(CF组件实例).

另一方面,当你专注于逐个访问时,很容易操作完全封装对象.这也是需要使用标准的CRUD(Create, Read, Update, Delete)可选项的原因.

如果认同这两个基本模式,你将通过为各个模式提供单独的组件的方式,来设计你的组件.通过例子可以很好地说明这一点:

l          如果我们有一个叫做Order的事件模型对象,,我们会提供一个OrderGateway组件用于接收访问以及一个OrderDAO组件用于逐个访问(或者建立逐个访问到Order对象不过看下面).

l          OrderGateway组件可以提供类似findAll(),findWhere(),,findByID()的方法,并且返回标准的query对象(甚至findByID()也会返回一个单行数据).

l          OrderDAO组件提供了类似store(), load(), update(), delete() CRUD方法,用来通过在Order组件的getter/setter或者某种Order数据的快照上的数据交换,来操作特定Order对象.例如,一个bean:Order组件实现类似getSnaoshot()的方法返回一个bean,setSnapshot()将一个bean看作一个变量这个bean包含了用于Order对象的稳定核心数据.有时候,事务对象与数据访问对象之间因为性能原因(或者因为程序可以信任组件交换低水平封装的数据,像结构体或者某些不透明的数据结构等)需要更加直接的数据传递.这些最优化的只是已经超出本文档的知识范围.

 

将这些操作从事务模型分离,有助于保持其中立性.通道(gateway)组件可以被优化来得到大型的数据集,缓存等.DAO组件可以被优化来用于复杂数据更新,建立对象访问池等等.重复一句,有关优化方法的知识已经超越本文档的范畴,不过这两种截然不同的数据访问模式将让你起步于正确的道路.

如上面讲的,如果愿意的话,你可以在你的事务模型对象上直接执行CRUD方法,不过推荐你使用分离的DAO组件.在事务模型对象执行CRUD的好处是:

l          需要明了的组件较少(因为你没有分离的DAO组件)

l          不需要运行数据传递装置(因为CRUD方法在你的事务模型对象里直接访问数据

缺点:

l          事务逻辑里的SQL容易混乱(比如,同种组件里,破坏了持续层的封装性)

l          事务模型将变得臃肿(过于复杂而降低了内聚性)

l          很难交换持续层装置来使用不同的数据源(因为它与事务模型对象有关联)—分离的DAO层允许你一边可以用一种方式持续某个事务模型,一边又可以用其它方式持续其它的.

 

如你所想,以高内聚性和低耦合性为标志的好的封装性,促使你从事务模型组件分离出通道和DAO组件.

有关的例子可以到mach-ii.info下载.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值