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

六月 7, 20264读书笔记JavaScript

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

组合模式

组合模式就是用小的子对象来构建更大的对象,而这些小的子对象本身也许是由更小的“孙对象”构成的

组合模式的用途

组合模式将对象组合成树形结构,以表示“部分-整体”的层次结构。除了用来表示树形结构之外,组合模式的另一个好处是通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性
  1. 表示树形结构
  2. 利用对象多态性统一对待组合对象和单个对象

请求在树中传递的过程

在组合模式中,请求在树中传递的过程总是遵循一种逻辑
以宏命令为例,请求从树最顶端的对象往下传递,如果当前处理请求的对象是叶对象(普通子命令),叶对象自身会对请求作出相应的处理;如果当前处理请求的对象是组合对象(宏命令),组合对象则会遍历它属下的子节点,将请求继续传递给这些子节点

更强大的宏命令

现在我们需要一个“超级万能遥控器”,可以控制家里所有的电器,这个遥控器拥有以下功能:
  • 打开空调
  • 打开电视和音箱
  • 关门、打开电脑、登录QQ
var MacroCommand = function () {
  return {
    commandsList: [],
    add: function (command) {
      this.commandsList.push(command);
    },
    execute: function () {
      for (var i = 0, command; (command = this.commandsList[i++]); ) {
        command.execute();
      }
    },
  };
};

var openAcCommand = {
  execute: function () {
    console.log('打开空调');
  },
};
/**********
  家里的电视和音响是连接在一起的,所以可以用一个宏命令来组合打开电视和打开音响的命令
 *********/

var openTvCommand = {
  execute: function () {
    console.log('打开电视');
  },
};

var openSoundCommand = {
  execute: function () {
    console.log('打开音响');
  },
};

var macroCommand1 = MacroCommand();
macroCommand1.add(openTvCommand);
macroCommand1.add(openSoundCommand);

/********* 关门、打开电脑和打登录QQ的命令 ****************/

var closeDoorCommand = {
  execute: function () {
    console.log('关门');
  },
};

var openPcCommand = {
  execute: function () {
    console.log('开电脑');
  },
};

var openQQCommand = {
  execute: function () {
    console.log('登录QQ');
  },
};

var macroCommand2 = MacroCommand();
macroCommand2.add(closeDoorCommand);
macroCommand2.add(openPcCommand);
macroCommand2.add(openQQCommand);

/********* 现在把所有的命令组合成一个“超级命令” **********/

var macroCommand = MacroCommand();
macroCommand.add(openAcCommand);
macroCommand.add(macroCommand1);
macroCommand.add(macroCommand2);

/********* 最后给遥控器绑定“超级命令” **********/

var setCommand = (function (command) {
  document.getElementById('button').onclick = function () {
    command.execute();
  };
})(macroCommand);
基本对象可以被组合成更复杂的组合对象,组合对象又可以被组合,这样不断递归下去,这棵树的结构可以支持任意多的复杂度。

透明性带来的安全问题

组合模式的透明性使得发起请求的客户不用去顾忌树中组合对象和叶对象的区别,但它们在本质上有是区别的。
组合对象可以拥有子节点,叶对象下面就没有子节点,所以我们也许会发生一些误操作,比如试图往叶对象中添加子节点。解决方案通常是给叶对象也增加 add 方法,并且在调用这个方法时,抛出一个异常来及时提醒客户
var MacroCommand = function () {
  return {
    commandsList: [],
    add: function (command) {
      this.commandsList.push(command);
    },
    execute: function () {
      for (var i = 0, command; (command = this.commandsList[i++]); ) {
        command.execute();
      }
    },
  };
};

var openTvCommand = {
  execute: function () {
    console.log('打开电视');
  },
  add: function () {
    throw new Error('叶对象不能添加子节点');
  },
};
var macroCommand = MacroCommand();
macroCommand.add(openTvCommand);
openTvCommand.add(macroCommand); // Uncaught Error: 叶对象不能添加子节点

一些值得注意的地方

  • 组合模式不是父子关系
  • 对叶对象操作的一致性
  • 双向映射关系:
  • 用职责链模式提高组合模式性能

何时使用组合模式

  • 表示对象的部分-整体层次结构。组合模式可以方便地构造一棵树来表示对象的部分-整体结构。
  • 客户希望统一对待树中的所有对象。组合模式使客户可以忽略组合对象和叶对象的区别,客户在面对这棵树的时候,不用关心当前正在处理的对象是组合对象还是叶对象,也就不用写一堆ifelse语句来分别处理它们。组合对象和叶对象会各自做自己正确的事情,这是组合模式最重要的能力。

模板方法模式

模板方法模式是一种只需使用继承就可以实现的非常简单的模式。
模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。

咖啡与茶

泡一杯咖啡
var Coffee = function () {};
Coffee.prototype.boilWater = function () {
  console.log('把水煮沸');
};

Coffee.prototype.brewCoffeeGriends = function () {
  console.log('用沸水冲泡咖啡');
};

Coffee.prototype.pourInCup = function () {
  console.log('把咖啡倒进杯子');
};

Coffee.prototype.addSugarAndMilk = function () {
  console.log('加糖和牛奶');
};

Coffee.prototype.init = function () {
  this.boilWater();
  this.brewCoffeeGriends();
  this.pourInCup();
  this.addSugarAndMilk();
};

var coffee = new Coffee();
coffee.init();
泡一杯茶
var Tea = function () {};

Tea.prototype.boilWater = function () {
  console.log('把水煮沸');
};

Tea.prototype.steepTeaBag = function () {
  console.log('用沸水浸泡茶叶');
};

Tea.prototype.pourInCup = function () {
  console.log('把茶水倒进杯子');
};

Tea.prototype.addLemon = function () {
  console.log('加柠檬');
};

Tea.prototype.init = function () {
  this.boilWater();
  this.steepTeaBag();
  this.pourInCup();
  this.addLemon();
};

var tea = new Tea();
tea.init();
分离共同点
var Beverage = function () {};
Beverage.prototype.boilWater = function () {
  console.log('把水煮沸');
};

Beverage.prototype.brew = function () {}; // 空方法,应该由子类重写

Beverage.prototype.pourInCup = function () {}; // 空方法,应该由子类重写

Beverage.prototype.addCondiments = function () {}; // 空方法,应该由子类重写

Beverage.prototype.init = function () {
  this.boilWater();
  this.brew();
  this.pourInCup();
  this.addCondiments();
};
创建 Coffee 和 Tea 子类
var Coffee = function () {};

Coffee.prototype = new Beverage();

Coffee.prototype.brew = function () {
  console.log('用沸水冲泡咖啡');
};

Coffee.prototype.pourInCup = function () {
  console.log('把咖啡倒进杯子');
};
Coffee.prototype.addCondiments = function () {
  console.log('加糖和牛奶');
};

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

// Tea
var Tea = function () {};

Tea.prototype = new Beverage();

Tea.prototype.brew = function () {
  console.log('用沸水浸泡茶叶');
};

Tea.prototype.pourInCup = function () {
  console.log('把茶倒进杯子');
};

Tea.prototype.addCondiments = function () {
  console.log('加柠檬');
};

var tea = new Tea();
tea.init();
在上面的例子中,到底谁才是所谓的模板方法呢?答案是Beverage.prototype.init
Beverage.prototype.init被称为模板方法的原因是,该方法中封装了子类的算法框架,它作为一个算法的模板,指导子类以何种顺序去执行哪些方法。在Beverage.prototype.init方法中,算法内的每一个步骤都清楚地展示在我们眼前。

抽象类

模板方法模式是一种严重依赖抽象类的设计模式。JavaScript 在语言层面并没有提供对抽象类的支持,我们也很难模拟抽象类的实现。
在 Java 中,类分为两种,一种为具体类,另一种为抽象类。具体类可以被实例化,抽象类不能被实例化。
抽象类的作用:
  • 抽象类和接口一样可以用于向上转型
  • 除了用于向上转型,抽象类也可以表示一种契约
用 Java 实现
public abstract class Beverage {    // 饮料抽象类
  final void init(){    // 模板方法
    boilWater();
    brew();
    pourInCup();
    addCondiments();
  }

  void boilWater(){        // 具体方法boilWater
    System.out.println( "把水煮沸" );
  }

  abstract void brew();        // 抽象方法brew
  abstract void addCondiments();        // 抽象方法addCondiments
  abstract void pourInCup();        // 抽象方法pourInCup
}

public class Coffee extends Beverage{        // Coffee类
  @Override
  void brew() {    // 子类中重写brew方法
    System.out.println( "用沸水冲泡咖啡" );
  }

  @Override
  void pourInCup(){    // 子类中重写pourInCup方法
    System.out.println( "把咖啡倒进杯子" );
  }

  @Override
  void addCondiments() {        // 子类中重写addCondiments方法
    System.out.println( "加糖和牛奶" );
  }
}

public class Tea extends Beverage{        // Tea类
  @Override
  void brew() {        // 子类中重写brew方法
    System.out.println( "用沸水浸泡茶叶" );
  }

  @Override
  void pourInCup(){        // 子类中重写pourInCup方法
      System.out.println( "把茶倒进杯子" );
  }

  @Override
  void addCondiments() {        // 子类中重写addCondiments方法
      System.out.println( "加柠檬" );
  }
}

public class Test {

  private static void prepareRecipe( Beverage beverage ){
      beverage.init();
  }

  public static void main( String args[] ){
      Beverage coffee = new Coffee();   // 创建coffee对象
      prepareRecipe( coffee );   // 开始泡咖啡
      // 把水煮沸
      // 用沸水冲泡咖啡
      // 把咖啡倒进杯子
      // 加糖和牛奶

      Beverage tea = new Tea();   // 创建tea对象
      prepareRecipe( tea );   // 开始泡茶
      // 把水煮沸
      // 用沸水浸泡茶叶
      // 把茶倒进杯子
      // 加柠檬
  }
}
JavaScript 并没有从语法层面提供对抽象类的支持。
两种变通的解决方案:
  1. 用鸭子类型来模拟接口检查,以便确保子类中确实重写了父类的方法。
  2. Beverage.prototype.brew等方法直接抛出一个异常,如果因为粗心忘记编写Coffee.prototype.brew方法,那么至少我们会在程序运行时得到一个错误

使用场景

从大的方面来讲,模板方法模式常被架构师用于搭建项目的框架,架构师定好了框架的骨架,程序员继承框架的结构之后,负责往里面填空
Web开发中也能找到很多模板方法模式的适用场景,比如我们在构建一系列的UI组件,这些组件的构建过程一般如下所示:
  1. 初始化一个div容器
  2. 通过ajax请求拉取相应的数据
  3. 把数据渲染到div容器里面,完成组件的构造
  4. 通知用户组件渲染完毕
可以把这4个步骤都抽象到父类的模板方法里面,父类中还可以顺便提供第(1)步和第(4)步的具体实现。当子类继承这个父类之后,会重写模板方法里面的第(2)步和第(3)步。

钩子方法

通过模板方法模式,在父类中封装了子类的算法框架。这些算法框架在正常状态下是适用于大多数子类的,但如果有一些特别“个性”的子类呢?
钩子方法(hook)可以用来解决这个问题,放置钩子是隔离变化的一种常见手段。
var Beverage = function () {};

Beverage.prototype.boilWater = function () {
  console.log('把水煮沸');
};

Beverage.prototype.brew = function () {
  throw new Error('子类必须重写brew方法');
};

Beverage.prototype.pourInCup = function () {
  throw new Error('子类必须重写pourInCup方法');
};

Beverage.prototype.addCondiments = function () {
  throw new Error('子类必须重写addCondiments方法');
};

// hook
Beverage.prototype.customerWantsCondiments = function () {
  return true; // 默认需要调料
};

Beverage.prototype.init = function () {
  this.boilWater();
  this.brew();
  this.pourInCup();
  if (this.customerWantsCondiments()) {
    // 如果挂钩返回true,则需要调料
    this.addCondiments();
  }
};

var CoffeeWithHook = function () {};

CoffeeWithHook.prototype = new Beverage();

CoffeeWithHook.prototype.brew = function () {
  console.log('用沸水冲泡咖啡');
};

CoffeeWithHook.prototype.pourInCup = function () {
  console.log('把咖啡倒进杯子');
};

CoffeeWithHook.prototype.addCondiments = function () {
  console.log('加糖和牛奶');
};

CoffeeWithHook.prototype.customerWantsCondiments = function () {
  return window.confirm('请问需要调料吗?');
};

var coffeeWithHook = new CoffeeWithHook();
coffeeWithHook.init();

JavaScript 版本

JavaScript 语言实际上没有提供真正的类式继承,继承是通过对象与对象之间的委托来实现的
var Beverage = function (param) {
  var boilWater = function () {
    console.log('把水煮沸');
  };

  var brew =
    param.brew ||
    function () {
      throw new Error('必须传递brew方法');
    };

  var pourInCup =
    param.pourInCup ||
    function () {
      throw new Error('必须传递pourInCup方法');
    };

  var addCondiments =
    param.addCondiments ||
    function () {
      throw new Error('必须传递addCondiments方法');
    };

  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();
模板方法模式是一种典型的通过封装变化提高系统扩展性的设计模式。在传统的面向对象语言中,一个运用了模板方法模式的程序中,子类的方法种类和执行顺序都是不变的,所以我们把这部分逻辑抽象到父类的模板方法里面。而子类的方法具体怎么实现则是可变的,于是我们把这部分变化的逻辑封装到子类中。

享元模式

享元(flyweight)模式是一种用于性能优化的模式,“fly”在这里是苍蝇的意思,意为蝇量级。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。

初识享元模式

假设有个内衣工厂,目前的产品有50种男式内衣和50种女士内衣,为了推销产品,工厂决定生产一些塑料模特来穿上他们的内衣拍成广告照片。正常情况下需要50个男模特和50个女模特,然后让他们每人分别穿上一件内衣来拍照。
var Model = function (sex, underwear) {
  this.sex = sex;
  this.underwear = underwear;
};

Model.prototype.takePhoto = function () {
  console.log('sex= ' + this.sex + 'underwear=' + this.underwear);
};

for (var i = 1; i <= 50; i++) {
  var maleModel = new Model('male', 'underwear' + i);
  maleModel.takePhoto();
}

for (var j = 1; j <= 50; j++) {
  var femaleModel = new Model('female', 'underwear' + j);
  femaleModel.takePhoto();
}
享元模式改进
var Model = function (sex) {
  this.sex = sex;
};

Model.prototype.takePhoto = function () {
  console.log('sex= ' + this.sex + ' underwear=' + this.underwear);
};

var maleModel = new Model('male'),
  femaleModel = new Model('female');

for (var i = 1; i <= 50; i++) {
  maleModel.underwear = 'underwear' + i;
  maleModel.takePhoto();
}

for (var j = 1; j <= 50; j++) {
  femaleModel.underwear = 'underwear' + j;
  femaleModel.takePhoto();
}

内部状态与外部状态

享元模式要求将对象的属性划分为内部状态与外部状态,享元模式的目标是尽量减少共享对象的数量,关于如何划分内部状态和外部状态,下面的几条经验提供了一些指引:
  • 内部状态存储于对象内部。
  • 内部状态可以被一些对象共享。
  • 内部状态独立于具体的场景,通常不会改变。
  • 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。
这样一来,我们便可以把所有内部状态相同的对象都指定为同一个共享的对象。而外部状态可以从对象身上剥离出来,并储存在外部。

文件上传的例子

在微云上传模块的开发中,对象爆炸的问题
var id = 0;

window.startUpload = function (uploadType, files) {
  // uploadType区分是控件还是flash
  for (var i = 0, file; (file = files[i++]); ) {
    var uploadObj = new Upload(uploadType, file.fileName, file.fileSize);
    uploadObj.init(id++); // 给upload对象设置一个唯一的id
  }
};

var Upload = function (uploadType, fileName, fileSize) {
  this.uploadType = uploadType;
  this.fileName = fileName;
  this.fileSize = fileSize;
  this.dom = null;
};

Upload.prototype.init = function (id) {
  var that = this;
  this.id = id;
  this.dom = document.createElement('div');
  this.dom.innerHTML =
    '<span>文件名称:' +
    this.fileName +
    ',文件大小: ' +
    this.fileSize +
    '</span>' +
    '<button class="delFile">删除</button>';

  this.dom.querySelector('.delFile').onclick = function () {
    that.delFile();
  };
  document.body.appendChild(this.dom);
};

Upload.prototype.delFile = function () {
  if (this.fileSize < 3000) {
    return this.dom.parentNode.removeChild(this.dom);
  }

  if (window.confirm('确定要删除该文件吗? ' + this.fileName)) {
    return this.dom.parentNode.removeChild(this.dom);
  }
};

startUpload('plugin', [
  {
    fileName: '1.txt',
    fileSize: 1000,
  },
  {
    fileName: '2.html',
    fileSize: 3000,
  },
  {
    fileName: '3.txt',
    fileSize: 5000,
  },
]);
startUpload('flash', [
  {
    fileName: '4.txt',
    fileSize: 1000,
  },
  {
    fileName: '5.html',
    fileSize: 3000,
  },
  {
    fileName: '6.txt',
    fileSize: 5000,
  },
]);
享元模式重构文件上传
// 剥离外部状态
var Upload = function( uploadType){
  this.uploadType = uploadType;
};

// 定义Upload.prototype.del函数
Upload.prototype.delFile = function (id) {
  uploadManager.setExternalState(id, this); // (1)

  if (this.fileSize < 3000) {
    return this.dom.parentNode.removeChild(this.dom);
  }
  if (window.confirm('确定要删除该文件吗? ' + this.fileName)) {
    return this.dom.parentNode.removeChild(this.dom);
  }
};

// 工厂进行对象实例化
var UploadFactory = (function () {
  var createdFlyWeightObjs = {};
  return {
    create: function (uploadType) {
      if (createdFlyWeightObjs[uploadType]) {
        return createdFlyWeightObjs[uploadType];
      }

      return (createdFlyWeightObjs[uploadType] = new Upload(uploadType));
    },
  };
})();

//管理器封装外部状态
var uploadManager = (function () {
  var uploadDatabase = {};

  return {
    add: function (id, uploadType, fileName, fileSize) {
      var flyWeightObj = UploadFactory.create(uploadType);

      var dom = document.createElement('div');
      dom.innerHTML =
        '<span>文件名称:' +
        fileName +
        ',文件大小: ' +
        fileSize +
        '</span>' +
        '<button class="delFile">删除</button>';

      dom.querySelector('.delFile').onclick = function () {
        flyWeightObj.delFile(id);
      };
      document.body.appendChild(dom);

      uploadDatabase[id] = {
        fileName: fileName,
        fileSize: fileSize,
        dom: dom,
      };

      return flyWeightObj;
    },
    setExternalState: function (id, flyWeightObj) {
      var uploadData = uploadDatabase[id];
      for (var i in uploadData) {
        flyWeightObj[i] = uploadData[i];
      }
    },
  };
})();

var id = 0;

window.startUpload = function (uploadType, files) {
  for (var i = 0, file; (file = files[i++]); ) {
    var uploadObj = uploadManager.add(
      ++id,
      uploadType,
      file.fileName,
      file.fileSize
    );
  }
};

startUpload('plugin', [
  {
    fileName: '1.txt',
    fileSize: 1000,
  },
  {
    fileName: '2.html',
    fileSize: 3000,
  },
  {
    fileName: '3.txt',
    fileSize: 5000,
  },
]);

startUpload('flash', [
  {
    fileName: '4.txt',
    fileSize: 1000,
  },
  {
    fileName: '5.html',
    fileSize: 3000,
  },
  {
    fileName: '6.txt',
    fileSize: 5000,
  },
]);

享元模式的适用性

享元模式带来的好处很大程度上取决于如何使用以及何时使用,一般来说,以下情况发生时便可以使用享元模式。
  • 一个程序中使用了大量的相似对象。
  • 由于使用了大量对象,造成很大的内存开销。
  • 对象的大多数状态都可以变为外部状态。
  • 剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象。
享元模式是为解决性能问题而生的模式,这跟大部分模式的诞生原因都不一样。在一个存在大量相似对象的系统中,享元模式可以很好地解决大量对象带来的性能问题。
END

评论

欢迎分享你的看法

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

全部评论

0

还没有评论

来说两句吧!