前端开发里的设计模式

前端开发中的设计模式

设计模式的定义是,在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案。在不同的编程语言中,对设计模式的实现其实是可能会有区别的。比如java和javascript,在Java这种静态编译型语言中,无法动态地给已存在的对象添加职责,所以一般通过包装类的方式来实现装饰者模式。但在JavaScript这种动态解释型语言中,给对象动态添加职责是再简单不过的事情。这就造成了JavaScript语言的装饰者模式不再关注于给对象动态添加职责,而是关注于给函数动态添加职责。本篇博文将介绍以下几个比较常见的设计模式:

  • 单例模式
  • 观察者模式
  • 命令模式
  • 职责链模式

单例模式

单例模式的定义是保证一个类只有一个实例,并且提供一个访问它的全局访问点。有些时候一些对象我们往往只需要一个,比如线程池、全局缓存、浏览器中的window对象等。单例模式的优点是:

  • 可以用来划分命名空间,减少全局变量的数量
  • 使用单例模式可以使代码组织的更为一致,使代码容易阅读和维护
  • 可以被实例化,且实例化一次

要实现一个标准的单例模式并不复杂,无非是用一个变量标识当前是否已经为某个类创建过对象,如果是,则在下一次获取这个类的实例时,直接返回之前创建的对象。下面是单例模式的基本结构:

// 单例模式
var Singleton = function(name){
    this.name = name;
    this.instance = null;
};
Singleton.prototype.getName = function(){
    return this.name;
};
// 获取实例对象
Singleton.getInstance = function(name) {
    if(!this.instance) {
        this.instance = new Singleton(name);
    }
    return this.instance;
};
// 测试单例模式的实例
var a = Singleton.getInstance("aa");
var b = Singleton.getInstance("bb");

实际上因为单例模式是只实例化一次,所以a和b其实是相等的。也即是说下面语句的值为true。

console.log(a===b)

由于单例模式只实例化一次,因此第一次调用,返回的是a实例的对象,继续调用的时候,b的实例也就是a的实例,因此下面打印的都是aa:

console.log(a.getName());// aa

console.log(b.getName());// aa  

观察者模式

观察者模式又叫做发布-订阅模式,它定义了对象间的一种一对多的依赖关系,当一个对象的状态发生变化时,所有依赖于他的对象都将得到通知,在javascript的开发中,一般用事件模型来替代传统的发布 — 订阅模式。

发布 — 订阅模式可以广泛应用于异步编程中,这是一种替代传递回调函数的方案。比如,我们可以订阅 ajax请求的 error 、 succ 等事件。或者如果想在动画的每一帧完成之后做一些事情,那我们可以订阅一个事件,然后在动画的每一帧完成之后发布这个事件。在异步编程中使用发布 — 订阅模式,我们就无需过多关注对象在异步运行期间的内部状态,而只需要订阅感兴趣的事件发生点。

发布 — 订阅模式还可以取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。发布 — 订阅模式让两个对象松耦合地联系在一起,虽然不太清楚彼此的细节,但这不影响它们之间相互通信。当有新的订阅者出现时,发布者的代码不需要任何修改;同样发布者需要改变时,也不会影响到之前的订阅者。只要之前约定的事件名没有变化,就可以自由地改变它们。

实际上,只要我们曾经在 DOM 节点上面绑定过事件函数,那我们就曾经使用过发布 — 订阅模式,以下代码便是一个示例:

document.body.addEventListener( 'click', function(){
alert(2);}, false );
document.body.click(); // 模拟用户点击

在这里需要监控用户点击 document.body 的动作,但是我们没办法预知用户将在什么时候点击。所以我们订阅 document.body 上的 click 事件,当 body 节点被点击时, body 节点便会向订阅者发布这个消息。就像是楼房购买,购房者不知道房子什么时候开售,于是他在订阅消息后等待售楼处发布消息。

除了 DOM 事件,我们还会经常实现一些自定义的事件,这种依靠自定义事件完成的发布 —订阅模式可以用于任何 JavaScript代码中。实现发布 — 订阅模式的步骤如下:
1. 首先指定好谁充当发布者;
2. 然后给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者;
3. 最后发布消息的时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者回调函数。

var salesOffices = {}; // 定义售楼处
salesOffices.clientList = []; // 缓存列表,存放订阅者的回调函数
salesOffices.listen = function( fn ){ // 增加订阅者
    this.clientList.push( fn ); // 订阅的消息添加进缓存列表
};
salesOffices.trigger = function(){ // 发布消息
    for( var i = 0, fn; fn = this.clientList[ i++ ]; ){
        fn.apply( this, arguments ); // arguments 是发布消息时带上的参数
    }
};
//调用
salesOffices.listen( function( price, squareMeter ){//订阅消息
    console.log( '价格= ' + price );
    console.log( 'squareMeter= ' + squareMeter );
});
salesOffices.trigger( 2000000, 88 ); // 输出:200 万,88 平方米

至此,实现了最简单的发布-订阅模式。比起在Java中实现观察者模式,还是有不同的,在Java里面实现,通常会把订阅者对象当成引用传入发布者对象中,同时订阅者对象还需提供一个名为诸如 update的方法,供发布者对象在适合的时候调用。而在 JavaScript中,我们用注册回调函数的形式来代替传统的发布 — 订阅模式。

命令模式

命令模式中的命令(command)指的是一个执行某些特定事情的指令。

命令模式的应用场景是:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么,此时希望用一种松耦合的方式来设计软件,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。

传统的面向对象的模式设计代码的方式是,假设html结构如下:

<button id="button1">刷新菜单目录button>
<button id="button2">增加子菜单button>
<button id="button3">删除子菜单button>

JavaScript的代码如下:

var b1 = document.getElementById("button1"),
    b2 = document.getElementById("button2"),
    b3 = document.getElementById("button3");

 // 定义setCommand 函数,该函数负责往按钮上面安装命令。点击按钮后会执行command对象的execute()方法。
 var setCommand = function(button,command){
    button.onclick = function(){
        command.execute();
    }
 };
 // 下面我们自己来定义各个对象来完成自己的业务操作
 var MenuBar = {
    refersh: function(){
        alert("刷新菜单目录");
    }
 };
 var SubMenu = {
    add: function(){
        alert("增加子菜单");
    },
    del: function(){
        alert("删除子菜单");
    }
 };
 // 下面是编写命令类
 var RefreshMenuBarCommand = function(receiver){
    this.receiver = receiver;
 };
 RefreshMenuBarCommand.prototype.execute = function(){
    this.receiver.refersh();
 }
 // 增加命令操作
 var AddSubMenuCommand = function(receiver) {
    this.receiver = receiver;
 };
 AddSubMenuCommand.prototype.execute = function() {
    this.receiver.add();
 }
 // 删除命令操作
 var DelSubMenuCommand = function(receiver) {
    this.receiver = receiver;
 };
 DelSubMenuCommand.prototype.execute = function(){
    this.receiver.del();
 }
 // 最后把命令接收者传入到command对象中,并且把command对象安装到button上面
 var refershBtn = new RefreshMenuBarCommand(MenuBar);
 var addBtn = new AddSubMenuCommand(SubMenu);
 var delBtn = new DelSubMenuCommand(SubMenu);

 setCommand(b1,refershBtn);
 setCommand(b2,addBtn);
 setCommand(b3,delBtn);

不过上述代码太过繁琐,用javascript的回调函数,接收者被封闭在回调函数产生的环境中,执行操作将会更加简单,仅仅执行回调函数即可。

var setCommand = function(button,func) {
    button.onclick = function(){
        func();
    }
 }; 
 var MenuBar = {
    refersh: function(){
        alert("刷新菜单界面");
    }
 };
 var SubMenu = {
    add: function(){
        alert("增加菜单");
    }
 };
 // 刷新菜单
 var RefreshMenuBarCommand = function(receiver) {
    return function(){
        receiver.refersh();    
    };
 };
 // 增加菜单
 var AddSubMenuCommand = function(receiver) {
    return function(){
        receiver.add();    
    };
 };
 var refershMenuBarCommand = RefreshMenuBarCommand(MenuBar);
 // 增加菜单
 var addSubMenuCommand = AddSubMenuCommand(SubMenu);
 setCommand(b1,refershMenuBarCommand);

 setCommand(b2,addSubMenuCommand);

职责链模式

职责链模式的定义是:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。

假设有这样的场景,我们负责一个售卖手机的电商网站,经过分别交纳 500元定金和 200元定金的两轮预定后(订单已在此时生成),现在已经到了正式购买的阶段。公司针对支付过定金的用户有一定的优惠政策。在正式购买后,已经支付过 500元定金的用户会收到 100元的商城优惠券,200元定金的用户可以收到 50元的优惠券,而之前没有支付定金的用户只能进入普通购买模式,也就是没有优惠券,且在库存有限的情况下不一定保证能买到。

  • orderType :表示订单类型(定金用户或者普通购买用户), code 的值为 1的时候是 500元定金用户,为 2的时候是 200元定金用户,为 3的时候是普通购买用户。
  • pay :表示用户是否已经支付定金,值为 true 或者 false , 虽然用户已经下过 500元定金的订单,但如果他一直没有支付定金,现在只能降级进入普通购买模式
  • stock :表示当前用于普通购买的手机库存数量,已经支付过 500 元或者 200 元定金的用户不受此限制

把这个流程代码化:

var order = function( orderType, pay, stock ){
if ( orderType === 1 ){ // 500 元定金购买模式
    if ( pay === true ){ // 已支付定金
        console.log( '500 元定金预购, 得到 100 优惠券' );
    }else{ // 未支付定金,降级到普通购买模式
        if ( stock > 0 ){ // 用于普通购买的手机还有库存
            console.log( '普通购买, 无优惠券' );
            }else{
                console.log( '手机库存不足' );
                }
            }
        }
        else if ( orderType === 2 ){ // 200 元定金购买模式
            if ( pay === true ){
                console.log( '200 元定金预购, 得到 50 优惠券' );
            }else{
                if ( stock > 0 ){
                    console.log( '普通购买, 无优惠券' );
                }else{
                    console.log( '手机库存不足' );
                }
            }
        }       else if ( orderType === 3 ){
            if ( stock > 0 ){
                console.log( '普通购买, 无优惠券' );
            }else{
                console.log( '手机库存不足' );
            }
        }
    };
    order( 1 , true, 500); // 输出: 500 元定金预购, 得到 100 优惠券

现在我们采用职责链模式重构这段代码,先把 500 元订单、200 元订单以及普通购买分成 3个函数。
接下来把 orderType 、 pay 、 stock 这 3个字段当作参数传递给 500元订单函数,如果该函数不符合处理条件,则把这个请求传递给后面的 200元订单函数,如果 200元订单函数依然不能处理该请求,则继续传递请求给普通购买函数,代码如下:

// 500 元订单
var order500 = function( orderType, pay, stock ){
    if ( orderType === 1 && pay === true ){
        console.log( '500 元定金预购, 得到 100 优惠券' );
    }else{
        order200( orderType, pay, stock ); // 将请求传递给 200 元订单
    }
};
// 200 元订单
var order200 = function( orderType, pay, stock ){
    if ( orderType === 2 && pay === true ){
        console.log( '200 元定金预购, 得到 50 优惠券' );
    }else{
        orderNormal( orderType, pay, stock ); // 将请求传递给普通订单
    }
};
// 普通购买订单
var orderNormal = function( orderType, pay, stock ){
    if ( stock > 0 ){
        console.log( '普通购买, 无优惠券' );
    }else{
        console.log( '手机库存不足' );
    }
};
// 测试结果:
order500( 1 , true, 500); // 输出:500 元定金预购, 得到 100 优惠券
order500( 1, false, 500 ); // 输出:普通购买, 无优惠券
order500( 2, true, 500 ); // 输出:200 元定金预购, 得到 500 优惠券
order500( 3, false, 500 ); // 输出:普通购买, 无优惠券
order500( 3, false, 0 ); // 输出:手机库存不足

可以看到,执行结果和前面那个巨大的 order 函数完全一样,但是代码的结构已经清晰了很多,我们把一个大函数拆分了 3个小函数,去掉了许多嵌套的条件分支语句。


你可能感兴趣的:(前端杂记)