软件设计模式在JavaScript中的应用
 

2009-01-06 作者:朱先忠 来源:IT168

 

1.简介

  在Ajax技术出现之前,传统的Web应用中JavaScript代码所占的比例较少,并且大多数情况下都是采用面向过程的编程方式,真正将JavaScript作为一门高级语言进行正式编程的开发人员也很少。而目前富客户端的Web应用开发已经成为大势所趋,JavaScript在Web应用中所占的比例会越来越大。如何有效地实现代码的复用以及灵活应对需求的变化已经正式列入JavaScript程序员必须探讨的议事日程。

GoF的《Design Patterns:Elements of Reusable Object-Oriented Software》一书成为当代程序员研究设计模式的典范与开山之作。然而,目前针对设计模式的探讨主要停留在以Java与C#等强类型高级语言方面,那么,对于像JavaScript这样的弱类型语言是否也适用设计模式呢?答案是肯定的。在本文中,我将通过几个简短的示例来向你展示如何把设计模式应用于像JavaScript这样的弱类型语言的编程之中。

2.Singleton模式(亦称“单例模式”)

(1)概念

Singleton模式作为一种非常基本和重要的创建型模式,其职责是保证一个类有且只有一个实例,并提供一个访问它的全局访问点。

在程序设计过程中,有很多情况下需要确保一个类只能有一个实例。传统的编程语言中为了使一个类只有一个实例,最容易的方法是在类中嵌入静态变量,并在第一个实例中设置该变量,而且每次进入构造函数都要做检查,不管类有多少个实例,静态变量只能有一个实例。为了防止类被多次初始化,需要把构造函数声明为私有类型,这样便只能在静态方法里创建一个实例。

在JavaScript中,虽然我们仍然可以指定静态方法来构造对象,但由于我们不能利用构造函数的“私有”来禁止多个实例的生成,因此要完全实现Singleton并没有想象中那么简单。

(2)举例

假设有一个作为工具类使用的对象,它不包含任何业务逻辑,也不包含任何需要改变的数据。使用这样的对象时不需要每次都进行实例化,只需要存在一个全局的对象供调用即可。

根据刚才的分析,我们就可以在JavaScript中通过如列表1所示的方式实现Singleton模式。模式可以保证一个类仅有一个实例,并且提供一个访问它的全局访问点。

列表1:在JavaScript中创建Singleton模式类

function Singleton(){
this.property1=”hello”;
this.methodl=function(x,y){
return x+y;
}
//定义一个静态属性
Singleton._instance_=null
Singleton.getInstance=
function(){
if(this._instance_==null){
//如果Singleton实例不存在,则进行实例化
this._instance_=new Singleton();
}
//返到Singleton的对象实例
return this._instance_;
};

上面的getInstance方法首先判断静态属性Singleton._instance_是否为null;如果为null,则新建一个Singleton类的对象,保存在Singleton._instance_,最后返回Singleton._instance_属性。
至于Singleton类的使用方法则很简单:

var instA =Singleton.getInstance();  //返回Singleton类的唯一实例

但遗憾的是,以上的代码不能禁止用户直接实例化Singleton类。例如,通过以下语句实例化Singleton类仍然是成立的:

var instA =new Singleton ();

而随后你调用如下语句也是成立的:

alert(typeof(instA));
alert(instA.property1);

因此,我们需要进一步修改Singleton类的构造函数,使得Singleton类只能在Singleton.getInstance方法中进行实例化。

function Singleton(){
if(Singleton.caller!=Singleton.getInstance){
throw new Error(”Can not new Singleton instance!”);
}
…………
}

这样当用户试图自己创建多个对象的时候,通过人工抛出异常来阻止。不过这么做你感觉是不是还是有一点别扭?创建单例就创建单例吧,干嘛非要抛出一个异常呢?但无论如何,使用上述方案已经能够实现了仅能创建类的单例的目的。

下面,我们来讨论第三种方法(也是最优秀的方法)。这种方法巧妙利用了JavaScript对于“匿名”函数的支持特征来间接实现禁止对Singleton类构造函数的访问,从而较好地模拟了私有构造函数的特性,最终比较完美地解决了用JavaScript实现Singleton模式的问题。

列表2:借助于“匿名”函数的支持在JavaScript中创建Singleton模式类

<script type="text/javascript">
(
function(){
    
//SingletonFactory Interface
    SingletonFactory = {
        getInstance : getInstance
    }
    
//private classes
    function SingletonObject()
    {
        SingletonObject.prototype.methodA =
function()
        {
            alert('methodA');
        }
        SingletonObject.prototype.methodB =
function()
        {
            alert('methodB');
        }
        SingletonObject.instance =
this;
    }  
    
//SingletonFactory implementions
    function getInstance()
    {
        
if(SingletonObject.instance == null)
            
return new SingletonObject();          
        
else
            
return SingletonObject.instance;
    }
})();
var instA = null;
try
{
    alert("试图通过new SingletonObject()构造实例!");
    instA =
new SingletonObject();
}
catch(e){
    alert("SingletonObject构造函数不能从外部访问,系统抛出了异常!");
}
instA = SingletonFactory.getInstance();  
//通过Factory上定义的静态方法获得
var instB = SingletonFactory.getInstance();
instA.methodA();
instB.methodA();
alert(instA == instB);
//成功    
</script>

上面的第三种创建方式可谓挖空心思,穷JavaScript之能事,但终于获取了令人满意的效果。

3.Factory Method模式

(1)概念

根据GoF的定义,Factory Method模式的目的是为了定义一个创建对象的接口,由子类来决定实例化哪一个类。更准确地说应该是,Factory Method模式是将创建对象实例的责任转移到工厂类中,并利用抽象的原理,将实例化行为延迟到具体工厂类中。

(2)举例

在某些情况下,我们的确无法确定将要创建的对象是哪个类的实例,这样的一个典型示例就是在Ajax应用程序中需要创建XMLHttpRequest(XHR)对象时,因为大家都知道在不同的浏览器中XHR对象的实现类是不同的。

通过引入Factory Method模式,我们即可以轻松使用JavaScript实现创建通用的XHR对象,相应的代码实现如下所示。

function XMLHttpFactory(){}
XMLHttpFactorv.CreateXMLHttp=
function() {
if(typeof XMLHttpRequest!=”undefined”){
//支持XMLHttpRequest对象的浏览器:例如Firefox等
return new XMLHttpRequest();
}
else if(typeof window.ActiveXObject!=”undefined”){
//支持ActiveX对象的浏览器,即IE
return new ActiveXObject(“MSXML2.XMLHttp”);
}
}
 

然后,通过以下代码便可以相当容易地判断出浏览器的类型并进而创建XHR对象。

var xmlhttp=XMLHttpFactory.createXMLHttp();

此后,基于以上创建的XHR对象就可以进行Ajax风格的服务器端调用了。

4.Decorator模式

  (1)概念

Decorator模式是结构型模式的一种,它充分利用了继承与聚合的优势,其主要目的是为了给一个对象动态地添加以新的职责,而此职责并不修改原有的行为,而是在原有行为基础上添加新的功能,就好比装饰工人为一座新居的墙上涂抹上色彩缤纷的颜料一般。

【备注】面向方面编程(AOP),是一种改进已经存在的模式和发现新模式的方法。面向方面的编程能够独立于任何继承层次结构而应用改变类或对象行为的方面。然后,在运行时或编译时应用这些方面。AOP编程引入了一组新概念:接合点(Joinpoint)—代码中定义明确的可识别的点。切点(Pointcut)—通过配置或编码指定接合点的一种方法。通知(Advice)—表示需要执行交叉切割动作的一种方法。混入(Mixin)—通过将一个类的实例混入目标类的实例引入新行为。对于好的编程来说,设计模式已经变成了常用的方式。AOP可以给我们实际上,横切关注点代码的注入,因此说它也就是一种新式类型的软件设计模式。

(2)举例

有意思的是,我们也可以把Decorator模式应用于JavaScript中实现面向方面编程(AOP)。现在,不妨让我们观察一下列表3中定义的代码。

列表3:使用Decorator模式在JavaScript中实现面向方面编程(AOP)

function Decorator(object){
object.setup=
function(method){
//如果从未执行过Setup方法,则进行初始化
if(!(‘_’+method in object)){
//保留原有的method方法
object[‘_’+method]=object[method];
//初始化前置和后置通知的列表
object[‘beforeAdvice_’+method]=[];
object[‘afterAdvice_’+method] =[];
//改写method方法
object[method]=function(){
//前置通知列表
var before=this[‘beforeAdvice_’+method];
//后置通知列表
var after=this[‘afterAdvice_’+method];
//返回值
var ret;
//执行前置通知中的函数
for(var i=0; i<before.length; i++) {
before[i].call(
this,arguments);
}
//执行原有的方法
ret=this[‘_’  +method](arguments);
//调用后置通知中的函数
for (var i=; i<after.1ength; i++){
after[i].call(
this, arguments);
}
//返回原有方法的返回值
return ret;
}
}
};

//添加前置通知
object.addBeforeAdvice=function(method, f)  {
object.setup(method);
//最后添加的前置通知将最先被执行
object[‘beforeAdvice_’ +method].unshift(f);
};
//添加后置通知
object.addAfterAdvice=function(method, f)  {
object.Setup(method);
//最后添加的后置通知将最后被执行
object[‘afterAdvice_’  +method].push(f);
};
}
 

Decorator函数的基本思路是将原有的函数替换,在替换后的函数中增加对前置、后置通知的调用。下面通过一个具体的示例来说明Decorator函数的使用方法,首先定义一个类testClass。

var testClass  =  function()  {
testClass.prototype.pl=”hello”;
testClass.prototype.ml =
function()  {
alert(
this.p1);
}
};

如果希望在所有testClass类的实例上增加前置或后置通知,那么需要对testClass.prototype属性进行处理。

Decorator(testClass.prototype);
testClass.prototype.addBeforeAdvice(“m1”,
function()  {
alert(“beforeAdvice”);
});
testClass.prototype.addAfterAdvice(“m1”,
function()  {
alert(”afterAdvice”);
});
 

此时,创建一个testClass类的对象实例,并且执行它的ml方法,程序将依次输出“beforeAdvice”、“hello”和“afterAdvice”。

var t=new testClass ();
t.ml();  
//依次输出“beforeAdvice”、“hello”和“afterAdvice”
 

如果仅希望对testClass类的某个具体实例添加前置、后置通知,那么直接处理该实例即可。

var t=new testClass();
Decorator(t);
t.addBeforeAdvice(“ml” ,
function](){
alert(“beforeAdvice”);
});
t.addAfterAdvice(“ml”,
function(){
alert(“afterAdvice”);
});

5.Observer模式

(1)概念

使用Observer模式的目的是为了定义对象之间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并且自动更新。在以下情况下,一般可以考虑使用Observer模式。

  • 当一个抽象模型有两个方面,其中一个方面依赖于另一方面。将这二者封装在独立的对象中以便使它们可以各自独立地改变和复用。
  • 当对一个对象的改变需要同时改变其他对象,而不知道具体有多少对象有待改变。
  • 当一个对象必须通知其他对象,而它又不能假定其他对象是谁。换言之,你不希望这些对象是紧密耦合的。

(2)举例

下面,我们将应用Observer模式实现简单的JavaScript事件处理机制。首先需要做一些准备工作,即对Array对象进行扩展,为其增加indexOf和removeAt方法,代码如下:

//扩展destination对象的属性,将source对象的所有属性复制到destination对象
Object.extend=function(destination,source){
for(property in source){
destination[property]=source[property];
}
return destination;
}
//扩展Array对象的属性
Object.extend(Array.prototype,{
//在数组中查找object对象
indexOf: function (object){
for(var i=0,1ength=this.length; i<length; i++)
    
if(this[i]==object) return i;
return -1;
},
//将数组中指定位置的元素删除
removeAt: function (index){
if(index<0 || index>=this.length) return null;
switch(index){
case 0:
return this.shift();
break;
case this.length -1:
return this.pop();
break;
default:
var head=this.s1ice(0,index);
var tail=this.s1ice(index+1);
var ele=this[index];
this=head.concat(tail);
return ele;
break;
}
}
});
 

接下来,定义Observer类和Subject类。其中,Observer类的代码如下所示:

//Observer类
function Observer(){}
object.extend(Observer.prototype, {
//实体Observer类需要覆盖Update方法
Update:function(){
return;
}
});
 

Subject类的代码如下:

//Subject类
function Subject(){}
Object.extend(Subject.prototype,{
//Observer对象的数组
observers:[],
//通知每一个Observer对象执行Update方法
notify:function(context){
for(var i=0; i<this.observers.length; i++)
this.observers[i].Update(context);
},
//添加一个新的Observer
addObserver:function(observer)  {
if(!observer.Update)
throw new Error(“addObserver error!”);
this.observers.push(observer);
},
//删除已有的Observer
removeObserver:function(Observer)  {
if(!observer.Update)
throw new Error(“removeObserver error!”);
this.observers.removeAt(this.observers.indexOf(observer));
},
});
 

到此为止,使用Observer模式所需要的类都已经实现。下面是使用这些类实现Observer模式的步骤:

  • 创建Subject对象实例;
  • 创建若干个Observer对象实例,分别覆盖它们的Update方法;
  • 将Observer对象实例订阅到Subject对象上;
  • 通过Subject对象发出通知,此时所有订阅了该Subject对象的Observer对象均会执行各自的Update方法;
  • 如果需要撤销某个Observer对象对Subject对象的订阅,可以调用removeObserver方法实现。

最后,我们通过一个实例来演示Observer模式的应用。首先,创建一个Subject对象并且为该对象定义publishEvent方法。
 

//新建一个Subject对象
var concreteSubject=new Subject();
//定义publishEvent方法,该方法的作用是通知所有的Observer,通知的内容是data
concreteSubject.publishEvent=function(data){
document.write(“published:”+data);
this.notify(data);
}

 

//新建一个Subject对象
var concreteSubject=new Subject();
//定义publishEvent方法,该方法的作用是通知所有的Observer,通知的内容是data
concreteSubject.publishEvent=function(data)  (
document.write(“published:”+data);
this.notify(data);
}

//新建一个Observer对象,并覆盖其Update方法
var concreteObserverl=new Observer();
concreteObserverl.Update=
function(data){
document.write(“concreteObserverl received:”+data);
}

//将concreteObserverl订阅到concreteSubject
concreteSubject.addObserver(concreteObserverl);

//新建第二个Observer对象,并覆盖其Update方法
var concreteObserver2=new Observer();
concreteObserver2.Update=
function(data){
document.write(“concreteObserver2 received:”+data);
}

//将concreteObserver2订阅到concreteSubject
concreteSubject.addObserver(concreteObserver2);
 

执行concreteSubject对象的publishEvent方法,发出通知。

concreteSubject.publishEvent(“msg”);

此时程序将输出:

published:msg
concreteObserverl received:msg
concreteObserver2 receired:msg

这样的输出结果说明concreteObserverl和concreteObserver2都接收到来自concreteSubject的通知。接下来调用removeObserver方法撤消一个concreteSubject的订阅。

concreteSubject.removeObserver(concreteObserver2);

再次发出通知:

concreteSubject.publishEvent(“msg”);

这一次只有concreteObserver1对象接收到通知,程序输出结果为:

published:msg
concreteObserver1 receired:msg

使用Observer模式可以在JavaScript中实现通用的事件机制。有些熟悉Ajax开发的朋友可能知道Prototype框架和Dojo框架这两个客户端框架。其实,这两个框架的事件机制就是采用了类似此处介绍的方法实现的,有兴趣的朋友可进一步研究。

6. Façade模式

(1)概念

Façade模式可以为子系统中的一组接口提供一个一致的界面,它定义了一个高层接口,这个接口使得相应的子系统更加容易使用。Façade模式在Web开发中的一个典型应用是实现统一的API接口,消除各种浏览器在具体特性实现中的差异。

(2)举例

在基于Ajax的Web开发中,当使用XHR对象请求服务器端的数据时,我们通常需要按照以下的步骤进行编程:

  • 创建XHR对象;
  • 注册回调函数;
  • 用open方法设置请求方式(POST/GET)、URL和模式(同步/异步);
  • 用send发送请求;
  • 监视请求的状态,当达到某一特定状态时,执行某项特殊功能。

可以看到,使用XHR对象的步骤是比较复杂的,在每个步骤中都需要考虑浏览器兼容性问题。

【补充】熟悉Prototype框架的朋友可能知道此框架提供了一个Ajax.Request对象,使用该对象不必劳开发人员考虑以上这些繁琐步骤以及步骤之后的细节,通过统一的方式即可调用。有兴趣的读者可以下载Prototype的源码自行学习Ajax.Request对象的具体实现方法,在此不再赘述。

下面,我们将通过一个简单的示例来说明Façade模式的应用。该示例的目标是向用户输出一行欢迎信息,提示用户使用的浏览器类型。为了简单起见,示例中仅区分IE和其他浏览器。相关的代码如下所示:

//IE浏览器对象
function IEBrowser()  {
this.hello=function(){
alert(”IE browser”);
}
}
//非IE浏览器对象
function NonIEBrowser()  (
this.hello =function(){
alert(“NonIE browser”);
}
}
//Facade对象
var Facade={};
Facade.hello=
function(){
var browser;
//如果为IE浏览器
if(window.ActiveXObject)  browser=new IEBrowser();
//如果为非IE浏览器
else browser=new NonIEBrowser();

Browser.hello();
);
 

Facade.hello方法根据浏览器类型,创建不同的browser对象,再调用browser对象的hello方法。这样对于用户而言,无需考虑浏览器的类型,只需要简单地调用Facade对象的hello方法即可。

另外,IEBrowser和NonIEBrowser分别是两个独立的类,它们的内部实现可能会在未来发生变化,但是这都不会影响到外部对象Facade对它们的调用,即Facade.hello的代码无需改变。

7. Command模式

(1)概念

Command模式把一个请求或操作封装到一个对象中。具体地说,Command模式允许系统使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。Command模式把发出命令的责任和执行命令的责任分割开,委派给不同的对象。

(2)举例

下面我们通过一个JavaScript计算器程序的设计思路来说明Command模式的应用。

首先,我们马上会想到定义如下的Calculator对象,其中包括了定义四则运算的四个方法。

var Calculator={
//加法
add: function(x,y) {
return x+y;
},
//减法
substract: function(x, y) {
return x-y;
},
//乘法
multiply: function(x, y) {
return x*y;
},
//除法
divide: function(x, y) {
return x/y;
},
};
 

诚然,基于以上的Calculator对象,我们可以直接调用Calculator.add等方法来实现基本的四则运算;但是,在某些情况下我们不希望直接调用Calculator对象内部的方法,因为这样会增加对象之间的依赖关系。也就是说,如果Calculator对象的内部实现发生了变化,那么调用Calculator进行计算的相关程序代码也必须同时进行相应的修改。这样以来,就与面向对象设计中应当尽可能减少对象之间耦合度的理念相违背。

根据设计模式的相关理论,我们可以尝试使用Command模式来改进以上设计。在此,使用Command模式的基本思想是:将任何类型的计算都看作是对Calculator对象的一个请求,请求的内容包括运算的类型和参与运算的操作数。于是,通过如下的代码即可计算出“1+2”的运算结果。

Calculator.calc({
type: “add”,
//加法运算
op1:1, //第1个操作数为l
op2:2 //第2个操作数为2
});
 

根据上面的使用格式,我们可以得到如下所示的Calculator.calc方法定义:

Calculator.calc=function(command)  {
return Calculator[command.type](command.opl,command.op2);
};
 

这样以来,我们就可以通过如下Calculator.calc方法来实现上述各类运算:

Calculator.calc({type: “add”,opl:1,op2:1});//加法运算,返回2
Calculator.calc({type: “Substract”,opl:6,op2:2});//减法运算,返回4
Calculator.calc({type: “multiply”,opl:5,op2:2)); //乘法运算,返回10
Calculator.calc({type: “divide”,opl:8,op2:4));//除法运算,返回2
 

8.  小结

基于设计模式编程的思想尽管已经提出好几年了,但是仅仅在最近几年才引起第一线开发人员的重视,这从互联网上众多的资料不难得知。设计模式是基于面向对象编程的,因此掌握起来并不容易,特别是它需要在软件开发不断地实践才能最终灵活运用各种设计模式。

设计模式不依赖于任何语言,因此在JavaScript开发中同样适用,仅仅是这方面的资料甚少罢了。本文正是基于客户端常见的JavaScript开发实例,重点讨论了Singleton、Factory Method、Decorator、Observer、Façade、Command共6种典型设计模式的运用。


火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。
资源网站: UML软件工程组织