设计模式

Favori,

Design Pattern

图:Peter Tarka

1.Single Responsibility Principle,单一职责原则,简称SRP 实现类要职责单一。

2.Open Close Principle,开闭原则,简称OCP 对扩展开放,对修改关闭。

3.Liskov Substitution Principle,里氏替换原则,简称LSP 不要破坏继承体系。

4.Interface Segregation Principle,接口隔离原则,简称ISP 设计接口的时候要精简单一。

5.Dependence Inversion Principle,依赖倒置原则,简称DIP

设计原则

1、开放-封闭原则(OCP)

软件实体(类、模块、函数)等应该是可以 扩展的,但是不可修改

当需要改变一个程序的功能或者给这个程序增加新功能的时候,可以使用增加代码的方式,尽量避免改动程序的源代码,防止影响原系统的稳定

2、单一职责原则 (SRP)

不同的类具备不同的职责,各司其职。做系统设计是,如果发现有一个类拥有了两种职责,那么就要问一个问题:可以将这个类分成两个类吗?如果真的有必要,那就分开,千万不要让一个类干的事情太多。 如果一个方法承担了过多的职责,那么在需求的变迁过程中,需要改写这个方法的可能性就越大。

3、最少知道原则 (LKP 迪米特原则)

一个对象应该对其他对象有最少的了解, 类中只暴露不得不暴露的,其内部实现不暴露出去。

4、接口隔离原则

如果一个类实现一个接口,但这个接口中有它不需要的方法,那么就需要把这个接口拆分,把它需要的方法提取出来,组成一个新的接口让这个类去实现,一个接口对实现它的类都是有用的。接口足够小

5、依赖倒置原则

举例人吃苹果,我想吃苹果,但是我还想吃橘子,如果按照程序思维的话。就是三个类型,人 Class,苹果 Class,橘子 Class,这种方式冗杂不好维护,不易理解,用水果来抽象化,苹果类继承并实现吃的动作。 使用接口或抽象类 上层不应依赖下层实现

6、里氏替换原则

是对开闭原则的补充,子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法 子类中可以增加自己特有的方法

模式分类

模式分类名称
创建型工厂模式
单例模式
建造者模式
原型模式
结构型适配器模式
装饰器模式
代理模式
行为型策略模式
迭代器模式
观察者模式
命令模式
状态模式
模板模式
职责链模式
享元模式

工厂模式

工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象,用工厂方法代替 new 操作的一种模式。 构造函数和创建者分离,对 new 操作进行封装,隐藏创建过程、暴露共同接口, 符合开放封闭原则

class Creator {
  create(name) {
    return new Animal(name);
  }
}
 
class Animal {
  constructor(name) {
    this.name = name;
  }
}
 
var creator = new Creator();
 
var duck = creator.create("Duck");
console.log(duck.name); // Duck
 
var chicken = creator.create("Chicken");
console.log(chicken.name); // Chicken

实际应用:Button Producer:生产不同类型的按钮 => 生产多个本质相同,利用传参区分不同属性的元素 => 工厂

单例模式

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

class PlayStation {
  constructor() {
    this.state = 'off';
  }
  play() {
    if (this.state === 'on') {
      console.log('别闹,已经在happy了');
      return;
    }
    this.state = 'on';
    console.log('开始happy');
  }
  shutdown() {
    if (this.state === 'off') {
      console.log('已经关闭');
      return;
    }
    this.state = 'off';
    console.log('已经关机,请放心');
  }
}
PlayStation.instance = undefined;
PlayStation.getInstance = (function() {
  return function() {
    if(!PlayStation.instance) {
      PlayStation.instance = new PlayStation();
    }
    return PlayStation.instance;
  }()
}

实际应用:全局应用 router store => 只需要一个实例 => 单例

建造者模式

拆分简单模块、独立执行 => 注重过程与搭配

class Product {
  constructor(name) {
    this.name = name;
  }
  init() {
    console.log("Product init");
  }
}
 
class Skin {
  constructor(name) {
    this.name = name;
  }
  init() {
    console.log("Skin init");
  }
}
 
class Shop {
  constructor() {
    this.package = "";
  }
  create(name) {
    this.package = new PackageBuilder(name);
  }
  getGamePackage() {
    return this.package.getPackage();
  }
}
 
class PackageBuilder {
  constructor(name) {
    this.game = new Product(name);
    this.skin = new Skin(name);
  }
  getPackage() {
    return this.game.init() + this.skin.init();
  }
}

实际应用:页头组件 Header: 包含了 title、button、breadcum => 生产多重不同类型的元素 => 建造者

原型模式

用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

在 JavaScript 中,实现原型模式是在 ECMAScript5 中,提出的 Object.create 方法,使用现有的对象来提供新创建的对象的proto

var prototype = {
  name: "Jack",
  getName: function () {
    return this.name;
  },
};
 
var obj = Object.create(prototype, {
  job: {
    value: "IT",
  },
});
 
console.log(obj.getName()); // Jack
console.log(obj.job); // IT
console.log(obj.__proto__ === prototype); //true

适配器模式

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

适配独立模块,保证模块间的独立解耦且连接兼容

//假如BaiduMap类的原型方法不叫show,而是叫display,这时候就可以使用适配器模式了,因为我们不能轻易的改变第三方的内容。在BaiduMap的基础上封装一层,对外暴露show方法。
class GooleMap {
  show() {
    console.log("渲染谷歌地图");
  }
}
 
class BaiduMap {
  display() {
    console.log("渲染百度地图");
  }
}
 
// 定义适配器类, 对BaiduMap类进行封装
class BaiduMapAdapter {
  show() {
    var baiduMap = new BaiduMap();
    return baiduMap.display();
  }
}
 
function render(map) {
  if (map.show instanceof Function) {
    map.show();
  }
}
 
render(new GooleMap()); // 渲染谷歌地图
render(new BaiduMapAdapter()); // 渲染百度地图

场景:中间转换参数、保持模块间独立的时候

实际应用:两个模块:筛选器和表格,需要做一个联动。但筛选器的数据不能直接传入表格,需要做数据结构转换 ,模块之间独立,需要做数据结构转换

装饰器模式

以动态地给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象。 是一种“即用即付”的方式,能够在不改变对 象自身的基础上,在程序运行期间给对象动态地 添加职责

是为对象动态加入行为,经过多重包装,可以形成一条装饰链

// 动态将责任附加到对象上
// 设备创建->设备创建时升级
class Device {
  create() {
    console.log("PlayStation4");
  }
}
class Phone {
  create() {
    console.log("iphone18");
  }
}
class Decorator {
  constructor(device) {
    this.device = device;
  }
  create() {
    this.device.create();
    this.update(device);
  }
  update(device) {
    console.log(device + "pro");
  }
}
 
const device = new Device();
device.create();
 
const newDevice = new Decorator(device);
newDevice.create();

场景:附着于多个组件上,批量动态赋予功能的时候

实际应用:目前有按钮、title、icon 三个组件。希望开发一个模块,让三个组件同时具备相同功能

代理模式

代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问

class MyImage {
  constructor() {
    this.img = new Image();
    document.body.appendChild(this.img);
  }
  setSrc(src) {
    this.img.src = src;
  }
}
 
class ProxyImage {
  constructor() {
    this.proxyImage = new Image();
  }
 
  setSrc(src) {
    let myImageObj = new MyImage();
    myImageObj.img.src = "file://xxx.png"; //为本地图片url
    this.proxyImage.src = src;
    this.proxyImage.onload = function () {
      myImageObj.img.src = src;
    };
  }
}
 
var proxyImage = new ProxyImage();
proxyImage.setSrc("http://xxx.png"); //服务器资源url

场景:将代理对象与调用对象分离,不直接调用目标对象

实际应用:ul 中多个 li,每个 li 上的点击事件 => 利用冒泡做委托,事件绑定在 ul 上

策略模式

定义有一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。

将算法的使用和算法的实现分离开来。

一个基于策略模式的程序至少由两部分组成:

第一个部分是一组策略类,策略类封装了具体的算法,并负责具体的计算过程。

第二个部分是环境类 Context,Context 接受客户的请求,随后把请求委托给某一个策略类。要做到这点,说明 Context 中要维持对某个策略对象的引用

// 绩效为S 的人年终奖有4 倍工资,绩效为A 的人年终奖有3 倍工资,而绩效为B 的人年终奖是2 倍工资。
 
var strategies = {
  S: function (salary) {
    return salary * 4;
  },
  A: function (salary) {
    return salary * 3;
  },
  B: function (salary) {
    return salary * 2;
  },
};
var cacularBonus = function (level, salary) {
  return strategies[level](salary);
};
var level2 = cacularBonus("A", 200);
//我们将计算年终奖的算法,放在一个对象内部,封装起来,在调用的时候可以通过不同的等级获得不同的计算方式。而我们有新的等级或者新的计算方式的时候,我们对该对象进行更改就可以了。避免了在一个函数内部进行计算,提高了可维护性。

迭代器模式

迭代器模式是指提供一种方法顺序访问一个有序聚合对象中的各个元素,而又不需要暴露该对象的内部表示。

在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素

class Creater {
  constructor(list) {
    this.list = list;
  }
 
  // 创建一个迭代器,也叫遍历器
  createIterator() {
    return new Iterator(this);
  }
}
 
class Iterator {
  constructor(creater) {
    this.list = creater.list;
    this.index = 0;
  }
 
  // 判断是否遍历完数据
  isDone() {
    if (this.index >= this.list.length) {
      return true;
    }
    return false;
  }
 
  next() {
    return this.list[this.index++];
  }
}
 
var arr = [1, 2, 3, 4];
 
var creater = new Creater(arr);
var iterator = creater.createIterator();
console.log(iterator.list); // [1, 2, 3, 4]
while (!iterator.isDone()) {
  console.log(iterator.next());
  // 1
  // 2
  // 3
  // 4
}

javascript 中有序数据集合包括

  • Array
  • Map
  • Set
  • String
  • typeArray
  • arguments
  • NodeList
var arr = [1, 2, 3, 4];
 
var iterator = arr[Symbol.iterator]();
 
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: 4, done: false}
console.log(iterator.next()); // {value: undefined, done: true}

观察者模式 (订阅发布模式)

也称作观察者模式,定义了对象间的一种一对多的依赖关系,当一个对象的状态发 生改变时,所有依赖于它的对象都将得到通知

取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。

与传统的发布-订阅模式实现方式(将订阅者自身当成引用传入发布者)不同,在 JS 中通常使用注册回调函数的形式来订阅

// 当一个属性发生状态改变时,观察者会连续引发所有的相关状态改变
// 通过智能家居一键开始游戏
class MediaCenter {
  constructor() {
    this.state = "";
    this.observers = [];
  }
  attach(observer) {
    this.observers.push(observer);
  }
  getState() {
    return this.state;
  }
  setState(state) {
    this.state = state;
    this.notifyAllobservers();
  }
  notifyAllobservers() {
    this.observers.forEach((ob) => {
      ob.update();
    });
  }
}
 
class observer {
  constructor(name, center) {
    this.name = name;
    this.center = center;
    this.center.attach(this);
  }
  update() {
    console.log(`${this.name} update, state: ${this.center.getState()}`);
  }
}
 
const center = new MediaCenter();
const ps = new Observer("ps", center);
const tv = new Observer("tv", center);
 
center.setState("on");

发布订阅模式可以使代码解耦,满足开放封闭原则 当过多的使用发布订阅模式,如果订阅消息始终都没有触发,则订阅者一直保存在内存中。

命令模式

用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系

命令中带有 execute 执行、undo 撤销、redo 重做等相关命令方法,建议显示地指示这些方法名

//一个自增命令,提供执行、撤销、重做功能
 
// 自增
function IncrementCommand() {
  // 当前值
  this.val = 0;
  // 命令栈
  this.stack = [];
  // 栈指针位置
  this.stackPosition = -1;
}
 
IncrementCommand.prototype = {
  constructor: IncrementCommand,
 
  // 执行
  execute: function () {
    this._clearRedo();
 
    // 定义执行的处理
    var command = function () {
      this.val += 2;
    }.bind(this);
 
    // 执行并缓存起来
    command();
 
    this.stack.push(command);
 
    this.stackPosition++;
 
    this.getValue();
  },
 
  canUndo: function () {
    return this.stackPosition >= 0;
  },
 
  canRedo: function () {
    return this.stackPosition < this.stack.length - 1;
  },
 
  // 撤销
  undo: function () {
    if (!this.canUndo()) {
      return;
    }
 
    this.stackPosition--;
 
    // 命令的撤销,与执行的处理相反
    var command = function () {
      this.val -= 2;
    }.bind(this);
 
    // 撤销后不需要缓存
    command();
 
    this.getValue();
  },
 
  // 重做
  redo: function () {
    if (!this.canRedo()) {
      return;
    }
 
    // 执行栈顶的命令
    this.stack[++this.stackPosition]();
 
    this.getValue();
  },
 
  // 在执行时,已经撤销的部分不能再重做
  _clearRedo: function () {
    this.stack = this.stack.slice(0, this.stackPosition + 1);
  },
 
  // 获取当前值
  getValue: function () {
    console.log(this.val);
  },
};
var incrementCommand = new IncrementCommand();
 
// 模拟事件触发,执行命令
var eventTrigger = {
  // 某个事件的处理中,直接调用命令的处理方法
  increment: function () {
    incrementCommand.execute();
  },
 
  incrementUndo: function () {
    incrementCommand.undo();
  },
 
  incrementRedo: function () {
    incrementCommand.redo();
  },
};
 
eventTrigger["increment"](); // 2
eventTrigger["increment"](); // 4
 
eventTrigger["incrementUndo"](); // 2
 
eventTrigger["increment"](); // 4
 
eventTrigger["incrementUndo"](); // 2
eventTrigger["incrementUndo"](); // 0
eventTrigger["incrementUndo"](); // 无输出
 
eventTrigger["incrementRedo"](); // 2
eventTrigger["incrementRedo"](); // 4
eventTrigger["incrementRedo"](); // 无输出
 
eventTrigger["increment"](); // 6

状态模式

状态模式允许一个对象在其内部状态改变的时候改变行为,这个对象看上去像是改变了它的类一样,状态模式把所研究对象的行为包装在不同的状态对象里,每一个状态对象都属于一个抽象状态类的一个子类。

区分事物内部的状态,把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>state-demo</title>
  </head>
 
  <body>
    <button id="btn">开关</button>
    <script>
      // 定义一个关闭状态的类
      class OffLightState {
        constructor(light) {
          this.light = light;
        }
        // 每个类都需要这个方法,在不同状态下按都需要触发这个方法
        pressBtn() {
          this.light.setState(this.light.weekLightState);
          console.log("开启弱光");
        }
      }
 
      // 定义一个弱光状态的类
      class WeekLightState {
        constructor(light) {
          this.light = light;
        }
        pressBtn() {
          this.light.setState(this.light.strongLightState);
          console.log("开启强光");
        }
      }
 
      // 定义一个强光状态的类
      class StrongLightState {
        constructor(light) {
          this.light = light;
        }
        pressBtn() {
          this.light.setState(this.light.offLightState);
          console.log("关闭电灯");
        }
      }
 
      class Light {
        constructor() {
          this.offLightState = new OffLightState(this);
          this.weekLightState = new WeekLightState(this);
          this.strongLightState = new StrongLightState(this);
          this.currentState = null;
        }
        setState(newState) {
          this.currentState = newState;
        }
        checkState(state) {
          this.currentState = this[state];
        }
      }
 
      let light = new Light();
      light.checkState("strongLightState");
      var btn = document.getElementById("btn");
      btn.onclick = function () {
        light.currentState.pressBtn();
      };
    </script>
  </body>
</html>

模板模式

模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。

在抽象父类中封装子类的算法框架,它的 init 方法可作为一个算法的模板,指导子类以何种顺序去执行哪些方法。

由父类分离出公共部分,要求子类重写某些父类的(易变化的)抽象方法

模板方法模式一般的实现方式为继承

以运动作为例子,运动有比较通用的一些处理,这部分可以抽离开来,在父类中实现。具体某项运动的特殊性则有自类来重写实现。

最终子类直接调用父类的模板函数来执行

// 体育运动
function Sport() {}
 
Sport.prototype = {
  constructor: Sport,
 
  // 模板,按顺序执行
  init: function () {
    this.stretch();
    this.jog();
    this.deepBreath();
    this.start();
 
    var free = this.end();
 
    // 运动后还有空的话,就拉伸一下
    if (free !== false) {
      this.stretch();
    }
  },
 
  // 拉伸
  stretch: function () {
    console.log("拉伸");
  },
 
  // 慢跑
  jog: function () {
    console.log("慢跑");
  },
 
  // 深呼吸
  deepBreath: function () {
    console.log("深呼吸");
  },
 
  // 开始运动
  start: function () {
    throw new Error("子类必须重写此方法");
  },
 
  // 结束运动
  end: function () {
    console.log("运动结束");
  },
};
 
// 篮球
function Basketball() {}
 
Basketball.prototype = new Sport();
 
// 重写相关的方法
Basketball.prototype.start = function () {
  console.log("先投上几个三分");
};
 
Basketball.prototype.end = function () {
  console.log("运动结束了,有事先走一步");
  return false;
};
 
// 马拉松
function Marathon() {}
 
Marathon.prototype = new Sport();
 
var basketball = new Basketball();
var marathon = new Marathon();
 
// 子类调用,最终会按照父类定义的顺序执行
basketball.init();
marathon.init();

职责链

使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链 传递该请求,直到有一个对象处理它为止

请求发送者只需要知道链中的第一个节点,弱化发送者和一组接收者之间的强联系,可以便捷地在职责链中增加或删除一个节点,同样地,指定谁是第一个节点也很便捷

class Action {
  constructor(name) {
    this.name = name;
    this.nextAction = null;
  }
  setNextAction(action) {
    this.nextAction = action;
  }
  handle() {
    console.log(`${this.name}请审批,是否可以打游戏`);
    if (this.nextAction !== null) {
      this.nextAction.handle();
    }
  }
}
 
const dad = new Action("爸");
const mom = new Action("妈");
const wife = new Action("夫人");
 
dad.setNextAction(mom);
mom.setNextAction(wife);
 
dad.handle();

享元模式

是一种用于性能优化的模式,它的目标是尽量减少共享对象的数量

运用共享技术来有效支持大量细粒度的对象。

调将对象的属性划分为内部状态(属性)与外部状态(属性)。内部状态用于对象的共享,通常不变;而外部状态则剥离开来,由具体的场景决定。

在程序中使用了大量的相似对象时,可以利用享元模式来优化,减少对象的数量

// 举个栗子,要对某个班进行身体素质测量,仅测量身高体重来评判
 
// 健康测量
function Fitness(name, sex, age, height, weight) {
  this.name = name;
  this.sex = sex;
  this.age = age;
  this.height = height;
  this.weight = weight;
}
 
// 开始评判
Fitness.prototype.judge = function () {
  var ret = this.name + ": ";
 
  if (this.sex === "male") {
    ret += this.judgeMale();
  } else {
    ret += this.judgeFemale();
  }
 
  console.log(ret);
};
 
// 男性评判规则
Fitness.prototype.judgeMale = function () {
  var ratio = this.height / this.weight;
 
  return this.age > 20 ? ratio > 3.5 : ratio > 2.8;
};
 
// 女性评判规则
Fitness.prototype.judgeFemale = function () {
  var ratio = this.height / this.weight;
 
  return this.age > 20 ? ratio > 4 : ratio > 3;
};
 
var a = new Fitness("A", "male", 18, 160, 80);
var b = new Fitness("B", "male", 21, 180, 70);
var c = new Fitness("C", "female", 28, 160, 80);
var d = new Fitness("D", "male", 18, 170, 60);
var e = new Fitness("E", "female", 18, 160, 40);
 
// 开始评判
a.judge(); // A: false
b.judge(); // B: false
c.judge(); // C: false
d.judge(); // D: true
e.judge(); // E: true