JavaScript 设计模式学习笔记(六)

六月 7, 20263读书笔记JavaScript

很早之前看《JavaScript 设计模式与开发实践》整理的笔记,把这些笔记搬到这里,仅供个人学习记录使用,如有需要请支持正版图书,侵删。

状态模式

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

第一个例子:电灯程序

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();
状态模式改进:
通常我们谈到封装,一般都会优先封装对象的行为,而不是对象的状态。
但在状态模式中刚好相反,状态模式的关键是把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部。
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
};

// 接下来改写Light类,现在不再使用一个字符串来记录当前的状态,
// 而是使用更加立体化的状态对象
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();

状态模式的定义

状态模式的定义:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
  1. 将状态封装成独立的类,并将请求委托给当前的状态对象,当对象的内部状态改变时,会带来不同的行为变化。
  2. 从客户的角度来看,我们使用的对象,在不同的状态下具有截然不同的行为,这个对象看起来是从不同的类中实例化而来的,实际上这是使用了委托的效果。

状态模式优缺点

优点:
  • 状态模式定义了状态与行为之间的关系,并将它们封装在一个类里。通过增加新的状态类,很容易增加新的状态和转换。
  • 避免Context无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了Context中原本过多的条件分支。
  • 用对象代替字符串来记录当前状态,使得状态的切换更加一目了然。
  • Context中的请求动作和状态类中封装的行为可以非常容易地独立变化而互不影响。
缺点:
  • 状态模式的缺点是会在系统中定义许多状态类,编写20个状态类是一项枯燥乏味的工作,而且系统中会因此而增加不少对象。
  • 由于逻辑分散在状态类中,虽然避开了不受欢迎的条件分支语句,但也造成了逻辑分散的问题,我们无法在一个地方就看出整个状态机的逻辑。

状态模式中的性能优化点

有两种选择来管理state对象的创建和销毁。
  1. 仅当state对象被需要时才创建并随后销毁
  2. 一开始就创建好所有的状态对象,并且始终不销毁它们
可以用第一种方式来节省内存,这样可以避免创建一些不会用到的对象并及时地回收它们。但如果状态的改变很频繁,最好一开始就把这些state对象都创建出来,也没有必要销毁它们,因为可能很快将再次用到它们。
state对象之间是可以共享的,各Context对象可以共享一个state对象,这也是享元模式的应用场景之一

状态模式和策略模式

状态模式和策略模式像一对双胞胎,它们都封装了一系列的算法或者行为,它们的类图看起来几乎一模一样,但在意图上有很大不同,因此它们是两种迥然不同的模式。
相同点:策略模式和状态模式的相同点是,它们都有一个上下文、一些策略或者状态类,上下文把请求委托给这些类来执行。
区别:策略模式中的各个策略类之间是平等又平行的,它们之间没有任何联系,所以客户必须熟知这些策略类的作用,以便客户可以随时主动切换算法;而在状态模式中,状态和状态对应的行为是早已被封装好的,状态之间的切换也早被规定完成,“改变行为”这件事情发生在状态模式内部。对客户来说,并不需要了解这些细节。

JavaScript 版本的状态机

JavaScript 这种“无类”语言中,没有规定让状态对象一定要从类中创建而来。
另外一点,JavaScript 可以非常方便地使用委托技术,并不需要事先让一个对象持有另一个对象。
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); // 把请求委托给FSM状态机
  };
};

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();
另外一种方法,即利用下面的 delegate 函数来完成这个状态机编写
var delegate = function (client, delegation) {
  return {
    buttonWasPressed: function () {
      // 将客户的操作委托给delegation对象
      return delegation.buttonWasPressed.apply(client, arguments);
    },
  };
};

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

var Light = function () {
  this.offState = delegate(this, FSM.off);
  this.onState = delegate(this, FSM.on);
  this.currState = this.offState; // 设置初始状态为关闭状态
  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();
  };
};
var light = new Light();
light.init();

适配器模式

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

适配器模式的应用

如果现有的接口已经能够正常工作,那我们就永远不会用上适配器模式。适配器模式是一种“亡羊补牢”的模式,没有人会在程序的设计之初就使用它。
// 当我们向googleMap和baiduMap都发出“显示”请求时,googleMap和baiduMap分别
// 以各自的方式在页面中展现了地图:
var googleMap = {
  show: function () {
    console.log('开始渲染谷歌地图');
  },
};

var baiduMap = {
  show: function () {
    console.log('开始渲染百度地图');
  },
};

var renderMap = function (map) {
  if (map.show instanceof Function) {
    map.show();
  }
};

renderMap(googleMap); // 输出:开始渲染谷歌地图
renderMap(baiduMap); // 输出:开始渲染百度地图
假如baiduMap提供的显示地图的方法不叫show而叫display呢?
baiduMap这个对象来源于第三方,正常情况下我们都不应该去改动它。此时我们可以通过增加baiduMapAdapter来解决问题:
var googleMap = {
  show: function () {
    console.log('开始渲染谷歌地图');
  },
};

var baiduMap = {
  display: function () {
    console.log('开始渲染百度地图');
  },
};

var baiduMapAdapter = {
  show: function () {
    return baiduMap.display();
  },
};

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

小结

有一些模式跟适配器模式的结构非常相似,比如装饰者模式、代理模式和外观模式。这几种模式都属于“包装模式”,都是由一个对象来包装另一个对象。
区别它们的关键仍然是模式的意图。
  • 适配器模式主要用来解决两个已有接口之间不匹配的问题,它不考虑这些接口是怎样实现的,也不考虑它们将来可能会如何演化。适配器模式不需要改变已有的接口,就能够使它们协同作用。
  • 装饰者模式和代理模式也不会改变原有对象的接口,但装饰者模式的作用是为了给对象增加功能。装饰者模式常常形成一条长的装饰链,而适配器模式通常只包装一次。代理模式是为了控制对对象的访问,通常也只包装一次。
  • 外观模式的作用倒是和适配器比较相似,有人把外观模式看成一组对象的适配器,但外观模式最显著的特点是定义了一个新的接口。
END

评论

欢迎分享你的看法

发表评论
欢迎留言,请友好互动
点击添加表情:

全部评论

0

还没有评论

来说两句吧!