1. 设计背景
JavaScript动作迁移器来源于这样的环境:某个操作因为代码复用和增强其内聚性而被分为几个单独的动作(函数),这些动作中在逻辑上存在先后关系,即前一个动作完成后才能继续下一个动作,但是某些动作却是异步的(asynchronous),这样在编码过程中就不能按过程调用的方式来编写,必须将异步动作的下一个动作放到该异步动作中,这种调用方式,在代码量很小,其逻辑不是很复杂的情况下,还是可以的,但是当异步动作中代码量较大且逻辑复杂的情况下,会增加函数的耦合度,降低代码的阅读性,加大代码维护的难度。
为了减少代码的耦合,是逻辑更加清晰,于是设计产生了动作迁移器。使用动作迁移器,所有的动作都将在操作开始前进行定义,操作开始后,就将按照定义的动作序列调用各个动作。并且,每个动作可按照其运行的状态调用其他不同的动作或是内部不同的状态。
2. 术语解释
首先,解释一下该动作迁移器所涉及到的术语的含义:
动作迁移器(ActionTransfer),维护的是一系列动作的执行路线,使动作能够按照指定的迁移路径运行下去。
动作(Action),指的是一个确定的完整的执行过程,而不是执行片段,如判断是否为真之类的,该动作完成后没有后续的与该操作处理相关的过程,但其可以有执行后得到的数据,该数据可以被后续动作继续使用。
迁移路径(TransferPath),即一个动作到下一个动作的运行线路,它规定了某个动作执行完后,其后续所要执行的动作,或是该动作的完结。每个动作都可以有执行状态,可以从动作的某个状态迁移到另一个动作,或另一个动作的某个状态,也可以是某个动作的不同状态间的转移。
动作状态(简称状态,Status),即某个动作运行完毕后,可以通过状态指示其执行是成功还是失败或是出现异常等,动作迁移器可以根据其执行状态,沿着迁移路径继续进行处理。
动作的迁移可以是没有起点的,也可以有多个起点,动作迁移器不需从起点开始迁移,可以从任何一个动作开始迁移,开始迁移后,其迁移的路径是按照指定的路径运行下去,直到该条路径的终点(即没有下一个迁移动作)。
3. 功能结构
该动作迁移器内置的属性有当前动作(action)、初始动作(initAction)、和迁移路径(transferPath),并设方法start(启动初始动作)、setAction(设置当前迁移动作)、transfer(根据动作状态进行指定的迁移操作)、clear(清除迁移路径)。详细设计代码如下:
ActionTransfer = Ext.extend(Object, {
action : 'start'
, constructor : function(config, scope) {
var conf = config || {};
this.scope = scope || this;
this.initAction = conf.initAction || 'start';
// 迁移路径,包含start和end动作
this.transferPath = Ext.applyIf(conf.path || {}, {
'start' : function() {},
'end' : function() {}
});
if (conf.auto === true) {
this.start();
}
}
/**
* 启动迁移
* @return {ActionTransfer} this
*/
, start : function() {
return this.setAction(this.initAction).transfer();
}
/**
* 设置当前动作
* @param {String} action the current action's name
* @param {Function} fn the function will be called, after setting action
* @return {ActionTransfer} this
*/
, setAction : function(action, fn) {
if (!this.transferPath) {
alert('ActionTransfer : no defined route chain');
} else if (action && action != ''
&& this.transferPath[action]) {
this.action = action;
if (typeof fn == 'function') {
fn.apply(this.scope);
}
} else {
alert('ActionTransfer : the specified action('
+ action + ') isn\'t defined');
}
return this;
}
/**
* 根据当前动作的运行状态进行迁移(调用状态指定的方法)
* @param {String} status the name of action's status
* @param {Arguments...} args... the arguments which are used by the transfer function
* @return {ActionTransfer} this
*/
, transfer : function(status/*, args...*/) {
var args = Array.prototype.slice.call(arguments, 1),
name = this.action,
action = this.transferPath[name];
if (!this.transferPath) {
alert('ActionTransfer : no defined transfer path');
} else if (!name) {
alert('ActionTransfer : no action');
} else if (typeof action == 'function') {
action.apply(this.scope, args);
} else if (typeof action == 'object'){
if (!action[status]) {
status && status != ''
? alert('ActionTransfer : undefined status('
+ status + ') of action('
+ name + ')')
: alert('ActionTransfer : unknown status');
} else if (typeof action[status] == 'function') {
action[status].apply(this.scope, args);
} else {
alert('ActionTransfer : '
+ 'no executable transfer function');
}
} else {
alert('ActionTransfer : Oh, I cann\'t do with action('
+ name + ')');
}
return this;
}
/**
* 清除迁移路径
* @return {ActionTransfer} this
*/
, clear : function() {
delete this.transferPath;
return this;
}
});
迁移路径(transferPath)是关联数组(按字符串索引元素,而非数字索引的数组)对象,其中为动作对应的执行函数或动作对应的状态集合,在状态集合中为状态对应的执行函数。如:
transferPath = {
'start' : function() {
// do something for starting
},
'request' : {
'success' : function() {
// do something...
},
'failed' : function() {
// do other thing...
}
}
};
方法setAction中的参数fn是在设置好当前动作后所执行的函数,该函数可以用于启动所设置的当前动作,或是做一些准备工作。
要进行迁移,可以有如下两种方式:
修改当前动作后立即进行迁移:
transfer.setAction('动作名称').transfer('所设定的动作的状态', '该动作所传递的数据');
不修改当前动作并进行迁移:
transfer.transfer('当前动作执行的状态', '当前动作所传递的数据');
4. 适用范围
动作迁移器(ActionTransfer)适用于某个过程可以被细分为几个单独的动作(Action),Action之间需要一定的数据传递,但是传递数据的Action是异步的,在异步的操作中又需要针对不同的运行状态调用并将数据传递到其他不同的Action的情况。特别是,当异步Action可以被多个不同的过程所使用,但该Action完成后调用的Action又不相同时,动作迁移器便可派上用场,它可以极大地简化代码,减少代码间的耦合和代码的重复编写,并且使过程更加清晰、明了,利于代码的理解和维护。
5. 案例分析
为了加深对该动作迁移器的认识,现举例如下。
假如,有一个B/S结构的程序需要添加个人私章功能,而且用户的私章是保存在服务器端的,在私章的加盖操作时的过程是这样的:
用户点击“加盖私章”按钮,客户端检查是否已经获取有私章信息,如果有私章信息,则进入加盖操作,如果没有,则将向服务器请求私章信息。
请求私章数据的过程,使用的是Ajax技术,其为异步动作。在该动作中,如果成功获取私章,则将进入加盖操作,如果获取失败或是服务器连接异常,则通常需要弹出错误或异常提示。
在请求过程中,如果服务器返回的私章数据为空,表明用户还未上传私章,则其将不能使用私章,所以,这时需要弹出上传对话框,待用户上传完成后,再进入加盖动作。
上传对话框使用的是Ext,其创建也是异步的,并且上传动作也是使用的Ajax,在成功上传后,将调用加盖动作,失败或异常时也需弹出提示。
整个过程大概就是这样,如果按照一般的方法,我会这样写这个加盖过程的代码,如下:
startStamp : function() {
if (!this.priData) {
// 无私章信息,则发送请求
this.request();
} else {
// 否则,直接加盖私章
this.stamp(this.priData);
}
}
, request : function() {
var me = this;
Ext.Ajax.request({
success : function (response) {
if (success) {
var sig = null;
if (!data) {
// 私章不存在,上传
me.popupWin();
});
} else {
// 已上传,则直接加盖
me.stamp(data);
}
} else {
Ext.Msg.alert('失败', '错误信息');
}
}
});
}
上传窗口的创建过程在此就不列出了,下面仅列出上传的Ajax请求过程:
if (form.form.isValid()) {
form.form.submit({
success : function(form, action) {
// 加盖私章
me.stamp(data);
win.close();
},
failure : function(form, action) {
if (action.result) {
Ext.Msg.alert('失败', '私章上传失败');
} else {
Ext.Msg.alert('失败', '私章上传失败,具体原因无法得知,请联系服务器管理员获取详情');
}
win.close();
}
});
}
从以上过程可以看出,在异步动作中都显式地调用了其下一步所涉及到的动作,该方法的弊端在于,如果后来的需求有变导致下一步动作也改变了,就需要重新修改调用的动作,这便增加了代码的耦合性,容易造成“牵一发而动全身”的副作用。
但是,在引入动作迁移器后,情况将得到很大程度的改善。
首先,看一下私章加盖的动作迁移图:
接着,看一下具体的代码:
createTransferfer : function() {
var me = this, win = null,
transfer = new ActionTransfer({
initAction : 'start',
auto : false,
path : { // 私章加盖(stamp) -- S
'start' : function() { // S-1: 开始
// S-1-1: 已获取到私章数据,迁移到加盖(stamp)动作S-3
if (me.priData) {
transfer.setAction('stamp')
.transfer(null, me.priData);
}
// S-1-2: 未获取到私章数据,则请求私章数据,
// 将迁移到请求(request)动作S-2
else {
me.request();
}
}
, 'request' : { // S-2: 请求私章数据
'success' : function(data) { // S-2-1: 获取成功
transfer.transfer('finish'); // 内部迁移: 请求过程结束
// S-2-1-1: 有私章数据,则迁移到加盖(stamp)动作S-3
if (data) {
transfer.setAction('stamp')
.transfer(null, data);
}
// S-2-1-2: 无私章数据,弹出上传框,
// 在点击上传按钮后迁移到上传(upload)动作S-4
else {
win = me.popupWin();
}
}
, 'failed' : function(msg) { // S-2-2: 获取失败
transfer.transfer('finish');
// S-2-2-1: 弹出失败提示
Ext.Msg.alert('失败', msg);
}
, 'abort' : function(msg) { // S-2-3: 获取异常
transfer.transfer('finish');
// S-2-3-1: 弹出异常提示
Ext.Msg.alert('异常', msg);
}
, 'finish' : function() { // S-2-4: 请求完成
// S-2-4-1: 取消表单mask
me.form.unmask();
}
}
, 'stamp' : function(sigData) { // S-3: 加盖私章
me.stamp(sigData);
}
, 'upload' : { // S-4: 上传私章
'success' : function(data) { // S-4-1: 上传成功
// S-4-1-1: 迁移到加盖(stamp)动作S-3
transfer.transfer('finish');
transfer.setAction('stamp')
.transfer(null, data);
}
, 'failed' : function(msg) { // S-4-2: 上传失败
transfer.transfer('finish');
// S-4-2-1: 弹出失败提示
Ext.Msg.alert('失败', msg);
}
, 'abort' : function(msg) { // S-4-3: 上传异常
transfer.transfer('finish');
// S-4-3-1 : 弹出异常提示
Ext.Msg.alert('异常', msg);
}
, 'finish' : function() { // S-4-4: 上传结束
// S-4-4-1: 关闭上传窗口
win.close();
}
}
}
});
return transfer;
}
私章请求过程修改如下:
request : function() {
var me = this;
me.transfer.setAction('request');
Ext.Ajax.request({
success : function(response) {
if (success) {
me.transfer.transfer('success', data);
} else {
me.transfer.transfer('failed', '错误信息');
}
}
, failure : function(response) {
me.transfer.transfer('abort', '出现异常');
}
});
}
上传请求过程修改如下:
if (form.form.isValid()) {
me.transfer.setAction('upload');
form.form.submit({
success : function(form, action) {
me.transfer.transfer('success', data);
},
failure : function(form, action) {
var msg = '私章上传失败';
var status = 'failed';
if (!action.result) {
status = 'abort';
}
me.transfer.transfer(status, msg);
}
});
}
最后,按如下过程初始化私章加盖的迁移器,并启动即可:
startStamp : function() {
var me = this;
me.transfer = me.createTransferfer();
me.transfer.start();
}
怎么样,是不是简单多了?
6. 后记
虽然,该动作迁移器目前针对的是JavaScript编程,但是进行扩展修改后应该也可以用到其他编程语言中,不管怎么说,我觉得这种思路还是比较好的,当然,如果有更好的方法,也希望大家能提出来,大家共同讨论一下。