MrErHu / blog

Star 就是最大的鼓励 👏👏👏

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

前端设计模式总结:(一)

MrErHu opened this issue · comments

什么是设计模式

设计模式一直都是程序员进阶绕不开的话题,有人将其奉为圣经,有人认为设计模式大多数都是形而上学的死板理论。Erich Gamma、Richard Helm 、Ralph Johnson、John Vlisside所组成的四人帮(GOF: Gang of Four)总结了面对对象语言的23种设计模式,将其写在《设计模式:可复用面向对象软件的基础》一书中,《设计模式》最初讲的是静态类型语言中的设计模式,大部分代码由C++写成,对于JavaScrip这种动态语言而言并不是全部适用,甚至很多模式内含在JavaScript语言内部,因此对于JavaScript的设计模式不能对已有模式生搬硬套,而是需要根据其内涵**进行灵活用运。

设计模式: 在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案

设计模式简单的讲就是针对特定问题的一种解决方案,这些设计模式并不是GOF的发明,而是早已长期存在于软件开发中,GOF所做的就将是这些问题抽象出来并给与恰当的命名。设计模式并不是能够解决任何问题的银弹,而是针对特定问题的解释方案,不仅如此部分设计模式可能还会带来代码量的增加,并且把系统的逻辑搞得更加复杂,因此对于设计模式而言,没有好与坏,只有是否适合于当前你的场景。

GOF所提出的二十三种设计模式可以分为三类:

  • 创建范例
  • 结构范例
  • 行为范例

其中创建范例是指关于如何创建范例的方式。而结构范例是指类与对象的复合关系。行为范例是指对象间如何联系和通讯的。本篇文章首先介绍几种常见的行为范例。

模板方法模式(Template Method Pattern)

模板方法模式是一种利用继承实现的非常简单的设计模式。模板方法模式由两部分构成,一部分是抽象父类,一部分是具体实现的子类。父类用来实现算法级的架构和子类方法的实现顺序,而子类用来实现具体的步骤逻辑。这样子类就能在不改变算法架构的情况下,重新定义算法中的某些步骤。

举一个最经典的咖啡与茶的例子,假设我们泡一杯咖啡的步骤是:

  • 把水煮沸
  • 沸水冲泡咖啡
  • 把咖啡倒进杯子
  • 加糖和牛奶

而泡一壶茶的步骤是:

  • 把水煮沸
  • 沸水冲泡茶叶
  • 把茶倒进杯子
  • 加柠檬

我们可以发现,泡一杯咖啡喝泡一杯茶的过程是大同小异的,经过抽象我们可以整理为以下四个步骤:

  • 煮沸水
  • 沸水冲泡特定材料
  • 将饮料倒进杯子
  • 加调料

因此从代码的角度而言,具体的步骤实现的逻辑可能有所不同,但实际的执行顺序确实相同的。因此我们可以在父类中提取出对应的模板方法:

var Beverage = function( param ){

    var boilWater = function(){
        console.log("煮沸水");
    };

    var brew = param.brew || function(){
        throw new Error("子类必须重写")
    };

    var pourInCup = param.pourInCup || function(){
        throw new Error("子类必须重写")
    };

    var addCondiments = param.addCondiments || function(){
        throw new Error("子类必须重写")
    };

    var F = function(){};

    F.prototype.init = function(){
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    };

    return F;
};

var Coffee = Beverage({
    brew: function(){
        console.log("沸水冲泡咖啡");
    },
    pourInCup: function(){
        console.log("把咖啡倒进杯子");
     },
     addCondiments: function(){
        console.log("加入糖和牛奶");
     }
});

var Tea = Beverage({
    brew: function(){
        console.log("沸水冲泡茶叶");
    },
    pourInCup: function(){
        console.log("把茶倒进杯子");
     },
     addCondiments: function(){
        console.log("加柠檬");
     }
});

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

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

模板方式模式在前端开发中,尤其是框架层面的设计中非常常见,例如React中,各个组件生命周期函数执行顺序总是不变的,是由React内部封装了模板方法,而各个组件的生命周期函数内部实现细节是不相同的。不仅如此,在模板方法模式中,某些方法足够特殊,可能需要跳过执行某些方法,这个时候我们就可以采用钩子函数(hook),是否需要挂钩,这是由子类自行决定的,钩子函数的返回决定了模板方法后面部分的执行步骤。例如,React就提供了shouldComponentUpdate这个方法,让子类自行决定是否需要执行接下来重新渲染的步骤。

模板方法模式能很好的将变化的逻辑封装在子类,而将不变的逻辑抽象到父类,我们通过增加新的子类,就能不断的为系统扩展新的功能,符合开发-闭合原则。

迭代器模式(Iterator Pattern)

迭代器模式指的是提供一种方法顺序访问聚合对象中的元素并且不暴露对象内部的表示。大部分语言都内置了迭代器实现,JavaScript从ES5就对数组提供了forEach的迭代器。

var arr = [1,2,3,4];
arr.forEach((value, index) => 
    console.log("index: ", index, " value: ", value)
)

迭代器分为内部迭代器和外部迭代器,内部迭代器是指内部已经定义好迭代规则,完全接手整个迭代调用。例如上面提到的forEach函数。

外部迭代器必须显式地请求迭代下一个元素,外部迭代器增加了调用的复杂度但增加了迭代器的灵活性。

var Iterator = function( obj ){
    var current = 0;

    var next = function(){
        current += 1;
    };

    var isDone = function(){
        return current >= obj.length;
    };

    var getCurrItem = function(){
        return obj[ current ];
    };

    return {
        next: next,
        isDone: isDone,
        getCurrItem: getCurrItem
        length: obj.length
    }
};

比如如果我们采用内迭代器模式比较两个数组是否相同,可能会是如下:

var compare = function( ary1, ary2 ){
    if ( ary1.length !== ary2.length ){
        return false;
    }
    ary1.forEach(, function( i, n ){
        if ( n !== ary2[ i ] ){
            return false;
        }
    });
    return true;
};

上面的比较因为借助了闭包,才能够实现比较的逻辑。对于其他的语言实现起来就相当的麻烦,但是如果采用外迭代器的话,实现起来就会相对灵活:

var compare = function( iterator1, iterator2 ){
    if(iterator1.length !== iterator2.length){
        return false;
    }
    while( !iterator1.isDone() && !iterator2.isDone() ){
        if ( iterator1.getCurrItem() !== iterator2.getCurrItem() ){
             return false;
        }
        iterator1.next();
        iterator2.next();
    }

   return true;
}

观察者模式(Observer Pattern)

观察者模式又称为发布-订阅者模式,用于定义对象间一对多的关系,当发布者改变时,所有的订阅者都会得到通知。观察者模式在前端开发中使用十分广泛,最常见的就是事件模型

document.addEventListener("click", function (){
  console.log("用户点击")
})

上面的代码就是最简单的DOM事件绑定,我们并不知道用户会在什么时候点击页面。我们只要订阅document.body上的click事件,当body节点被点击时,就会发布消息,也就是回调函数被执行。观察者模式可以将对象间互相显式调用接口的硬编码解耦,对象间不需要互相了解彼此细节。在MVC和MVVM中使用非常的广泛。

class Event {
    clientList = {};

    listen(key, fn){
        if ( !this.clientList[ key ] ){
            this.clientList[ key ] = [];
        }
        this.clientList[ key ].push(fn);
    }

    trigger(key, ...args){
        let fns = this.clientList[ key ];

        if ( !fns || fns.length === 0 ){
            return false;
        }

        for( var i = 0, fn; fn = fns[ i++ ]; ){
            fn.apply( this, args );
        }
    }
}

let event = new Event();
event.listen("click",  () => {
    console.log("click");
})

event.trigger("click");

上面是一个最简单的观察者模式,因为JavaScript是采用异步编程,并且函数作为一等公民,实现起来非常的简单。但是在传统的面对对象语言中,实现起来就稍微的麻烦一点。在Vue的Observer模块就实现了一个Emitter,用来处理事件模型:

function Emitter (ctx) {
  this._ctx = ctx || this
}

var p = Emitter.prototype

p.on = function (event, fn) {
  this._cbs = this._cbs || {}
  ;(this._cbs[event] || (this._cbs[event] = []))
    .push(fn)
  return this
}
// 三种模式 
// 不传参情况清空所有监听函数 
// 仅传事件名则清除该事件的所有监听函数
// 传递事件名和回调函数,则对应仅删除对应的监听事件
p.off = function (event, fn) {
  this._cbs = this._cbs || {}

  // all
  if (!arguments.length) {
    this._cbs = {}
    return this
  }

  // specific event
  var callbacks = this._cbs[event]
  if (!callbacks) return this

  // remove all handlers
  if (arguments.length === 1) {
    delete this._cbs[event]
    return this
  }

  // remove specific handler
  var cb
  for (var i = 0; i < callbacks.length; i++) {
    cb = callbacks[i]
    // 这边的代码之所以会有cb.fn === fn要结合once函数去看
    // 给once传递的监听函数其实已经被wrapped过
    // 但是仍然可以通过原来的监听函数去off掉
    if (cb === fn || cb.fn === fn) {
      callbacks.splice(i, 1)
      break
    }
  }
  return this
}
// 触发对应事件的所有监听函数,注意最多只能用给监听函数传递三个参数(采用call)
p.emit = function (event, a, b, c) {
  this._cbs = this._cbs || {}
  var callbacks = this._cbs[event]

  if (callbacks) {
    callbacks = callbacks.slice(0)
    for (var i = 0, len = callbacks.length; i < len; i++) {
      callbacks[i].call(this._ctx, a, b, c)
    }
  }

  return this
}
// 触发对应事件的所有监听函数,传递参数个数不受限制(采用apply)
p.applyEmit = function (event) {
  this._cbs = this._cbs || {}
  var callbacks = this._cbs[event], args

  if (callbacks) {
    callbacks = callbacks.slice(0)
    args = callbacks.slice.call(arguments, 1)
    for (var i = 0, len = callbacks.length; i < len; i++) {
      callbacks[i].apply(this._ctx, args)
    }
  }

  return this
}
// 通过调用on与off事件事件,在第一次触发之后就`off`对应的监听事件
p.once = function (event, fn) {
  var self = this
  this._cbs = this._cbs || {}

  function on () {
    self.off(event, on)
    fn.apply(this, arguments)
  }

  on.fn = fn
  this.on(event, on)
  return this
} 

观察者模式可以很好的实现对象之间的解耦,但是另一方面观察者模式又存在者非常明显的缺点,首先观察者模式会消耗时间和内存,并且如果过度使用的话,对象间的关联都会被掩盖,使得程序难以调试和理解,因为你可能很追溯到事件的起源。

职责链模式

职责链模式是指将请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。

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

如果将这个流程写成代码的话:

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优惠券

上面的代码庞大且难以阅读,并且扩展性差,当我们想要增加另一种购买模式时,只能够深入函数内部修改,这也是违反开闭原则的。当时当我们用职责链改写上面的代码时:

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 === 'nextSuccessor' ){
        return this.successor && this.successor.passRequest.apply( this.successor, arguments );
    }

    return ret;
};
// 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( '手机库存不足' );
    }
};
var chainOrder500 = new Chain( order500 );
var chainOrder200 = new Chain( order200 );
var chainOrderNormal = new Chain( orderNormal );
// 指定节点在职责链中的顺序
chainOrder500.setNextSuccessor( chainOrder200 );
chainOrder200.setNextSuccessor( chainOrderNormal );
// 最后把请求传递给第一个节点
chainOrder500.passRequest( 1, true, 500 );    // 输出:500元定金预购,得到100优惠券
chainOrder500.passRequest( 2, true, 500 );    // 输出:200元定金预购,得到50优惠券
chainOrder500.passRequest( 3, true, 500 );    // 输出:普通购买,无优惠券
chainOrder500.passRequest( 1, false, 0 );     // 输出:手机库存不足

上面的代码庞大且难以阅读,并且扩展性差,当我们想要增加另一种购买模式时,只能够深入函数内部修改,这也是通过将上面的代码改造成职责链,整个代码的逻辑就相对清晰了,并且如果此时要增加其他的处理逻辑,只需要增加其中的节点,然后重新设置链中相关节点的顺序即可。当前在JavaScript中我们也可以利用AOP的方式实现职责连,更加的简便

Function.prototype.after = function( fn ){
    var self = this;
    return function(){
        var ret = self.apply( this, arguments );
        if ( ret === 'nextSuccessor' ){
            return fn.apply( this, arguments );
        }

        return ret;
    }
};
var order = order500yuan.after( order200yuan ).after( orderNormal );

order( 1, true, 500 );    // 输出:500元定金预购,得到100优惠券
order( 2, true, 500 );    // 输出:200元定金预购,得到50优惠券
order( 1, false, 500 );   // 输出:普通购买,无优惠券

如下图,职责链模式能够很好的耦合请求发送者和接受请求者之前复杂的关系,但是存在明显的缺点,首先我们不能保证请求在职责链中一定会得到处理,因此我们最好在职责链末尾增加相应的步骤保底。并且职责链在程序中会增加节点,而大部分节点并没有实际的作用,从性能方面考虑,过长的职责链可能会带来性能的损耗。

中介者模式

中介者模式主要用来解除对象与对象之间N:N复杂的耦合关系,通过引入中介者,所有的对象都只与中介者对象关联,将复杂的N:N网状关系变为相对简单的1:N关联。

<body>
  选择颜色: 
  <select id="colorSelect">
    <option value="">请选择</option>
    <option value="red">红色</option>
    <option value="blue">蓝色</option>
  </select>
  选择内存: 
  <select id="memorySelect">
    <option value="">请选择</option>
    <option value="32G">32G</option>
    <option value="16G">16G</option>
  </select>
  输入购买数量: <input type="text" id="numberInput"/><br/>
  <!--输入部分结束-->
  
  您选择了颜色: <div id="colorInfo"></div><br/>
  您选择了内存: <div id="memoryInfo"></div><br/>
  您输入了数量: <div id="numberInfo"></div><br/>
  <button id="nextBtn" disabled="true">请选择手机颜色和购买数量</button>
<body>
// 各种手机库存(通常来自于后端,这里前端进行模拟)
var goods = { 
  "red|32G": 3,
  "red|16G": 0,
  "blue|32G": 1,
  "blue|16G": 6
};
// 中介者
var mediator = (function(){
  // 获得所有节点的引用,以便对其进行操作(中介者必许获得对其他对象的引用)
  var colorSelect = document.getElementById( 'colorSelect' ),
    memorySelect = document.getElementById( 'memorySelect' ),
    numberInput = document.getElementById( 'numberInput' ),
    colorInfo = document.getElementById( 'colorInfo' ),
    memoryInfo = document.getElementById( 'memoryInfo' ),
    numberInfo = document.getElementById( 'numberInfo' ),
    nextBtn = document.getElementById( 'nextBtn' );
  return {
    changed( obj ){
      var color = colorSelect.value, // 颜色
        memory = memorySelect.value,// 内存
        number = numberInput.value, // 数量
        stock = goods[ color + '|' + memory ]; // 颜色和内存对应的手机库存数量
      if ( obj === colorSelect ){ // 如果改变的是选择颜色下拉框
        colorInfo.innerHTML = color;
      }else if ( obj === memorySelect ){
        memoryInfo.innerHTML = memory;
      }else if ( obj === numberInput ){
        numberInfo.innerHTML = number;
      }
      if ( !color ){
        nextBtn.disabled = true;
        nextBtn.innerHTML = '请选择手机颜色';
        return;
      }
      if ( !memory ){
        nextBtn.disabled = true;
        nextBtn.innerHTML = '请选择内存大小';
        return;
      }
      if ( ( ( number - 0 ) | 0 ) !== number - 0 ){ // 输入购买数量是否为正整数
        nextBtn.disabled = true;
        nextBtn.innerHTML = '请输入正确的购买数量';
        return;
      }
      nextBtn.disabled = false;
      nextBtn.innerHTML = '放入购物车';
    }
  }
})();
// 与中介者联系起来,事件函数
colorSelect.onchange = function(){
  mediator.changed( this );
};
memorySelect.onchange = function(){
  mediator.changed( this );
};
numberInput.oninput = function(){
  mediator.changed( this );
};

通过引入中介者对象,所有的节点对象只跟中介者通信。当下拉选择框colorSelect、memorySelect和文本输入框numberInput发生了事件行为时,它们仅仅通知中介者它们被改变了,同时把自身当作参数传入中介者,以便中介者辨别是谁发生了改变。剩下的所有事情都交给中介者对象来完成,这样一来,无论是修改还是新增节点,都只需要改动中介者对象里的代码。

中介者模式使各个对象之间得以解耦,以中介者和对象之间的一对多关系取代了对象之间的网状多对多关系。各个对象只需关注自身功能的实现,对象之间的交互关系交给了中介者对象来实现和维护。中介者模式也存在一些缺点。其中,最大的缺点是系统中会新增一个中介者对象,因为对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者对象经常是巨大的。中介者对象自身往往就是一个难以维护的对象。

备注:上面代码来源于曾探的《JavaScript设计模式与开发实践》一书,描述的非常生动形象,非常推荐大家阅读,上面仅作为是本人的学习总结,望共同进步。