本节关注设计问题中的程序模型部分.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")(
这个listener从listener基类继承了这个方法).
Listener
组件的方法可以在句柄中调用,如下:
<notify listener="listenerName" method="someMethod" resultKey="someVariable" />
这是用当前事件来调用someMethod()方法,并将值存放在someVarisable返回,例如request.result.这个resultKey属性是可选的(当方法的returntype=”void”时无需定义它).
虽然listener可以通过announceEvent()
通告附加事件加入队列(并且在当前事件触发后的某个时候执行它们),但是listener值可以访问当前事件.一个典型的listener方法的任务是用来对一个和多个事务模型对象调用一个和多个方法.这个事务模型对象可以创建成闲置状态的,可以在session域里管理它或者在listener的configure()里创建它(并存放在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应该是无归属的—与任何数据实例无关—除非你为某种原因的确要缓存数据,例如保存变量值到变量域来保存请求里对变量的动态访问结果.因此,在使用var在listener声明本地变量时要格外小心,避免将某些东西存放到未命名的域里.同样要注意像<cfquery>
这类生成变量的标签,所以你应该以如下方式定义变量:
<cfset var userSelect = 0 />
<cfquery name="userSelect" ..>
...
</cfquery>
Session Façade
上面已经提到了,如果你需要管理per-session数据,你需要使用Session Façade这种设计模式.它的工作原理是只有你的 listener是session-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
等变量的基础上.这是很难看的做法.
对象传递模式就是用来解决这个问题的,它将数据聚集起来,在model与view之间传递.在以上的例子里,你需要单一的getPerson()方法来返回一个结构体(内含所有必要数据),这样view值依赖于resultKey.如果你希望进一步封装你的传递对象,可以使用bean—一个简单的CF组件,其中包含获取器(getter)和设置器(setter)以及构造器(init()).listener可以用它向传递对象放入所有数据然后返回给Mach-II.
Bean和表单触发器(Beans & Form Handling)
上节提到,bean是一个包含用以封装数据(变量)的getter和setter的简单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(例如set
Field1(event.getArg("
field1"))
, set
Field2(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 Edwards和Hal 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下载.