模式8-模版方法模式

模版方法模式是一种基于继承的设计模式。主要由两部分构成:

  1. 抽象父类:包含子类的算法框架和一些通用的具体方法;

  2. 具体实现的子类: 包含对于父类中抽象方法的实现,继承父类的整个算法实现方法,并且可以重写父类中的方法。

在类似于java这样的面向对象语言中,抽象类的使用在这个设计模式中非常重要。因为在编译的时候会对继承抽象类的子类进行检测,要求必须要对抽象方法进行实现。然而在javascript中没有类型检查,所以要保证子类实现了所有抽象父类的抽象方法,可以在运行时进行检测,即让抽象方法抛出错误。

示例:

var Beverage = function() {}
Beverage.prototype.boilWater = function(){
    console.log("boil water");
}
Beverage.prototype.brew = function (){
    throw new Error("you must define function brew");
}
Beverage.prototype.pourInCup = function (){
    throw new Error("you must define function pourInCup");
}
Beverage.prototype.addCondiments = function (){
    throw new Error("you must define function addCondiments");
}
Beverage.prototype.customerWantsCondiments = function(){
    throw new Error("you must define function customerWantsCondiments");
}

//泡饮料的顺序和步骤是定的,算是一个子类通用的算法
Beverage.prototype.init = function(){ 
    this.boilWater();
    this.brew();
    this.pourInCup(); 
    if(this.customerWantsCondiments){
        this.addCondiments();
    }
};

var Tea = function(){};               
Tea.prototype = new Beverage();        
Tea.prototype.brew = function(){       
    console.log("brew-up");            
}                                     
Tea.prototype.pourInCup = function(){
    console.log("pour tea");   
}
Tea.prototype.addCondiments = function(){
    console.log("add sugar and milk");
}
Tea.prototype.customerWantsCondiments = function() {
    return window.confirm("Do you need condiments?");
}

var tea = new Tea();
tea.init();

var Coffee = function(){};               
Coffee.prototype = new Beverage();        
Coffee.prototype.brew = function(){       
    console.log("brew coffee");            
}                                     
Coffee.prototype.pourInCup = function(){
    console.log("pour coffee");   
}
Coffee.prototype.addCondiments = function(){
    console.log("add lemon");
}
Coffee.prototype.customerWantsCondiments = function() {
    return window.confirm("Do you need condiments?");
}

var coffee = new Coffee();
coffee.init();

这个设计模式是有利于系统的拓展的,并且符合开放-封闭原则。另外,我们在js中不一定非要使用继承的方式来完成这个设计模式,也可以通过传入高阶函数作为参数来实现,用以替代父类中的抽象函数。


模式9-享元模式

享元模式是为了优化性能而存在的。假设系统中存在大量类似的对象而导致内存消耗过高,享元模式就非常有用了。
享元模式包含两种状态(即属性):

  1. 内部状态:

    -存储于对象内部。
    -可以被一些对象共享。
    -独立于具体的场景,通常不会改变。
  2. 外部状态:

    取决于具体的场景,并根据场景而变化,外部状态不能被共享。
    

    剥离了外部状态的对象成为共享对象,外部状态在必要时被传入共享对象来组装成一个完整的对象。系统中可能存在的最大享元对象个数等于不同内部状态的组合数。

示例:

//这个享元只有一个内部状态
var Model = function( sex ){ 
    this.sex = sex;
};
Model.prototype.takePhoto = function(){
    console.log( 'sex= ' + this.sex + ' underwear=' + this.underwear);
};

//分别创建一个男模特对象和一个女模特对象:
var maleModel = new Model( 'male' ), 
femaleModel = new Model( 'female' );

//给男模特依次穿上所有的男装,并进行拍照:
//注:这里我们是在需要的时候才传入外部状态
for ( var i = 1; i <= 50; i++ ){ 
    maleModel.underwear = 'underwear' + i; 
    maleModel.takePhoto();
};

//同样,给女模特依次穿上所有的女装,并进行拍照:
for ( var j = 1; j <= 50; j++ ){ 
    femaleModel.underwear = 'underwear' + j; 
    femaleModel.takePhoto();
};

应用:
文件上传,只根据上传组件的不同来(使用工厂)新建uploader对象(只含有一个uploadType内部状态,同样uploadType的被共享),而文件信息等储存在外部,只当需要(如删除文件)时才(通过uploadManager)将外部状态传入内部。

对象池是另一种性能优化的方案,其思想是创建一个池子用来存放空闲对象,当需要使用该对象时从池子中取,如果没有则创建,用完之后放回。不过与享元模式不同的是它不会区分内部状态和外部状态。


模式10-职责链模式

思想是将可能处理请求的对象连成一个链,请求者只需知道第一个对象,然后将请求沿着链依次传递直到遇到可以处理该请求的对象。这就使得请求发出着和请求接受者之间解耦,也就是说请求发出者不必知道哪个对象可以处理请求,避免了在一个函数中使用大量的if else判断。

示例:

var handler1 = function(params){
    if(params === true) {   // check condition
        console.log("request solved by handler1");
    } else {
        return false;      // condition not valid
    }
}

var handler2 = function(params){
    if(params === true) {   // check condition
        console.log("request solved by handler2");
    } else {
        return false;      // condition not valid
    }
}

var handler3 = function(params){
    if(params === true) {   // check condition
        console.log("request solved by handler3");
    } else {
        return false;      // condition not valid
    }
}

var Chain = function( fn ){ 
    this.fn = fn;
    this.successor = null; 
};
Chain.prototype.setNextSuccessor = function( successor ){ 
    return this.successor = successor;
};
Chain.prototype.passRequest = function(){
    var ret = this.fn.apply( this, arguments );
    if ( ret === false ){
        return this.successor && this.successor.passRequest.apply( this.successor, arguments );
    }
    return ret; 
};

//调用next函数可以手动传递请求,用于异步处理
Chain.prototype.next= function(){
    return this.successor && this.successor.passRequest.apply( this.successor, arguments );
};


var chain1 = new Chain(handler1);
var chain2 = new Chain(handler2);
var chain3 = new Chain(handler3);

//指定节点在职责链中的顺序
chain1.setNextSuccessor(chain2); 
chain2.setNextSuccessor(chain3);

//把请求传递给第一个节点:
chain1.passRequest(someParams); 

职责链模式使得各个链结点之间可以拆分重组,便于插入或删除结点。并且请求不一定要从第一个结点开始。同时,要避免职责链过长带来的性能问题。
另外,可以利用js的函数式特性将函数“链接”起来实现职责链模式,即为Function对象的原型添加after函数。

应用:
不同浏览器文件上传控件的选择,DOM事件冒泡等。


模式11-中介者模式

中介者模式是为了解除对象之间的强耦合关系,所有对象都通过中介者通信而不再相互引用。

示例:
假设一个网页中几个DOM元素的值共同决定一个按钮的有效性,
例如input必须有效,select1和select2必须选择

传统的方法中,我们需要为这三个DOM元素添加值改变监听函数,并且在每个监听函数中都要坚持另外两个DOM的值。这时,如果我们需要加入一个input2,就需要将所有其他DOM元素的监听函数进行修改。
如果使用中介者模式,我们可以创建一个mediator对象,并且提供一个向其发送消息的借口。然后,我们为其他DOM创建的监听函数中只需要向中介者发送一个消息并且把this作为参数传递告诉中介者是谁发的消息。而中介者的实现中,只需要对收到的不同消息进行处理即可,如果需要添加新的关联DOM,也只需要稍稍修改mediator的代码而不会需要修改其他DOM的监听函数了。

总之,中介者模式使对象之间的网状引用关系变成了一对多的关系,满足一个对象尽可能少地了解其他对象的原则。但是该模式引入了中介者对象,也会造成一些内存消耗。


模式12-装饰者模式

装饰者模式用于动态地给对象增加职责。

为了保证在执行原函数和this指向发生改变,我们需要引入代理函数。代理函数的功能是在原函数执行之前或之后,执行另外的函数。

示例:

使用AOP,改变Function对象的原型

Function.prototype.before = function(beforefn){
    var _self = this;
    return function(){
        beforefn.apply(this, arguments);  //确保this指向不变
        this.apply(this, arguments);
    }
}

var func1 = function(){
    alert("1");
}

func1 = func1.before(function(){
    alert("0");
});

func1();  //先输出0再输出1

不改变原型的做法:

var before = function(fn, beforefn){
    return function() {
        beforefn.apply(this, arguments);
        fn.apply(this, arguments);
    }
}

func1 = before(func1, function(){alert("0")});

应用:
对于某些用户操作(如单击按钮)数据统计上报,改变函数的arguments对象(如ajax传数据时添加属性),表单验证和提交功能的分离等。

这个设计模式使得开发人员在开发框架时可以只考虑对象的稳定和基础功能,其他需要添加的个性功能可以被动态添加。它不同于代理模式的是,代理模式的目的是为了控制对对象的访问或添加一些功能,而装饰者模式则是为了为对象动态添加功能,并且通常会形成一条长长的装饰链。

值得注意的是,装饰者模式返回的是一个新的函数,因此原函数上的属性会消失。同时,装饰链也叠加了函数的作用域,过长则会对性能产生影响。


模式13-状态模式

状态模式使得一个对象在其状态改变时改变其行为。也就是说,在不同状态下调用同一个名字的函数其函数功能是不同的。
在状态模式中,一般有两类对象:context和状态类。context构造函数中应该实例化所有的状态类并作为context的属性,以便context调用状态类中的方法。而状态类的构造函数应该以context作为参数,以便可以调用context中改变其state值的接口来对其进行赋值。

例如一个拥有不同光强度的灯的控制代码示例:

var Light = function(){
    this.offLightState = new OffLightState( this );  // 持有状态对象的引用 
    this.weakLightState = new WeakLightState( this );
    this.strongLightState = new StrongLightState( this ); 
    this.superStrongLightState = new SuperStrongLightState( this ); 
    this.button = null;
};

Light.prototype.init = function(){
    var button = document.createElement( 'button' ),
    self = this;
    this.button = document.body.appendChild( button ); 
    this.button.innerHTML = '开关';
    this.currState = this.offLightState;
    this.button.onclick = function(){ 
        self.currState.buttonWasPressed();
    } };

//接下来就要定义各种状态类
var OffLightState = function( light ){ 
    this.light = light;
};
OffLightState.prototype.buttonWasPressed = function(){ 
    console.log( '弱光' );
    this.light.setState( this.light.weakLightState );
};

// 其他状态类似,都需要定义一个buttonWasPressed方法,略

该设计模式优点是将对象的状态跟对应的方法一起封装在一个类里,使得状态的管理如添加删除等更easy。状态模式与策略模式很相似,都是有context和一些策略/状态类。不同点是策略类之间是平行的,用户需要知道他们的不同来选择调用,而状态类之间的状态转换关系是早就规定好的,用户也不必知道其内部不同点。

JavaScript版本的状态机(在js中状态类不一定需要通过类来创建,可以使用显式的对象):

var Light = function(){
    this.currState = FSM.off;   // 设置当前状态 
    this.button = null;
};

Light.prototype.init = function(){
    var button = document.createElement( 'button' ),
    self = this;
    button.innerHTML = '已关灯';
    this.button = document.body.appendChild( button );
    this.button.onclick = function(){ 
        self.currState.buttonWasPressed.call( self );
    }
};

var FSM = { 
    off: {
        buttonWasPressed: function(){
        console.log( '关灯' ); 
        this.button.innerHTML = '下一次按我是开灯'; 
        this.currState = FSM.on;
        }
    },
    on: {
        buttonWasPressed: function(){
        console.log( '开灯' ); 
        this.button.innerHTML = '下一次按我是关灯'; 
        this.currState = FSM.off;
    };

var light = new Light(); 
light.init();

模式14-适配器模式

适配器模式的作用就是转换不兼容的接口。

示例:

var getStudents = function(){
    //这个函数返回一个对象的数组
    var arr = [
        {"id": 0,
         "name": "LiLei"
        },
        {"id": 1,
         "name": "HanMeimei"
        }
    ];
}

var printStudents = function(fn){
    var params = fn();
    for(var i = 0; i < params.length; i++){
        console.log(params[i].name + ":" + params[i].id);
    }
}

printStudents(getStudents);

//倘若现在提供学生信息的函数变了,返回值类型也变了:
var getStudentsObj = function () {
    var stu = {
        "0": "LiLei",
        "1": "HanMeimei"
    };
}

//adaptor
var stuAdapter = function(oldfn){
    var newRes = {};
    var stu = oldfn();
    for(var i = 0; i < stu.length; i++) {
        newRes[stu[i].id] = stu[i].name; 
    }
    return function(){
        return newRes;
    }
}

printStudent(stuAdapter(oldfn));

装饰者模式与代理模式的结构与适配器模式很像,都是用一个对象来包装另一个对象,但不同点仍然是他们的意图。适配者模式只是转换接口,不需要知道对象的具体实现。


P.s. 本文总结自《JavaScript设计模式与开发实践》,曾探著


爱生活的玛西娜
30 声望0 粉丝

(OvO)与其过分忧虑看不清的未来,不如用心享受当下的每一个可能。