设计模式读书笔记之适配器模式、装饰者模式

适配器模式和装饰者模式

适配器模式

适配器模式是将一个类(对象)的接口(方法或者属性)转化成另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类(对象)可以一起工作

举个例子:

飞机类和火车类,他们都是交通运输工具,都适用于中长途,但就行驶方式来说,火车是在地上跑的,飞机是在天上飞的。如果要让火车在天上飞(flying),则可以复用飞机的飞行功能,但其具体的行驶动作还是应该在地上跑(running),此时,我们就可以创建一个火车的适配器,能够让火车也支持 flying 方法,但其内部还是调用的 running

  • 首先定义飞机和火车的抽象类函数
// 抽象工厂方法(实现子类继承抽象类的工厂)
var VehicleFactory = function (subType, superType) {
  // 判断抽象工厂中是否有该抽象类
  if (typeof VehicleFactory[superType] === "function") {
    // 缓存类(寄生式继承)
    function F() {}
    // 继承父类属性和方法
    F.prototype = VehicleFactory[superType].prototype;
    // 子类原型继承父类
    var p = new F();
    // 将子类constructor指向子类
    p.constructor = subType;
    // 设置子类的原型
    subType.prototype = p;
  } else {
    // 不存在改抽象类则抛出错误
    throw new Error("未创建该抽象类");
  }
};

// 飞机抽象类(抽象类是一种声明但不能使用的类)
VehicleFactory.Airplane = function () {};
VehicleFactory.Airplane.prototype = {
  flying: function () {
    throw new Error("该方法未定义!");
  },
  transportation: function () {
    throw new Error("该方法未定义!");
  },
};

// 火车抽象类
VehicleFactory.Train = function () {};
VehicleFactory.Train.prototype = {
  running: function () {
    throw new Error("该方法未定义!");
  },
  transportation: function () {
    throw new Error("该方法未定义!");
  },
};
  • 定义具体的火车和飞机的构造函数
// 飞机
var CivilAircraft = function () {
  VehicleFactory.Airplane.call(this);
};
VehicleFactory(CivilAircraft, "Airplane"); // 原型是Airplane
CivilAircraft.prototype.transportation = function () {
  console.log("速度很快的交通工具!");
};
CivilAircraft.prototype.flying = function () {
  console.log("能够飞起来!");
};

// 火车
var CivilTrain = function () {
  VehicleFactory.Train.call(this);
};
VehicleFactory(CivilTrain, "Train"); // 原型是Train
CivilTrain.prototype.transportation = function () {
  console.log("比飞机慢的交通工具!");
};
CivilTrain.prototype.running = function () {
  console.log("在地上驰骋!");
};
  • 为了让火车能够飞起来,让火车也支持 flying 方法,就需要创建一个新的火车适配器 TrainAdapter
// 火车适配器
var TrainAdapter = function (oTrain) {
  VehicleFactory.Airplane.call(this);
  this.oTrain = oTrain;
};
TrainAdapter.prototype = new VehicleFactory.Airplane();
TrainAdapter.prototype.flying = function () {
  this.oTrain.running();
};
TrainAdapter.prototype.transportation = function () {
  this.oTrain.transportation();
};

该构造函数接受一个火车的实例对象,然后使用 VehicleFactory.Airplane 进行 apply,其适配器原型是 VehicleFactory.Airplane,然后要重新修改其原型的 flying 方法,以便内部调用 oTrain.running()方法

  • 再测试一下飞机、火车及适配器的行为
var oCivilAircraft = new CivilAircraft();
var oCivilTrain = new CivilTrain();
var oTrainAdapter = new TrainAdapter(oCivilTrain);

//原有的飞机行为
oCivilAircraft.flying(); // 能够飞起来!
oCivilAircraft.transportation(); // 速度很快的交通工具!

//原有的火车行为
oCivilTrain.running(); // 在地上驰骋!
oCivilTrain.transportation(); // 比飞机慢的交通工具!

//适配器火车的行为(火车调用飞机的方法名称)
oTrainAdapter.transportation(); // 比飞机慢的交通工具!
oTrainAdapter.flying(); // 在地上驰骋!

验证成功,但是此处也暴露出这种适配器有一个问题,就是火车类原本的 transportation 方法在适配器中也需要重写一遍。如果需要适配器在继承火车类的基础上扩展所有飞机类的行为时,可以考虑使用多继承

适配器还有一些其他应用,在《JavaScript设计模式》这本书上是以 A 框架适配 JQuery 框架做介绍的。如果两个框架的api非常的相似,那么用 window.A = A = jQuery即可实现两个框架的适配,这样可以在不改变原来代码的情况下正确的运行新框架的接口。除此之外,适配器模式还可用于:

  • 使用一个已经存在的对象,但其方法或属性接口不符合你的要求;

  • 前后端数据传递,把后端数据适配成我们可用的数据格式再使用;

  • 你想创建一个可复用的对象,该对象可以与其它不相关的对象或不可见对象(即接口方法或属性不兼容的对象)协同工作;

  • 想使用已经存在的对象,但是不能对每一个都进行原型继承以匹配它的接口。对象适配器可以适配它的父对象接口方法或属性。

装饰者模式

装饰者模式就是在不改变原对象的基础上,通过对其进行包装扩展(添加属性或者方法)使原有对象可以满足更复杂的需求

举个例子:

现在有一个前人完成的项目,产品经理说要加新需求,当用户点击输入框时,如果输入框输入的内容有限制,那么在输入框下提示相应文案,且不同的输入框提示文案不相同。如名字输入框提示输入数字字母,电话输入框提示输入纯数字等等。如果输入框很多,那么一条一条查找代码并进行修改将非常麻烦

这时候我们可以使用装饰者模式,在原有的功能的基础上添加一些新功能来满足需求,这时候我们就不需要重写或者修改原来定义的方法

// 装饰者
var decorator = function (input, fn) {
  // 获取事件源
  var input = document.getElementById(input);
  // 如果事件源已经绑定事件
  if (typeof input.onclick === "function") {
    // 缓存事件源原有回调函数
    var oldClickFn = input.onClick;
    // 为事件源定义新的事件
    input.onClick = function () {
      // 事件源原有回调函数
      oldClickFn();
      // 执行事件源新增回调函数
      fn();
    };
  } else {
    // 事件源未绑定事件,直接为事件源添加新增回调函数
    input.onClick = fn;
  }
};

此时,我们在原有基础上为项目添加新功能时,可以不用深入了解原来的代码,只要调用这个装饰者并传入你新增的方法就可以了

// 姓名框新增功能
decorator('name_input', function() {
  console.log('姓名输入框只能输入汉字和英文!')
})
// 电话框新增功能
decorator('tel_input', function() {
  console.log('电话输入框只能输入纯数字!)
})

装饰者模式很简单,就是对原有对象的属性和方法的添加。但是装饰者模式很强大,因为它可以对原有功能进行扩展,比如在一些框架中对浏览器原有方法的扩展再封装就是用到了装饰者这个模式。

适配器模式和装饰者模式的差别

  • 适配器模式调用新增的方法时,虽然方法名字不同,但是调用的还是原来的方法,需要了解原有方法的具体细节
  • 装饰者模式中,新增一个方法可以不用考虑原有方法的实现细节,在原封不动的保持原有方法的前提下,新增我们自己的方法

学习中发现的问题:在适配器模式中,书中写到

window.A = A = jQuery;

这个连等式是怎么运行的呢?下面这个程序运行结果是什么呢?

var a = { n: 1 };
a.x = a = { n: 2 };
console.log(a.x); // 输出?

上面这个问题也很有趣,它输出的答案是undefined,为什么呢?因为在开始运行的时候,它初始化了一个a.x的变量,并且它的值为undefined,且这个a的值指向的是原来的{ n: 1},而赋值号(=)是从右到左执行,因此此时右边的a = {n: 2},a指向了一个新的地址,该地址里面保存的值为{ n: 2},然后a.x又被赋了一个值为{n: 2},注意此a非彼a,我们可以用一个中间变量看一下:
image

参考:
《Javascript 设计模式》 – 张荣铭
JS 适配器模式