作者: Ben Edwards
1。II体系下的Listener
在实际开发过程中,软件体系通常被加工为组件(component)和连接器(connector)的集合。组件是系统的功能元素。比方说,一个购物车,一个通讯录,或者一个数据库都可以是软件体系的组件。连接器是用于组件间通讯的协议,包括方法的调用,SQL查询和HTTP请求。系统所选择的体系决定了组件和连接器的词汇表(vocabulary),一种定义如何组合二者的约束的集合。
对定义体系中的组件和连接器外露界面的这一事项来说,很重要的风格特征(metrics)就是系统的内聚和耦合。内聚是组件用途单一性的度量标准。组件的内聚性越高,它的功能就越专注,复用所承担的假设就越少。
耦合性可用来度量组件间依赖程度。组件间依赖程度越低(它们的联系越松散),它就越独立、复用性也越好。最大化的内聚和最小化的耦合是灵活、易维护体系结构的两大特点。
基于事件,II(implicit invocation××)是具有高内聚和低耦合的精工体系的范例。因此,它被软件工程普遍接受。II体系的例子随处可见,事实上包括所有现代的操作系统、完整的开发环境和数据库管理系统等。
II系统由事件驱动。事件在系统需要做某事时被触发——比如像对引入请求的响应。事件在不同类型的实现中采取不同形式;对于Mach-II,事件是属性值里包含运行该事件所需上下文信息的对象(类似HTTP请求携带它所有表单和查询语句变量的方式)。
当一个事件被通告,系统就开始为该事件查找listener组件。Listener符合我们刚刚已经讨论过的组件创建标准——它们是系统的功能模块。请求作为listener的组件在配置时段里被(XML文件)注册,进而在运行时由确定的事件通告。当事件被触发,所有注册的该事件关联的listener经由动态确定(dynamically-determined)方法调用的手段,传递给该事件。通过这种方式,功能被间接地调用。事件通告(notify)listener的这种处理方式被称作事件宣告(event announcement)。
2。面向对象的框架下的listener
Listener在Mach-II中有三个主要的任务:
×Listener组件包含Mach-II应用程序的事务逻辑;
×Listener由与其由注册关系的事件通报;
×Listener可以宣告(announce)新的事件。
在用于ColdFusion的Mach-II中,框架组件,包括listener,被以ColdFusion组件(CFCs)的形式创建。所有为Mach-II应用程序开发的listener皆继承于Listener.cfc(MachII.framework.Listener),后者包含在Mach-II框架内核代码中。
Listener具有一些重要的功能,和三个对开发人员创建自己的listener相当重要的方法:
×announceEvent(string eventName.[struct eventArgs])——用于向框架通告一个新事件;
×configure()——在初始化组件后调用,来重写进而确定构造器(configuration)逻辑;
×getParameter( string paramName )——用于访问在XML配置文件中制定的配置参数。
在CFML组件浏览器查看Mach-II.framework.Listener组件,或许对你理解listener有所帮助。进入http://localhost:8500/cfide/componentutils/componentdoc.cfm,然后点击MachII.framework包。点击Listener组件查看该组件的具体信息。
3。创建Mach-II应用程序
在我们开始创建listener之前,让我们先创建一个Mach-II应用程序来测试即将创建的listener。我们需要使用从www.mach-ii.com下载来的MachAppSkeleton文件创建我们的应用程序。
MachAppSkeleton文件是创建新的Mach-II程序的模板,其中包含MachAppSkeleton-Readme.txt这个指导文件。我们将摘取其中的要点来创建程序和测试listener。
我们将要创建的程序可以起名为“MachII_HowTo_Listeners”。将MachAppSkeleton文件夹里面的内容全部拷到你的CF wwwroot下的名为MachII_HowTo_Listeners的文件夹里。
新的文件夹看上去像一下所示:
下一步,打开程序根目录下的Application.cfm文件,设置<cfapplication>标签的name属性值为“MachII_HowTo_Listeners”。
接着,打开mach-ii.xml。该XML文件的的第一部分定义程序的参数。设置参数applicationRoot为你的程序文件夹的路径,相对于网站根目录并以“/”开头。对于这个程序,applicationRoot的值应为“MachII_HowTo_Listeners”。
然后,设置参数defaultEvent值为“showLoginForm”。这是学定我们的程序的默认事件,它不需要用event-handler声明。
目前为止,mach-ii.xml配置文件的<properties>部分看上去像以下所示:
<!--- PROPERTIES --->
<properties>
<property name="applicationRoot" value="/MachII_HowTo_Listeners" />
<property name="defaultEvent" value="showLoginForm" />
<property name="eventParameter" value="event" />
<property name="parameterPrecedence" value="form" />
<property name="maxEvents" value="10" />
<property name="exceptionEvent" value="exceptionEvent" />
</properties>
接下来,注册两个<page-view>:一个是为loginForm.cfm,另一个是为loginWelcome.cfm。loginForm.cfm由一个提交username和password作为login事件(我们尚未为其声明event-handler)的form组成。loginWelcome.cfm则由简单的问候语和一个通告showLoginForm事件的链接构成。这两个页面的代码可以在文章结尾得到。
目前为止,mach-ii.xml配置文件的<page-views>部分看上去像以下所示:
<!-- PAGE-VIEWS -->
<page-views>
<page-view name="loginForm" page="/views/loginForm.cfm" />
<page-view name="loginWelcome" page="/views/loginWelcome.cfm" />
<page-view name="exception" page="/views/exception.cfm" />
</page-views>
最后,为我们的默认事件showLoginForm加<event-handler>。联络公共事件触发器(event-handler
public),使得它可以由网络请求(web-request)通告。当下的这个event-handler唯一需要做的事就是显示loginForm这个page-view。
目前为止,mach-ii.xml配置文件的<event-handler>部分看上去像以下所示:
<!-- EVENT-HANDLERS -->
<event-handlers>
<event-handler event="showLoginForm" access="public">
<view-page name="loginForm" />
</event-handler>
<event-handler event="exceptionEvent" access="private">
<view-page name="exception" />
</event-handler>
</event-handlers>
现在我们可以打开http://localhost:8500/MachII_HowTo_Listeners,并可以看到登录表单。因为网址中没有包含用以确定某个事件的参数,框架将触发默认事件(showLoginForm),即显示loginForm.cfm页。提交这个登录表单不会产生任何反应,因为我们还没有为这个登录事件确定event-handler。
5。创建新的listener组件
首先,定义一个名为LoginListener的CF组件(LoginListener.cfc),来做基于用户名和密码的登录权限鉴别。文件创建在程序的model目录下(推荐,但非必须)。同时,确保LoginListener扩展(extend)了MachII.framework.Listener,这样它就可以继承(inherits)listener组件原型的所有功能了。
<cfcomponent displayname=”LoginListener”
extends=”MachII.framework.Listener”
hint=”Authenticates a user’s login credentials based on username and
password.”>
</cfcomponent>
,接着,我们来的定义configure函数,目前先不定义它的函数体。在定义方法时,我们其实已经重写了Listener.cfc组件原型,因此,必须保留函数的argument和各种属性(如access,returntype等等)。
在学习到configure()函数更多内容前,我们的代码会像这样:
<cffunction name=”configure” access=”public” returntype=”void”>
<!--- DO NOTHING --->
</cffunction>
注意,不要为Mach-II的组件(如Listener)定义我们自己的init函数。它们已经在框架核心文件中被定义好了,且不可以重写。换句话说,定义在组件的Configure()方法是专门为自定义组件重写设计的。这些configure()函数在紧跟在框架初始化(initialization)阶段的框架配置(configuration)阶段被调用。
注册的listener实例在框架初始化的时候被创建和初始化。只有到框架被重新初始化的时候,同样的listener实例才会被维护和使用。关于初始化和配置阶段的具体信息超出了本文范畴,目前关键要明白,configure()函数是在所有的事件被触发前,声明listener需要运行的配置代码的。
最初的这几步,是我们每次创建新的listener的时候都要重复的。先是,声明listener组件,让它扩展MachII.framework.Listener,然后定义configure()函数来支持某些配置上的逻辑。
6。定义listener的函数
现在,我们来创建事务逻辑。现在我们要定义的LoginListener是负责查看用户名和密码,并鉴别用户权限的。
建立一个名为“attemptLogin”的函数来支持鉴定逻辑。注意,函数的access必须为declare,这样在能是方法被Mach-II框架调用。定义返回值为“void”(就是说,该函数将不返回任何值)。
你会问,如果attemptLogin()函数不返回值,我们该如何知道登录的尝试是否成功呢?要知道,Mach-II框架是有事件驱动的,listener可以通告事件来执行任务。所以,LoginListener会以公告事件的方式,来宣布登录成功与否。
对于事件的通讯,我们需要需要让attemptLogin()函数可以接受叫做“event”的变量。这个event是必须的,且类型为MachII.framework.Event。
目前的attemptLogin()函数看上去,像这样:
<cffunction name=”attemptLogin” access=”public” returntype=”void”>
<cfargument name=”event” type=”MachII.framework.Event” required=”true” />
<!--- TODO: GET USER INFO --->
<!--- TODO: AUTHENTICATE USER INFO --->
<!--- TODO: ANNOUNCE SUCCESS/FAILURE EVENT --->
</cffunction>
在被框架调用时,所有用来做登录尝试的用户信息将被封装事件对象当中。取得这个事件里的用户名和密码,要使用事件的getArg()函数:
<cffunction name=”attemptLogin” access=”public” returntype=”void”>
<cfargument name=”event” type=”MachII.framework.Event” required=”true” />
<!--- GET USER INFO --->
<cfset var username = arguments.event.getArg(‘username’) />
<cfset var password = arguments.event.getArg(‘password’) />
<!--- TODO: AUTHENTICATE USER INFO --->
<!--- TODO: ANNOUNCE SUCCESS/FAILURE EVENT --->
</cffunction>
一旦得到用户信息,我们就需要判断它们是否有效。由于潜在的判断方法有很多种(比如database,LDAP等等),我们将把这个功能分配给另一个叫做“isLoginValid”的函数来实现。这种做法对保持attemptLogin()函数的内聚性很有帮助!
将isLoginValid函数的访问域设为public,返回值设为boolean。它还需要变量username和password(类型都是string)。现在姑且设置函数的返回值为true,以后我们会回过头来完善它的。
<cffunction name=”isLoginValid” access=”public” returntype=”boolean”>
<cfargument name=”username” type=”string” required=”true” />
<cfargument name=”password” type=”string” required=”true” />
<cfreturn true />
</cffunction>
7。在listener中通告事件。
有了isLoginValid(),我们就可以完成attemptLogin()了。将我们从事件中得到的用户名和密码值传递给isLoginValid(),看看是否有效。如果有效,我们将通告“loginSuccess”事件,否则通告“loginFailure”事件。
为了通告事件,我们需要使用Listener.cfc原型中定义的announceEvent()函数。该函数将取得两个变量,第一个是eventName,是必要条件且必须是字符类型的。它定义了要通告的事件的名称。第二个是eventArg,是可选条件,不过需要的话,必须为结构体类型。它定义了将封装到新事件内的变量。
完成后的attemptLogin()如下:
<cffunction name=”attemptLogin” access=”public” returntype=”void”>
<cfargument name=”event” type=”MachII.framework.Event” required=”true” />
<!--- GET USER INFO --->
<cfset var username = arguments.event.getArg(‘username’) />
<cfset var password = arguments.event.getArg(‘password’) />
<!--- AUTHENTICATE USER INFO --->
<!--- ANNOUNCE SUCCESS/FAILURE EVENT --->
<cfif isLoginValid(username, password)>
<cfset announceEvent(‘loginSuccess’) />
<cfelse>
<cfset announceEvent(‘loginFailure’) />
</cfif>
</cffunction>
如果需要将用户登录信息放入loginSuccess和loginFailure事件的话,可以使用下面的代码:
<cfset announceEvent(‘loginSuccess’, arguments.event.getArgs()) />
这个代码可以将传递给attemptLogin()的所有变量,存入新的事件。
8.注册和配置listener。
我们现在已经得到开发好了的LoginListener,需要把它注册到mach-ii.xml配置文件中。
<listeners>部分的代码如下:
<!-- LISTENERS -->
<listeners>
<listener name="LoginListener"
type="MachII_HowTo_Listeners.model.LoginListener">
<invoker type="MachII.framework.invokers.CFCInvoker_Event" />
</listener>
</listeners>
Listener定义里的名称属性可以是任意值,它只是用来调用XML里的listener的标志符。Type属性则必须是通向该组件的完整路径。
必须注意的是那个<invoker>标签,invoker type值用来确定事件信息由何种方式传递给被调用的listener的。以后将进一步研究invoker,而现在要确保invoker type为“MachII.framework.invokers.CFCInvoker_Event”
我们还将为新的listener函数分扩展提供event-handler,另外还有为登录事件(包括loginSuccess和loginFailure。
登录事件句柄(event-handler)会通报LoginListener的attemptLogin()函数。LoginSeccess事件句柄将简单地显示loginWelcome。loginFailure事件句柄将通过showLoginForm事件再次显示loginForm。这两个句柄的访问域值设置为private。
将这些事件句柄加入<event-handlers>部分:
<!-- Notify the listener to validate login. -->
<event-handler event=”login” access=”public”>
<notify listener=”LoginListener” method=”attemptLogin” />
</event-handler>
<!-- On login success, show a welcome page. -->
<event-handler event=”loginSuccess” access=”private”>
<view-page name=”loginWelcome” />
</event-handler>
<!-- On login failure, show the login form. -->
<event-handler event=”loginFailure” access=”private”>
<announce event=”showLoginForm” />
</event-handler>
此时,我们需要回到LoginListener的<listener>元素上,再添加几个配置参数。这些参数在XML文件中确定下来,可以由listener的getParameter()函数访问。
在实际应用中,用户名和密码一般会通过数据库和LDAP服务来检验。在我们这个例子里,我们将添加两个参数:validUsername和validPassword。显然,它们表示有效的用户信息,值可以随便设置,在本例,我们设置为“implicit”和“invocation”。
将参数加入<listener>的<parameters>里:
<listener name="LoginListener"
type="MachII_HowTo_Listeners.model.LoginListener">
<invoker type="MachII.framework.invokers.CFCInvoker_Event" />
<parameters>
<parameter name="validUsername" value="implicit" />
<parameter name="validPassword" value="invocation" />
</parameters>
</listener>
现在我们要完成isValidLogin()函数,让它来核对用户提交的信息和配置文件中设置的validUsername和validPassword。用继承于listener原型的getParament函数,访问配置参数:
<cffunction name=”isLoginValid” access=”public” returntype=”boolean”>
<cfargument name=”username” type=”string” required=”true” />
<cfargument name=”password” type=”string” required=”true” />
<!--- AUTHENTICATE AGAINST PARAMETERS IN CONFIG XML --->
<cfif arguments.username EQ getParameter(‘validUsername’) AND
arguments.password EQ getParameter(‘validPassword’)>
<cfreturn true />
<cfelse>
<cfreturn false />
</cfif>
</cffunction>
使用参数除了可以确定有效用户信息外,还可能用来确定联系LDAP服务器和数据库的方法。
访问http://localhost:8500/MachII_HowTo_Listeners,提交表单,表单对Mach-II框架的请求将触发带有变量username和password、名为login的事件。登录事件句柄将通告LoginListener,并向它的attemptLogin方法传递该事件。通过事件的信息,listener会执行事务逻辑,并通报新的事件——loginSuccess或者loginFailure。
10.调用listener。
在我们提交登录表单给Macf-II框架的时候,后者做了很多工作来确保登录事件已经传递到我们的LoginListener上。其中的关键组件,叫做invoker。
Invoker是用来动态调用listener方法的。在注册listener给XML配置文件的同时,我们也为listener确定了一个invoker。框架通过这个invoker调用listener的方法来触发事件。
和listener一样,所有Mach-II的invoker都是被创建成CFC格式。自定义的invoker都继承了包含在Mach-II框架核心代码包里的ListenerInvocker.cfc原型(MachII.framework.ListenerInvoker)。
在MachII.framework.invokers包里含有两种类型的invoker——CFCInvoker_Event and
CFCInvoker_EventArgs。
名为CFCInvoker_Event的invoker为listener方法传递实体事件。因此,该方式调用的listener的方法需要类型为MachII.framework.Event、名为“event”的变量。
<invoker type="MachII.framework.invokers.CFCInvoker_Event" />
<cffunction name=”attemptLogin” access=”public” returntype=”void”>
<cfargument name=”event” type=”MachII.framework.Event” required=”true” />
. . .
</cffunction>
名为CFCInvoker_EventArgs的invoker需要将事件的变量与listener的方法需要的变量一一对应,并逐个传递。比方说,一个事件带有变量username和‘password,而被调用的listener的函数中存在名为“username”和“password”的两个变量,这样事件变量将以同名称传递给被调用的函数。
<invoker type="MachII.framework.invokers.CFCInvoker_EventArgs" />
<cffunction name=”attemptLogin” access=”public” returntype=”void”>
<cfargument name=”username” type=”string” required=”true” />
<cfargument name=”password” type=”string” required=”true” />
. . .
</cffunction>
<event-handler>标签里的<notify>元素指示框架去使用listener的invoker调用具体的方法。属性Listener的值应是已经注册的listener的名称。属性mathed的值指定了listener里将被调用的方法名。可选属性resultKey(本例没有用到)可用来储存特定范围的返回值。
我们将LoginListener的invoker类型设CFCInvoker_Event。attemptLogin()函数接受名为“event”的单一变量。如果我们是用CFCInvoker_EventArgs类型的invoker,那么attemptLogin函数就必须含有两个变量——username和password,来匹配事件中的同名变量。
11.总结与回顾。
Listener在Mach-II中有三个主要的任务:
×Listener组件包含Mach-II应用程序的事务逻辑;
×Listener由与其由注册关系的事件通报;
×Listener可以宣告(announce)新的事件。
在这篇文章中,我们已经学习了:
×怎样创建Mach-II应用程序;
×怎样创建Listener组件;
×如何在Mach-II配置文件中注册和配置listener;
×怎样为listener制定invoker;
×怎样创建event-handler,来使用listener履行事务逻辑;
×如何用listener通告event。
12.术语和定义。
内聚性(cohesion):用来度量组件(和方法)用途专一程度;
耦合性(coupling):用来度量组件间独立程度;
封装(encapsulation):使用类或组件来集中放置方法及其需要响应的变量;
继承(inheritence):履行和某个类的特定关系,来继承指定的方法(××);
父型(或者译为超类,supertype):将被其它类扩展的父类;
重写(或者译为重构,overriding):一种沿用父类的方法名,但进一步确定功能的方法。
13.相关资源。
以下所有资源可以在www.mach-ii.com得到:
×本例的代码(MachII_HowTo_Listeners.zip);
×MachAppSkeleton代码;
×Mach-II配置指南;
×Ben Edwards的介绍II体系的文章;
×更多Mach-II教程。