常用的几个设计模式

    15

原型模式

为JavaScript设计面向对象系统之初,Brendan Eich 就没有打算在JavaScript中加入类的概念。

在以类为中心的面向对象编程语言中,类和对象的关系可以想象成铸模和铸件的关系,对象总是从类中创建而来。而在原型编程的思想中,类并不是必需的,对象未必需要从类中创建而来, 一个对象是通过克隆另外一个对象所得到的

原型模式是用于创建对象的一种模式,如果我们想要创建一个对象, 一种方法是先指定它的类型,然后通过类来创建这个对象。原型模式选择了另外一种方式,我们 不再关心对象的具体类型,而是找到一个对象,然后通过克隆来创建一个一模一样的对象。

原型模式的实现关键,是语言本身是否提供了clone 方法。ECMAScript 5 提供了Object.create 方法,可以用来克隆对象。

但实际上,不需要调用Object.create,我们在JavaScript 遇到的每个对象,都是从Object.prototype 对象克隆而来的,Object.prototype 就是它们的原型。比如下面的obj1 对象和obj2 对象:

var obj1 = new Object();

var obj2 = {};

复制代码

在JavaScript 语言里,我们并不需要关心克隆的细节,因为这是引擎内部负责实现的。我们所需要做的只是显式地调用var obj1 = new Object() 或者 var obj2 = {}。此时,引擎内部会从 Object.prototype上面克隆一个对象出来,我们最终得到的就是这个对象。

再来看看如何用new 运算符从构造器中得到一个对象,下面的代码我们再熟悉不过了:

function Person( name ){

    this.name = name;

};

Person.prototype.getName = function(){

    return this.name;

};

var a = new Person( 'sven' )

console.log( a.name ); // 输出:sven

console.log( a.getName() ); // 输出:sven

console.log( Object.getPrototypeOf( a ) === Person.prototype );

复制代码

我们调用了new Person() ,但在这里Person并不是类,而是函数构造器,JavaScript的函数既可以作为普通函数被调用,也可以作为构造器被调用。当使用new 运算符来调用函数时,此时的函数就是一个构造器。

在Chrome 和Firefox 等向外暴露了对象__proto__属性的浏览器下,我们可以通过下面这段代码来理解new运算的过程:

function Person( name ){

    this.name = name;

};

Person.prototype.getName = function(){

    return this.name;

};

var objectFactory = function(){

    var obj = new Object(), // 从Object.prototype 上克隆一个空的对象

    Constructor = [].shift.call( arguments ); // 取得外部传入的构造器,此例是Person

    obj.__proto__ = Constructor.prototype; // 指向正确的原型

    var ret = Constructor.apply( obj, arguments ); // 借用外部传入的构造器给obj 设置属性

    return typeof ret === 'object' ? ret : obj; // 确保构造器总是会返回一个对象

};



var a = objectFactory( Person, 'sven' );

console.log( a.name ); // 输出:sven

console.log( a.getName() ); // 输出:sven

console.log( Object.getPrototypeOf( a ) === Person.prototype ); // 输出:true

复制代码

对于原型,我们很清楚的一点是当一个对象无法响应某个请求的时候,它会顺着原型链把请求传递下去,直到遇到一个可以处理该请求的对象为止。

单例模式

保证一个类仅有一个实例,并提供一个访问它的全局访问点

class Demo {



}



function getInstance() {

    let instance = null;

    return function () {

        if (!instance) {

            instance = new Demo();

        }

        return instance;

    }

}

const instance1 = getInstance();

const instance2 = getInstance();

策略模式

用于替换很多if else的场景

改造前

const calculateBonus = function (performanceLevel, salary) {

    if (performanceLevel === 'S') {

        return salary * 4;

    }

    if (performanceLevel === 'A') {

        return salary * 3;

    }

    if (performanceLevel === 'B') {

        return salary * 2;

    }

};

console.log(calculateBonus('B', 20000))

console.log(calculateBonus('S', 6000))

改造后

const strategies = {

    "S": function (salary) {

        return salary * 4;

    },

    "A": function (salary) {

        return salary * 3;

    },

    "B": function (salary) {

        return salary * 2;

    }

};

const calculateBonus = function (level, salary) {

    return strategies[level](salary);

};

console.log(calculateBonus('S', 20000))

console.log(calculateBonus('A', 10000))

状态模式

状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变。

我们来想象这样一个场景:有一个电灯,电灯上面只有一个开关。当电灯开着的时候,此时按下开关,电灯会切换到关闭状态;再按一次开关,电灯又将被打开。同一个开关按钮,在不同的状态下,表现出来的行为是不一样的。

首先给出不用状态模式的电灯程序实现:

    var Light = function () {

      this.state = 'off'; // 给电灯设置初始状态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.buttonWasPressed();

      }

    };



    Light.prototype.buttonWasPressed = function () {

      if (this.state === 'off') {

        console.log('开灯');

        this.state = 'on';

      } else if (this.state === 'on') {

        console.log('关灯');

        this.state = 'off';

      }

    };

    var light = new Light();

    light.init();

复制代码

令人遗憾的是,这个世界上的电灯并非只有一种。许多酒店里有另外一种电灯,这种电灯也只有一个开关,但它的表现是:第一次按下打开弱光,第二次按下打开强光,第三次才是关闭电灯。而这些必须改造上面的代码来完成这种新型电灯的制造。

但将状态改变的逻辑全部写在buttonWasPressed中存在以下问题:

  1. 违反开放-封闭原则的,每次新增或者修改light的状态,都需要改动buttonWasPressed方法中的代码。
  2. 所有跟状态有关的行为,都被封装在buttonWasPressed方法里,我们将无法预计这个方法膨胀到什么地步。
  3. 状态的切换非常不明显,仅仅表现为对state变量赋值,我们也没有办法一目了然地明白电灯一共有多少种状态。
  4. 状态之间的切换关系,不过是往buttonWasPressed方法里堆砌ifelse语句,这使buttonWasPressed更加难以阅读和维护

现在我们学习使用状态模式改进电灯的程序。状态模式的关键是把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部,所以button被按下的的时候,只需要在上下文中,把这个请求委托给当前的状态对象即可,该状态对象会负责渲染它自身的行为。

    // OffLightState:

    var OffLightState = function (light) {

      this.light = light;

    };

    OffLightState.prototype.buttonWasPressed = function () {

      console.log('弱光'); // offLightState 对应的行为

      this.light.setState(this.light.weakLightState); // 切换状态到weakLightState

    };

    // WeakLightState:

    var WeakLightState = function (light) {

      this.light = light;

    };

    WeakLightState.prototype.buttonWasPressed = function () {

      console.log('强光'); // weakLightState 对应的行为

      this.light.setState(this.light.strongLightState); // 切换状态到strongLightState

    };

    // StrongLightState:

    var StrongLightState = function (light) {

      this.light = light;

    };

    StrongLightState.prototype.buttonWasPressed = function () {

      console.log('关灯'); // strongLightState 对应的行为

      this.light.setState(this.light.offLightState); // 切换状态到offLightState

    };



    var Light = function () {

      this.offLightState = new OffLightState(this);

      this.weakLightState = new WeakLightState(this);

      this.strongLightState = new StrongLightState(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();

      }

    };

    Light.prototype.setState = function (newState) {

      this.currState = newState;

    };

    var light = new Light();

    light.init();

复制代码

执行结果跟之前的代码一致,但是使用状态模式的好处很明显,它可以使每一种状态和它对应的行为之间的关系局部化,这些行为被分散和封装在各自对应的状态类之中,便于阅读和管理代码。

另外,状态之间的切换都被分布在状态类内部,这使得我们无需编写过多的ifelse条件分支语言来控制状态之间的转换。

适配器模式

适配器模式的作用是解决两个软件实体间的接口不兼容的问题。使用适配器模式之后,原本由于接口不兼容而不能工作的两个软件实体可以一起工作。

假设我们要用googleMapbaiduMap分别以各自的方式在页面中展现地图。

    var googleMap = {

      show: function () {

        console.log('开始渲染谷歌地图');

      }

    };

    var baiduMap = {

      display: function () {

        console.log('开始渲染百度地图');

      }

    };

    var renderMap = function (map) {

      if (map.show instanceof Function) {

        map.show();

      }

    };

复制代码

由于两个地图的展示方法不同,所以不能直接调用renderMap函数,因此我们要为百度地图提供一个适配器。这样我们就可以同时渲染2个地图了。

    var baiduMapAdapter = {

      show: function () {

        return baiduMap.display();

      }

    };

    renderMap(googleMap); // 输出:开始渲染谷歌地图

    renderMap(baiduMapAdapter); // 输出:开始渲染百度地图

发布订阅模式

状态变化需要通知其他事件

    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); // (2) // arguments 是发布消息时带上的参数

      }

    };



    salesOffices.listen(function (price, squareMeter) { // 小明订阅消息

      console.log('价格= ' + price);

      console.log('squareMeter= ' + squareMeter);

    });

    salesOffices.listen(function (price, squareMeter) { // 小红订阅消息

      console.log('价格= ' + price);

      console.log('squareMeter= ' + squareMeter);

    });

    salesOffices.trigger(2000000, 88); // 输出:200 万,88 平方米

    salesOffices.trigger(3000000, 110); // 输出:300 万,110 平方米
评论区
共有评论 0
暂无评论