Design Patterns - childlabor/blog GitHub Wiki

前言

在软件开发领域,模式是指常见问题的通用解决方案。模式不是简单的代码复制和粘贴,而是一种最佳实践,一种高级抽象,是解决某一类问题的范本。

模式提供的经过论证的最佳实践,可以帮助我们更好的编码,避免重复造轮子。

模式提供了高一层的抽象,使用模式可以帮你理清头绪。

简单的代码编写技巧和技术问题处理方式的约定(代码注释)可以使开发者之间的交流更加通畅。

设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。

image

原则

  • Single Responsibility Principle 单一职责原则(重要)
    • 一个程序只做好一件事
    • 如果功能过于复杂就拆分开,每个部分保持独立
  • OpenClosed Principle 开放/封闭原则(重要)
    • 对扩展开放,对修改封闭
    • 增加需求时,扩展新代码,而非修改已有代码
  • Liskov Substitution Principle 里氏替换原则
    • 子类能覆盖父类
    • 父类能出现的地方子类就能出现
  • Interface Segregation Principle 接口隔离原则
    • 保持接口的单一独立
    • 类似单一职责原则,这里更关注接口
  • Dependency Inversion Principle 依赖倒转原则
    • 面向接口编程,依赖于抽象而不依赖于具体
    • 使用方只关注接口而不关注具体类的实现

核心思想——封装变化

  • 在实际开发中,不发生变化的代码可以说是不存在的。我们能做的只有将这个变化造成的影响最小化 —— 将变与不变分离,确保变化的部分灵活、不变的部分稳定。

  • 这个过程,就叫“封装变化”;这样的代码,就是我们所谓的“健壮”的代码,它可以经得起变化的考验。而设计模式出现的意义,就是帮我们写出这样的代码。

类型

  • 创建型:封装了创建对象过程中的变化,比如工厂模式,它做的事情就是将创建对象的过程抽离;
  • 结构型:封装的是对象之间组合方式的变化,目的在于灵活地表达对象间的配合与依赖关系;
  • 行为型:对象千变万化的行为进行抽离,确保我们能够更安全、更方便地对行为进行更改。

设计模式:

创建型:工厂模式

工厂模式定义一个用于创建对象的接口,这个接口由子类决定实例化哪一个类。

// 形状
class Circle {
    draw() {
        console.log("I'm a circle")
    }
}
class Rectangle {
    draw() {
        console.log("I'm a rectangle")
    }
}
class Square {
    draw() {
        console.log("I'm a square")
    }
}

// 形状工厂
class ShapeFactory {
    getShape(shapeType){
        switch(shapeType) {
            case 'CIRCLE':
                return new Circle();
            case 'RECTANGLE':
                return new Rectangle();
            case 'SQUARE':
                return new Square();
            default:
                return null;
        }
    }
}

// 调用
const shapeFactory = new ShapeFactory();
// 通过工厂拿各种形状
const shape1 = shapeFactory.getShape('CIRCLE');
shape1.draw();
const shape2 = shapeFactory.getShape('RECTANGLE');
shape2.draw();
const shape3 = shapeFactory.getShape('SQUARE');
shape3.draw();
/**
 * output:
 * I'm a circle
 * I'm a rectangle
 * I'm a square
 */

创建型:抽象工厂模式 ⭐

和工厂模式的好处很相似,给工厂做了一个统一的出入口,也方便了日后对这个工厂的修改。即工厂模式的工厂。

典型jQuery,根据$()传入的参数,返回不同子类对象,实现不同的api。

// 沿用工厂模式例子 又搞了个颜色工厂
// 再新加一个颜色工厂
class Red {
    fill() {
        console.log("fill red")
    }
}
class Blue {
    fill() {
        console.log("fill blue")
    }
}
class Green {
    fill() {
        console.log("fill green")
    }
}
class ColorFactory {
    getColor(color){
        switch(color) {
            case 'RED':
                return new Red();
            case 'BLUE':
                return new Blue();
            case 'GREEN':
                return new Green();
            default:
                return null;
        }
    }
}

// 把工厂通过抽象工厂生产出来 (抽象工厂)
class FactoryProducer {
    static getFactory(choice){
        switch(choice) {
            case 'SHAPE':
                return new ShapeFactory();
            case 'COLOR':
                return new ColorFactory();
            default:
                return null;
        }
    }
}

// 调用
//通过抽象工厂拿形状工厂
const shapeFactory = FactoryProducer.getFactory('SHAPE');
// 通过工厂拿各种形状
const shape1 = shapeFactory.getShape('CIRCLE');
shape1.draw();
const shape2 = shapeFactory.getShape('RECTANGLE');
shape2.draw();
const shape3 = shapeFactory.getShape('SQUARE');
shape3.draw();
//通过抽象工厂拿颜色工厂
const colorFactory = FactoryProducer.getFactory('COLOR');
// 通过工厂拿各种颜色
const color1 = colorFactory.getColor('RED');
color1.fill();
const color2 = colorFactory.getColor('BLUE');
color2.fill();
const color3 = colorFactory.getColor('GREEN');
color3.fill();
/**
 * output:
 * I'm a circle
 * I'm a rectangle
 * I'm a square
 * fill red
 * fill blue
 * fill green
 */
init = jQuery.fn.init = function( selector, context, root ) {
    var match, elem;

    // HANDLE: $(""), $(null), $(undefined), $(false)
    if ( !selector ) {
        return this;
    }

    // Method init() accepts an alternate rootjQuery
    // so migrate can support jQuery.sub (gh-2101)
    root = root || rootjQuery;

    // Handle HTML strings
    if ( typeof selector === "string" ) {
            
        //...

    // HANDLE: $(DOMElement)
    } else if ( selector.nodeType ) {
        
        //....

    // HANDLE: $(function)
    // Shortcut for document ready
    } else if ( jQuery.isFunction( selector ) ) {
        //....
    }

    return jQuery.makeArray( selector, this );
};

创建型:构造器模式 ⭐

让简单的对象通过组合的方式构造成多种复杂对象。又叫建造者模式,这是一种创建复杂对象的最佳实践。尤其是复杂对象多变的情况下,通过基础组件来组合,在基础组件变更时,多种依赖于基础组件的复杂组件也能方便变更,而不需要更改多种不同的复杂组件。

这里举例西式快餐,里面有非常多的套餐种类,但是各种套餐都是由不同种类的冷饮和汉堡组合而成。同时冷饮需要瓶子装,汉堡需要纸盒包住,那么我们可以先定义冷饮和汉堡类和它们所需要的瓶子和纸盒。

// 1. 基础类
// 纸盒
class Wrapper {
  pack() {
    return "Wrapper";
  }
}
// 瓶子
class Bottle {
  pack() {
    return "Bottle";
  }
}
// 汉堡需要纸盒包住
class Burger {
  packing() {
    return new Wrapper();
  }
}
// 冷饮需要瓶子装
class ColdDrink {
  packing() {
    return new Bottle();
  }
}

// 2. 具体实现类(继承)
// 蔬菜汉堡
class VegBurger extends Burger {
  price() {
    return 25.0;
  }
  name() {
    return "Veg Burger";
  }
}
// 肌肉汉堡
class ChickenBurger extends Burger {
  price() {
    return 50.5;
  }
  name() {
    return "Chicken Burger";
  }
}
// 可乐
class Coke extends ColdDrink {
  price() {
    return 30.0;
  }
  name() {
    return "Coke";
  }
}
// 百事
class Pepsi extends ColdDrink {
  price() {
    return 35.0;
  }
  name() {
    return "Pepsi";
  }
}

// 3. 组合类
// 一个套餐肯定是有多个不同冷饮和汉堡
class Meal {
  constructor() {
    const items = [];
    /**
     * 为什么不用Proxy而使用defineProperty
     * 因为Proxy虽然实现和defineProperty类似的功能
     * 但是在这个场景下,语意上是定义属性,而不是需要代理
     */
    Reflect.defineProperty(this, "items", {
      get: () => {
        if (this.__proto__ != Meal.prototype) {
          throw new Error("items is private!");
        }
        return items;
      },
    });
  }
  addItem(item) {
    this[this.itemsName].push(item);
  }
  getCost() {
    let cost = 0.0;
    for (const item of this[this.itemsName]) {
      cost += item.price();
    }
    return cost;
  }
  showItems() {
    for (const item of this[this.itemsName]) {
      const nameStr = "Item : " + item.name();
      const packStr = "Packing : " + item.packing().pack();
      const priceStr = "Price : " + item.price();
      console.log(`${nameStr},${packStr},${priceStr}`);
    }
  }
}

// 4. 构造器
// 提供多个套餐
class MealBuilder {
  prepareVegMeal() {
    const meal = new Meal();
    meal.addItem(new VegBurger());
    meal.addItem(new Coke());
    return meal;
  }
  prepareNonVegMeal() {
    const meal = new Meal();
    meal.addItem(new ChickenBurger());
    meal.addItem(new Pepsi());
    return meal;
  }
}

// 5. 调用
const mealBuilder = new MealBuilder();
const vegMeal = mealBuilder.prepareVegMeal();
console.log("Veg Meal");
vegMeal.showItems();
console.log("Total Cost: " + vegMeal.getCost());
const nonVegMeal = mealBuilder.prepareNonVegMeal();
console.log("\nNon-Veg Meal");
nonVegMeal.showItems();
console.log("Total Cost: " + nonVegMeal.getCost());
/**
 * output:
 * Veg Meal
 * Item : Veg Burger,Packing : Wrapper,Price : 25
 * Item : Coke,Packing : Bottle,Price : 30
 * Total Cost: 55
 *
 * Non-Veg Meal
 * Item : Chicken Burger,Packing : Wrapper,Price : 50.5
 * Item : Pepsi,Packing : Bottle,Price : 35
 * Total Cost: 85.5
 */

创建型:单例模式 ⭐

一个类只有一个实例,并提供一个访问它的全局访问点。

典型vuex全局只有一个store对象,单一状态树,方便管理全局统一的数据状态。

单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。

 class LoginForm {
    constructor() {
        this.state = 'hide'
    }
    show() {
        if (this.state === 'show') {
            alert('已经显示')
            return
        }
        this.state = 'show'
        console.log('登录框显示成功')
    }
    hide() {
        if (this.state === 'hide') {
            alert('已经隐藏')
            return
        }
        this.state = 'hide'
        console.log('登录框隐藏成功')
    }
    // 静态方法实现方式
    static getInstance() {
        if (!LoginForm.instance) {
            // 若这个唯一的实例不存在,那么先创建它
            LoginForm.instance = new LoginForm()
        }
        // 如果这个唯一的实例已经存在,则直接返回
        return LoginForm.instance
    }
 }
 // or 闭包实现方式
 LoginForm.getInstance = (function () {
     let instance
     return function () {
        // 若这个唯一的实例不存在,那么先创建它(单例)
        if (!instance) {
            instance = new LoginForm()
        }
        return instance
     }
 })()

let obj1 = LoginForm.getInstance()
obj1.show()

let obj2 = LoginForm.getInstance()
obj2.hide()

console.log(obj1 === obj2) // true

创建型:原型模式

原型模式不仅是一种设计模式,它还是一种编程范式(programming paradigm),是 JavaScript 面向对象系统实现的根基。

使用原型模式,并不是为了得到一个副本,而是为了得到与构造函数(类)相对应的类型的实例、实现数据/方法的共享。

不必强行把原型模式当作一种设计模式去理解,把它作为一种编程范式来讨论会更合适。

ECMAScript 2015 中引入的 JavaScript 类实质上是 JavaScript 现有的基于原型的继承的语法糖。类语法不会为 JavaScript 引入新的面向对象的继承模型。 ——MDN

// class
class Dog {
  constructor(name ,age) {
   this.name = name
   this.age = age
  }
  
  eat() {
    console.log('肉骨头真好吃')
  }
}

// 原型继承 (完全等价class)
function Dog(name, age) {
  this.name = name
  this.age = age
}

Dog.prototype.eat = function() {
  console.log('肉骨头真好吃')
}

结构型:装饰器模式

动态地给某个对象添加一些额外的职责,是一种实现继承的替代方案(轻量,动态组合)。

装饰类和被装饰类都只关心自身的核心业务,实现了解耦。方便动态的扩展功能,且提供了比继承更多的灵活性。

调用方法时,先执行目标对象原有的方法,再执行自行添加的特性(装饰)。

典型:vue的mixin、React的HOC

class Cellphone {
    create() {
        console.log('生成一个手机')
    }
}
class Decorator {
    constructor(cellphone) {
        this.cellphone = cellphone
    }
    create() {
        this.cellphone.create()
        this.createShell(cellphone)
    }
    createShell() {
        console.log('生成手机壳')
    }
}
// 测试代码
let cellphone = new Cellphone()
cellphone.create()

console.log('------------')
let dec = new Decorator(cellphone)
dec.create()

// ES7 中的装饰器 @语法糖 (浏览器原生不支持,Babel 进行转码)
function autopilotDecorator(target, key, descriptor) {
    const method = descriptor.value;
    
    descriptor.value = () => {
        method.apply(target);
        console.log('启动自动驾驶模式');
    }
    
    return descriptor;
}

class Car {
    @autopilotDecorator
    drive() {
        console.log('乞丐版');
    }
}

let car = new Car();
car.drive();    //乞丐版;启动自动驾驶模式;

结构型:适配器模式 ⭐

将一个类的接口转化为另外一个接口,以满足用户需求,使类之间接口不兼容问题通过适配器得以解决。

通常用于整合第三方SDK,封装旧接口等。

通过适配器模式可以达到:统一的接口,统一的入参,统一的出参,统一的规则的效果。

如果没必要使用适配器模式的话,可以考虑重构,如果使用的话,尽量把文档完善。

典型axios;vue的computed,原有data中的数据不满足当前的要求,通过计算属性的规则来适配成我们需要的格式,对原有数据并没有改变,只改变了原有数据的表现形式。

// axios 源码片段 适配node和浏览器环境下请求 (axios/lib/default.js)
function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    // For browsers use XHR adapter
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // For node use HTTP adapter
    adapter = require('./adapters/http');
  }
  return adapter;
}
class Plug {
  getName() {
    return 'iphone充电头';
  }
}

class Target {
  constructor() {
    this.plug = new Plug();
  }
  getName() {
    return this.plug.getName() + ' 适配器Type-c充电头';
  }
}

let target = new Target();
target.getName(); // iphone充电头 适配器转Type-c充电头

结构型:代理模式 ⭐

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

典型:元素事件委托(li事件委托到ul(ul.addEventListener...),减少事件量);接口跨域本地代理;ES6的proxy等

业务开发中最常见的四种代理类型:

  1. 事件代理(事件冒泡特性下的元素事件委托)
  2. 虚拟代理(图片懒加载、预加载)
  3. 缓存代理(用于一些计算量较大、性能消耗大的场景,避免重复)
  4. 保护代理(校验和拦截,Proxy)

装饰者模式实现上和代理模式类似:

  • 装饰者模式: 扩展功能,原有功能不变且可直接使用(后置)
  • 代理模式: 显示原有功能,但是经过限制之后的(前置)

假设当A 在心情好的时候收到花,小明表白成功的几率有 60%,而当A 在心情差的时候收到花,小明表白的成功率无限趋近于0。 小明跟A 刚刚认识两天,还无法辨别A 什么时候心情好。如果不合时宜地把花送给A,花 被直接扔掉的可能性很大,这束花可是小明吃了7 天泡面换来的。 但是A 的朋友B 却很了解A,所以小明只管把花交给B,B 会监听A 的心情变化,然后选 择A 心情好的时候把花转交给A,代码如下:

let Flower = function() {}
let xiaoming = {
  sendFlower: function(target) {
    let flower = new Flower()
    target.receiveFlower(flower)
  }
}
let B = {
  receiveFlower: function(flower) {
    A.listenGoodMood(function() {
      A.receiveFlower(flower)
    })
  }
}
let A = {
  receiveFlower: function(flower) {
    console.log('收到花'+ flower)
  },
  listenGoodMood: function(fn) {
    setTimeout(function() {
      fn()
    }, 1000)
  }
}
xiaoming.sendFlower(B)
// 虚拟代理实现图片预加载
var myImage = (function(){
    var imgNode = document.createElement( 'img' );
    document.body.appendChild( imgNode );
    return {
        setSrc: function( src ) {
            imgNode.src = src;
        }
    }
})();

var proxyImage = (function() {
    var img = new Image;
    img.onload = function() {
        myImage.setSrc( this.src );
    }
    return {
        setSrc: function( src ) {
            // 占位
            myImage.setSrc( './loading.gif' );
            img.src = src;
        }
    }
})();

proxyImage.setSrc( './pic.jpg' );
// 缓存代理 避免重复大量计算 (空间换时间)
// addAll方法会对你传入的所有参数做求和操作
const addAll = function() {
    console.log('进行了一次新计算')
    let result = 0
    const len = arguments.length
    for(let i = 0; i < len; i++) {
        result += arguments[i]
    }
    return result
}

// 为求和方法创建代理
const proxyAddAll = (function(){
    // 求和结果的缓存池
    const resultCache = {}
    return function() {
        // 将入参转化为一个唯一的入参字符串
        const args = Array.prototype.join.call(arguments, ',')
        
        // 检查本次入参是否有对应的计算结果
        if(args in resultCache) {
            // 如果有,则返回缓存池里现成的结果
            return resultCache[args]
        }
        // 入参存入缓存作为key值索引 同时返回结果
        return resultCache[args] = addAll(...arguments)
    }
})()

结构型:外观模式

为子系统中的一组接口提供了一个一致的界面,此模块定义了一个高层接口,这个接口使得这一子系统更加容易使用。

典例:解决浏览器兼容性问题

// 统一事件监听
let addMyEvent = function (el, ev, fn) {
    if (el.addEventListener) {
        el.addEventListener(ev, fn, false)
    } else if (el.attachEvent) {
        el.attachEvent('on' + ev, fn)
    } else {
        el['on' + ev] = fn
    }
}; 

// <div id="isShow">show-hide</div> 统一控制显示隐藏
function setBox(){
	var getId = document.getElementById('isShow');
	return {
		show : function(){
			getId.style.display = 'block';
		},
		hide : function(){
			getId.style.display = 'none';
		}
	}
}

结构型:桥接模式

桥接(Bridge)是用于把抽象化与现实化解耦,使得二者可以独立变化,在系统沿着多个维度变化的同时,又不断增加其复杂度。不具体实现业务,但是却是业务必须的需求方。有时也被称为“双适配器模式”。

// 具体实现
var Fn1 = function(a) {
  // dosomething...  
}
var Fn2 = function(b) {
  // dosomething...
}
var Bridge = function(a, b){
  this.one = new Fn1(a)
  this.two = new Fn2(b)
}

通过组合现有接口的方式,去组成一个新的实现,对应新的需求,不必重新定义接口,再重新为新接口写一个实现。所以接口和实现是可以组合的,这种组合我们称之为桥接模式。

结构型:组合模式

组合模式:又叫 “部分整体” 模式,将对象组合成树形结构,以表示 “部分-整体” 的层次结构。通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性。

// TS
// 定义接口规范
interface Compose {
    name: string,
    add(file: CFile): void,
    scan(): void
}

// 树对象 - 文件目录
class CFolder implements Compose {
    fileList = [];
    name: string;

    constructor(name: string) {
        this.name = name;
    }

    add(file: CFile) {
        this.fileList.push(file);
    }

    scan() {
        for (let file of this.fileList) {
            file.scan();
        }
    }
}

// 叶对象 - 文件
class CFile implements Compose {
    name: string;

    constructor(name: string) {
        this.name = name;
    }

    add(file: CFile) {
        throw new Error('文件下面不能再添加文件');
    }

    scan() {
        console.log(`开始扫描:${this.name}`)
    }
}

let mediaFolder = new CFolder('娱乐');
let movieFolder = new CFolder('电影');
let musicFolder = new CFolder('音乐');

let file1 = new CFile('钢铁侠.mp4');
let file2 = new CFile('再谈记忆.mp3');
movieFolder.add(file1);
musicFolder.add(file2);
mediaFolder.add(movieFolder);
mediaFolder.add(musicFolder);
mediaFolder.scan();

/* 输出:
开始扫描文件:钢铁侠.mp4
开始扫描文件:再谈记忆.mp3
*/

结构型:享元模式

主要思想是共享细粒度对象,也就是说如果系统中存在多个相同的对象,那么只需共享一份就可以了,不必每个都去实例化每一个对象,这样来精简内存资源,提升性能和效率。

  1. 目标对象具有一些共同的状态
  2. 这些共同的状态所对应的对象,可以被共享出来

享元模式的优点:

  • 由于减少了系统中的对象数量,提高了程序运行效率和性能,精简了内存占用,加快运行速度;
  • 外部状态相对独立,不会影响到内部状态,所以享元对象能够在不同的环境被共享;

享元模式的缺点:

  • 引入了共享对象,使对象结构变得复杂;
  • 共享对象的创建、销毁等需要维护,带来额外的复杂度(如果需要把共享对象维护起来的话);
var candidateNum = 10   // 考生数量
var examCarNum = 0      // 驾考车的数量

/* 驾考车构造函数 */
function ExamCar(carType) {
    examCarNum++
    this.carId = examCarNum
    this.carType = carType ? '手动档' : '自动档'
}

ExamCar.prototype.examine = function(candidateId) {
    console.log('考生- ' + candidateId + ' 在' + this.carType + '驾考车- ' + this.carId + ' 上考试')
}

for (var candidateId = 1; candidateId <= candidateNum; candidateId++) {
    var examCar = new ExamCar(candidateId % 2)
    examCar.examine(candidateId)
}

console.log('驾考车总数 - ' + examCarNum)
// 输出: 驾考车总数 - 10
// =================
// 享元模式重构
var candidateNum = 10
var examCarNum = 0

function ExamCar(carType) {
    examCarNum++
    this.carId = examCarNum
    this.carType = carType ? '手动档' : '自动档'
}

ExamCar.prototype.examine = function(candidateId) {
    console.log('考生- ' + candidateId + ' 在' + this.carType + '驾考车- ' + this.carId + ' 上考试')
}

// 共享对象
var manualExamCar = new ExamCar(true)
var autoExamCar = new ExamCar(false)

for (var candidateId = 1; candidateId <= candidateNum; candidateId++) {
    var examCar = candidateId % 2 ? manualExamCar : autoExamCar
    examCar.examine(candidateId)
}

console.log('驾考车总数 - ' + examCarNum)
// 输出: 驾考车总数 - 2

行为型:策略模式

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

优化大量if-else代码,考虑更优的映射方案,使代码变得易读、易维护。

没有定式,从设计模式原则出发,优化代码策略的方式。

/*
 * 当价格类型为“预售价”时,满 100 - 20,不满 100 打 9 折
 * 当价格类型为“大促价”时,满 100 - 30,不满 100 打 8 折
 * 当价格类型为“返场价”时,满 200 - 50,不叠加
 * 当价格类型为“尝鲜价”时,直接打 5 折
 */
function askPrice(tag, originPrice) {
  // 处理预热价
  if(tag === 'pre') {
    if(originPrice >= 100) {
      return originPrice - 20
    } 
    return originPrice * 0.9
  }
  // 处理大促价
  if(tag === 'onSale') {
    if(originPrice >= 100) {
      return originPrice - 30
    } 
    return originPrice * 0.8
  }

  // 处理返场价
  if(tag === 'back') {
    if(originPrice >= 200) {
      return originPrice - 50
    }
    return originPrice
  }

  // 处理尝鲜价
  if(tag === 'fresh') {
     return originPrice * 0.5
  }
}
// 策略模式优化重构:
// 定义一个询价处理器对象
const priceProcessor = {
  pre(originPrice) {
    if (originPrice >= 100) {
      return originPrice - 20;
    }
    return originPrice * 0.9;
  },
  onSale(originPrice) {
    if (originPrice >= 100) {
      return originPrice - 30;
    }
    return originPrice * 0.8;
  },
  back(originPrice) {
    if (originPrice >= 200) {
      return originPrice - 50;
    }
    return originPrice;
  },
  fresh(originPrice) {
    return originPrice * 0.5;
  },
};
// 询价函数
function askPrice(tag, originPrice) {
  return priceProcessor[tag](originPrice)
}

行为型:状态模式

状态模式(State Pattern) :允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。 状态模式主要解决的是当控制一个对象状态的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类中,可以把复杂的判断逻辑简化。

与策略模式类似,一个算法优化,一个对象状态优化。

行为型:观察者模式 ⭐⭐

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。 —— Graphic Design Patterns

在观察者模式里,至少应该有两个关键角色是一定要出现的——发布者和订阅者。用面向对象的方式表达的话,那就是要有两个类。

发布者类中做的是方法调用,订阅者类里做的就是方法的定义。

改模式又被称为发布-订阅者模式或消息机制,但是要较起真来,又不能给它们划严格的等号。观察者模式和发布-订阅模式之间的区别,在于是否存在第三方、发布者能否直接感知订阅者。如下图:

image

典例:Vue2.x数据双向绑定原理(defineProperty);Event Bus/ Event Emitter全局事件总线

// 监听器:
// observe方法遍历并包装对象属性
function observe(target) {
    // 若target是一个对象,则遍历它
    if(target && typeof target === 'object') {
        Object.keys(target).forEach((key)=> {
            // defineReactive方法会给目标属性装上“监听器”
            defineReactive(target, key, target[key])
        })
    }
}

// 定义defineReactive方法
function defineReactive(target, key, val) {
    const dep = new Dep()
    // 属性值也可能是object类型,这种情况下需要调用observe进行递归遍历
    observe(val)
    // 为当前属性安装监听器
    Object.defineProperty(target, key, {
         // 可枚举
        enumerable: true,
        // 不可配置
        configurable: false, 
        get: function () {
            return val;
        },
        // 监听器函数
        set: function (value) {
            console.log(`${target}属性的${key}属性从${val}值变成了了${value}`)
            val = value
            // 通知所有订阅者
            dep.notify()
        }
    });
}

// 订阅者:
// 定义订阅者类Dep
class Dep {
    constructor() {
        // 初始化订阅队列
        this.subs = []
    }
    
    // 增加订阅者
    addSub(sub) {
        this.subs.push(sub)
    }
    
    // 通知订阅者(是不是所有的代码都似曾相识?)
    notify() {
        this.subs.forEach((sub)=>{
            sub.update()
        })
    }
}
// 一个简单的Event Bus实现
class EventEmitter {
  constructor() {
    // handlers是一个map,用于存储事件与回调之间的对应关系
    this.handlers = {}
  }

  // on方法用于安装事件监听器,它接受目标事件名和回调函数作为参数
  on(eventName, cb) {
    // 先检查一下目标事件名有没有对应的监听函数队列
    if (!this.handlers[eventName]) {
      // 如果没有,那么首先初始化一个监听函数队列
      this.handlers[eventName] = []
    }

    // 把回调函数推入目标事件的监听函数队列里去
    this.handlers[eventName].push(cb)
  }

  // emit方法用于触发目标事件,它接受事件名和监听函数入参作为参数
  emit(eventName, ...args) {
    // 检查目标事件是否有监听函数队列
    if (this.handlers[eventName]) {
      // 如果有,则逐个调用队列里的回调函数
      this.handlers[eventName].forEach((callback) => {
        callback(...args)
      })
    }
  }

  // 移除某个事件回调队列里的指定回调函数
  off(eventName, cb) {
    const callbacks = this.handlers[eventName]
    const index = callbacks.indexOf(cb)
    if (index !== -1) {
      callbacks.splice(index, 1)
    }
  }

  // 为事件注册单次监听器
  once(eventName, cb) {
    // 对回调函数进行包装,使其执行完毕自动被移除
    const wrapper = (...args) => {
      cb.apply(...args)
      this.off(eventName, wrapper)
    }
    this.on(eventName, wrapper)
  }
}

行为型:迭代器模式

迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。 ——《设计模式:可复用面向对象软件的基础》

它就解决这一个问题——遍历。遍历集合的同时,我们不需要关心集合的内部结构。 集合可能是数组(Array)、类数组、对象(Object)、Map和Set等。

ES6约定,任何数据结构只要具备Symbol.iterator属性(这个属性就是Iterator的具体实现,它本质上是当前数据结构默认的迭代器生成函数),就可以被遍历——准确地说,是被for...of...循环和迭代器的next方法遍历。

典例:jQuery的each方法(内部)、ES6迭代器(外部)

内部迭代器:内部定义迭代规则,控制整个迭代过程,外部只需一次初始调用

优点:调用方式简单,外部仅需一次调用

缺点:迭代规则预先设置,欠缺灵活性。无法实现复杂遍历需求(如: 同时迭代比对两个数组)

// jQuery源码片段 each实现
each: function( obj, callback ) {
	var length, i = 0;

	if ( isArrayLike( obj ) ) {
		length = obj.length;
		for ( ; i < length; i++ ) {
			if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {
				break;
			}
		}
	} else {
		for ( i in obj ) {
			if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {
				break;
			}
		}
	}

	return obj;
},

...

// jQuery 的 $.each使用
$.each(['Angular', 'React', 'Vue'], function(index, value) {
    console.log([index, value]);
});

// 输出:[0, Angular]  [1, React]  [2, Vue]

外部迭代器: 外部显示(手动)地控制迭代下一个数据项

优点:灵活性更佳,适用面广,能应对更加复杂的迭代需求

缺点:需显示调用迭代进行(手动控制迭代过程),外部调用方式较复杂

// ES6迭代器:
function makeRangeIterator(start = 0, end = Infinity, step = 1) {
    let nextIndex = start;
    let iterationCount = 0;

    const rangeIterator = {
       next: function() {
           let result;
           if (nextIndex < end) {
               result = { value: nextIndex, done: false }
               nextIndex += step;
               iterationCount++;
               return result;
           }
           return { value: iterationCount, done: true }
       }
    };
    return rangeIterator;
}
// 使用这个迭代器看起来像这样:
let it = makeRangeIterator(1, 10, 2);

let result = it.next();
while (!result.done) {
 console.log(result.value); // 1 3 5 7 9
 result = it.next();
}

生成器函数使用 function*语法编写。

// 生成器函数
function* makeRangeIterator(start = 0, end = Infinity, step = 1) {
    for (let i = start; i < end; i += step) {
        yield i;
    }
}
var a = makeRangeIterator(1,10,2)
a.next() // {value: 1, done: false}
a.next() // {value: 3, done: false}
a.next() // {value: 5, done: false}
a.next() // {value: 7, done: false}
a.next() // {value: 9, done: false}
a.next() // {value: undefined, done: true}

行为型:模板方法模式

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

通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。

框架确定,执行顺序确定,步骤填充重写。

典例.vue单文件模板,钩子方法hook

从大的方面来讲,模板方法模式常被架构师用于搭建项目的框架,架构师定好了框架的骨架,程序员继承框架的结构之后,负责往里面填空。

/*
 * Coffee or Tea
 * (1) 把水煮沸
 * (2) 用沸水浸泡咖啡/茶叶
 * (3) 把咖啡/茶水倒进杯子
 * (4) 加糖和牛奶/柠檬
 */

/* 抽象父类:饮料 */
var Beverage = function(){};
// (1) 把水煮沸
Beverage.prototype.boilWater = function() {
  console.log("把水煮沸");
};
// (2) 沸水浸泡
Beverage.prototype.brew = function() {
  throw new Error("子类必须重写brew方法");
};
// (3) 倒进杯子
Beverage.prototype.pourInCup = function() {
  throw new Error("子类必须重写pourInCup方法");
};
// (4) 加调料
Beverage.prototype.addCondiments = function() {
  throw new Error("子类必须重写addCondiments方法");
};

/* 模板方法 */
Beverage.prototype.init = function() {
  this.boilWater();
  this.brew();
  this.pourInCup();
  this.addCondiments();
}

/* 实现子类 Coffee*/
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();

行为型:职责链模式

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

职责链模式的最大优点就是解耦了请求发送者和N个接收者之间的复杂关系,由于不知道链中的哪个节点可以处理你发出的请求,所以你只需把请求传递给第一个节点即可。

解决if-else 地狱

// 方法封装
// 执行节点
class ChainNode {
    constructor(main, next, options) {
        this.main = main
        this.next = next
        this.options = options
    }
    start () {
        let res = this.main(...arguments)
        res && this.next.start(...arguments)
    }
    setNext (callback) {
        this.next = callback
    }
}
// 链
class ResponsibilityChain {
    constructor() {
        this.chainNodes = {} // 责任节点
    }
    getChainNodes(chainName) { // 获取责任节点
        return this.chainNodes[chainName]
    }
    setChainNodes(name, chainNode) { // 设置责任节点
        this.chainNodes[name] = chainNode
    }
    insertChainNode () {}
    chainConstitute(array) { // 链
        for (let index = 0; index < array.length; index++) {
            let element = this.chainNodes[array[index]]
            let next = this.chainNodes[array[index + 1]]
            element.next = next
        }
    }
}

// 业务逻辑
var order500 = function(orderType, pay, stock) {
  if (orderType === 1 && pay === true) {
    console.log('500 元定金预购,得到100 优惠券');
  } else {
    return 'nextSuccessor'; // 我不知道下一个节点是谁,反正把请求往后面传递
  }
};
var order200 = function(orderType, pay, stock) {
  if (orderType === 2 && pay === true) {
    console.log('200 元定金预购,得到50 优惠券');
  } else {
    return 'nextSuccessor'; // 我不知道下一个节点是谁,反正把请求往后面传递
  }
};
var orderNormal = function(orderType, pay, stock) {
  if (stock > 0) {
    console.log('普通购买,无优惠券');
  } else {
    console.log('手机库存不足');
  }
};

// 包装职责链的节点
let responsibilityChain = new ResponsibilityChain();
responsibilityChain.setChainNodes('chainNode_1', new ChainNode(order500));
responsibilityChain.setChainNodes('chainNode_2', new ChainNode(order200));
responsibilityChain.setChainNodes('chainNode_final', new ChainNode(orderNormal));

// 指定节点在职责链中的顺序
responsibilityChain.chainConstitute(['chainNode_1', 'chainNode_2', 'chainNode_final']);

// 请求使用
let chainOrder500 = responsibilityChain.getChainNodes('chainNode_1');
chainOrder500.start( 1, true, 500 ); // 输出:500 元定金预购,得到100 优惠券
chainOrder500.start( 2, true, 500 ); // 输出:200 元定金预购,得到50 优惠券
chainOrder500.start( 3, true, 500 ); // 输出:普通购买,无优惠券
chainOrder500.start( 1, false, 0 ); // 输出:手机库存不足

// 如果需求变更,需要增加节点
var order300 = function(orderType, pay, stock){
 if (orderType === 3 && pay === true) {
    console.log('300 元定金预购,得到70 优惠券');
  } else {
    return 'nextSuccessor'; // 我不知道下一个节点是谁,反正把请求往后面传递
  }
};
responsibilityChain.setChainNodes('chainNode_3', new ChainNode(order300));
responsibilityChain.chainConstitute(['chainNode_1', 'chainNode_2', 'chainNode_3', 'chainNode_final']);

chainOrder500.start( 3, true, 500 ); // 输出:300 元定金预购,得到70 优惠券

行为型:命令模式

命令模式:请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令。

命令模式的由来,其实是回调(callback)函数的一个面向对象的替代品。

  1. 发送者(setCommond):不关心给哪个元素(对象),以及什么事件,只要通过参数传入就好。

  2. 命令对象(commondObj):只需要接收到接受者的参数,当发送者发出命令时,执行就好。

  3. 接受者(menu):不用关心在哪里被调用被谁调用,只需要按需执行就好了。

class Receiver {  // 接收者类
  execute() {
    console.log('接收者执行请求');
  }
}

class Command {   // 命令对象类
  constructor(receiver) {
    this.receiver = receiver;
  }
  execute () {    // 调用接收者对应接口执行
    console.log('命令对象->接收者->对应接口执行');
    this.receiver.execute();
  }
}

class Invoker {   // 发布者类
  constructor(command) {
    this.command = command;
  }
  invoke() {      // 发布请求,调用命令对象
    console.log('发布者发布请求');
    this.command.execute();
  }
}

const warehouse = new Receiver();       // 仓库
const order = new Command(warehouse);   // 订单
const client = new Invoker(order);      // 客户
client.invoke();

/*
输出:
  发布者发布请求
  命令对象->接收者->对应接口执行
  接收者执行请求
*/

行为型:备忘录模式 ⭐

在不破坏对象的封装性的前提下,在对象之外捕获并保存该对象内部的状态以便日后对象使用或者对象恢复到以前的某个状态。

场景:实现一个简单的状态机,用于状态的保存,回退。分页数据缓存,跳转权限记录等等。

//备忘类

class Memento {
    constructor(content){
        this.content = content
    }
    getContent(){
        return this.content
    }
}

// 备忘列表

class CareTaker {
    constructor(){
        this.list = []
    }
    add(memento){
        this.list.push(memento)
    }
    get(index){
        return this.list[index]
    }
}

// 编辑器

class Editor {
    constructor(){
        this.content = null
    }
    setContent(content){
        this.content = content
    }
    getContent(){
     return this.content
    }
    saveContentToMemento(){
        return new Memento(this.content)
    }
    getContentFromMemento(memento){
        this.content = memento.getContent()
    }
}

//测试代码

let editor = new Editor()
let careTaker = new CareTaker()


editor.setContent('111')
editor.setContent('222')
careTaker.add(editor.saveContentToMemento())
editor.setContent('333')
careTaker.add(editor.saveContentToMemento())
editor.setContent('444')

console.log(editor.getContent()) //444
editor.getContentFromMemento(careTaker.get(1))
console.log(editor.getContent()) //333

editor.getContentFromMemento(careTaker.get(0))
console.log(editor.getContent()) //222

缺点:备忘录模式的主要缺点是资源消耗过大,如果需要保存的原发器类的成员变量太多,就不可避免的需要占用大量的存储空间。

行为型:中介者模式

中介者模式的作用就是解除对象与对象之间的紧耦合关系。增加一个中介者对象后,所有的相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知中介者对象即可。中介者使各对象之间耦合松散,而且可以独立地改变它们之间的交互。中介者模式使网状的多对多关系变成了相对简单的一对多关系。

// 购买商品-选配 例子
// 所有判断逻辑通过中介者,解耦配置对象间的耦合
var goods = {
  // 手机库存
  "red|32G": 3,
  "red|16G": 0,
  "blue|32G": 1,
  "blue|16G": 6,
};
// 中介
var mediator = (function () {
  var colorSelect = document.getElementById("colorSelect"),
    memorySelect = document.getElementById("memorySelect"),
    numberInput = document.getElementById("numberInput"),
    colorInfo = document.getElementById("colorInfo"),
    memoryInfo = document.getElementById("memoryInfo"),
    numberInfo = document.getElementById("numberInfo"),
    nextBtn = document.getElementById("nextBtn");
  return {
    changed: function (obj) {
      var color = colorSelect.value, // 颜色
        memory = memorySelect.value, // 内存
        number = numberInput.value, // 数量
        stock = goods[color + "|" + memory]; // 颜色和内存对应的手机库存数量
      if (obj === colorSelect) {
        // 如果改变的是选择颜色下拉框
        colorInfo.innerHTML = color;
      } else if (obj === memorySelect) {
        memoryInfo.innerHTML = memory;
      } else if (obj === numberInput) {
        numberInfo.innerHTML = number;
      }
      if (!color) {
        nextBtn.disabled = true;
        nextBtn.innerHTML = "请选择手机颜色";
        return;
      }
      if (!memory) {
        nextBtn.disabled = true;
        nextBtn.innerHTML = "请选择内存大小";
        return;
      }
      if (((number - 0) | 0) !== number - 0) {
        // 输入购买数量是否为正整数
        nextBtn.disabled = true;
        nextBtn.innerHTML = "请输入正确的购买数量";
        return;
      }
      nextBtn.disabled = false;
      nextBtn.innerHTML = "放入购物车";
    },
  };
})();
// 事件函数:
colorSelect.onchange = function () {
  mediator.changed(this);
};
memorySelect.onchange = function () {
  mediator.changed(this);
};
numberInput.oninput = function () {
  mediator.changed(this);
};

缺点:最大的缺点是系统中会新增一个中介者对象,因为对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者对象经常是巨大的。中介者对象自身往往就是一个难以维护的对象。

行为型:访问者模式

访问者模式是将对数据操作和数据结构进行分离,将对数据中各元素的操作封装成独立的类,使其在不改变数据结构的前提下可以拓展对数据新的操作。

访问者模式包含抽象访问者、具体访问者、抽象元素、具体元素和对象结构。

  1. 抽象访问者:定义一个访问具体元素的接口,使每一个具体元素都有一个访问操作。
  2. 具体访问者:实现抽象访问者中定义的各个访问操作,确定访问者访问一个元素时该做什么。
  3. 抽象元素:声明一个包含接收操作的接口。
  4. 具体元素:实现包含接收操作的接口。
  5. 对象结构:包含元素角色的容器,提供让访问者对象遍历容器中所有元素的方法。
/**
 * 案例:
 * 有两家公司,【艺术公司】和【造币公司】;
 * 有两种材料,【纸】和【铜】。
 * 将【纸】给到【艺术公司】,可以得到图纸;
 * 将【铜】给到【艺术公司】,可以设计铜像。
 * 将【纸】给到【造币公司】,可以造出纸币;
 * 将【铜】给到【造币公司】,可以造出铜币。
 */

//抽象访问者-公司
class Company {
  create() {}
}
//具体访问者-艺术公司
class ArtCompany extends Company {
  create(el) {
    if (el instanceof Paper) {
      return "画图";
    } else if (el instanceof Cuprum) {
      return "设计铜像";
    }
  }
}
//具体访问者-造钱公司
class Mint extends Company {
  create(el) {
    if (el instanceof Paper) {
      return "造纸币";
    } else if (el instanceof Cuprum) {
      return "造铜币";
    }
  }
}
//抽象元素-材料
class Material {
  accept(visitor) {}
}
//抽象元素-纸币
class Paper extends Material {
  accept(visitor) {
    return visitor.create(this);
  }
}
//抽象元素-铜币
class Cuprum extends Material {
  accept(visitor) {
    return visitor.create(this);
  }
}
//对象结构-添加或删除材料,根据不同的公司做出不同的东西
class SetMaterial {
  constructor() {
    this.list = [];
  }
  accept(visitor) {
    let str = "";
    for (let i of this.list) {
      str += i.accept(visitor) + "\n";
    }
    return str;
  }
  add(el) {
    this.list.push(el);
  }
  remove(el) {
    this.list.filter((item) => item !== el);
  }
}
class Customer {
  static main() {
    //定义材料对象
    let setMaterial = new SetMaterial();
    //添加材料
    setMaterial.add(new Paper());
    setMaterial.add(new Cuprum());
    //根据不同的公司生产不同的东西
    let pro = setMaterial.accept(new ArtCompany());
    let pro1 = setMaterial.accept(new Mint());
    console.log(pro);
    console.log(pro1);
  }
}
Customer.main();

场景:

  • 对象结构相对稳定,但其操作算法经常变化。
  • 对象结构中的对象需要提供多种不同且不相关的操作。而且要避免这些操作的变化影响对象结构。
  • 对象结构包含很多类型的对象。希望对这些对象实施一些依赖于其具体类型的操作。

行为型:解释器模式

给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。

解释器模式提供了对语言或语法的的一些判断方式。

所谓解释器模式, 正则表达式就是他的一种应用,这其实为正则表达式提供了一种文法, 如何定义一个正则表达式, 以及如何解释这个正则表达式。

如果一种特定的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,该解释器通过解释这些句子来解决问题。

利用场景比较少。

// 定义对于语法的断言
class TerminalExpression {
    constructor(data){
       this.data = data; 
    }
    interpret(context) {
       if(context.indexOf(this.data)>-1){
          return true;
       }
       return false;
    }
}

// 添加断言的操作符 (表达式判断符)
class OrExpression {
    constructor(expr1, expr2) { 
        this.expr1 = expr1;
        this.expr2 = expr2;
    }
    interpret(context) {      
        return this.expr1.interpret(context) || this.expr2.interpret(context);
    }
}
class AndExpression {
    constructor(expr1, expr2) { 
        this.expr1 = expr1;
        this.expr2 = expr2;
    }
    interpret(context) {      
        return this.expr1.interpret(context) && this.expr2.interpret(context);
    }
}

// 组合断言操作符
// 获取对应表达式
function getMaleExpression(){
    const robert = new TerminalExpression("Robert");
    const john = new TerminalExpression("John");
    return new OrExpression(robert, john);    
}
function getMarriedWomanExpression(){
    const julie = new TerminalExpression("Julie");
    const married = new TerminalExpression("Married");
    return new AndExpression(julie, married);    
}

// 实现断言的判断
// 判断语句断言
const isMale = getMaleExpression();
const isMarriedWoman = getMarriedWomanExpression();
console.log("John is male? " + isMale.interpret("John"));
console.log("Julie is a married women? " 
+ isMarriedWoman.interpret("Married Julie"));
/**
 * output:
 * John is male? true
 * Julie is a married women? true
 */

参考

⚠️ **GitHub.com Fallback** ⚠️