设计模式分类导航

238 阅读9分钟

设计模式六个基本原则 

1.单一职责 

2.开闭原则 

3.里氏替换 

4.依赖倒转 

5.接口隔离 

6.迪米特法则(最少知识原则)  

设计模式分类 

一、创建型模式 

1.单例 

2.建造者 

3.原型 

4.工厂方法 

简单工厂:

/**
 * 抽象咖啡类
 */
abstract class Coffee {
  constructor(public name: string) { }
}

class LatteCoffee extends Coffee { }

class MachaCoffee extends Coffee { }

class AmericanoCoffee extends Coffee { }

// 简单工厂实现
class CoffeFactory {
  static buy(name: string) {
    switch (name) {
      case 'LatteCoffee':
        return new LatteCoffee('拿铁咖啡');
      case 'MachaCoffee':
        return new MachaCoffee('摩卡咖啡');
      case 'AmericanoCoffee':
        return new AmericanoCoffee('美式咖啡');
      default:
        throw new Error('没有您需要的咖啡')
    }
  }
}

console.log(CoffeFactory.buy('LatteCoffee'));
console.log(CoffeFactory.buy('MachaCoffee'));
console.log(CoffeFactory.buy('AmericanoCoffee'));

工厂方法:

/**
 * 抽象咖啡类
 */
abstract class Coffee {
  constructor(public name: string) { }
}

class LatteCoffee extends Coffee { }

class MachaCoffee extends Coffee { }

class AmericanoCoffee extends Coffee { }

// 抽象咖啡工厂类
abstract class CoffeeFactory {
  constructor() { }
}

class LatteCoffeeFactory extends CoffeeFactory {
  createCoffe() {
    console.log('您创建了一份拿铁咖啡');
  }
}
class MachaCoffeeFactory extends CoffeeFactory {
  createCoffe() {
    console.log('您创建了一份摩卡咖啡');
  }
}
class AmericanoCoffeeFactory extends CoffeeFactory {
  createCoffe() {
    console.log('您创建了一份美式咖啡');
  }
}

// 在工厂方法里,不再由 Factory 来创建产品,而是先创建了具体的工厂,然后具体的工厂创建产品
class Factory {
  static buy(name: string) {
    switch (name) {
      case 'LatteCoffee':
        // 先创建了拿铁咖啡具体的工厂,然后再由具体的工厂再创建产品
        return new LatteCoffeeFactory().createCoffe();
      case 'MachaCoffee':
        // 先创建了摩卡咖啡具体的工厂,然后再由具体的工厂再创建产品
        return new MachaCoffeeFactory().createCoffe();
      case 'AmericanoCoffee':
        // 先创建了美式咖啡具体的工厂,然后再由具体的工厂再创建产品
        return new AmericanoCoffeeFactory().createCoffe();
      default:
        throw new Error('没有您需要的咖啡')
    }
  }
}

console.log(Factory.buy('LatteCoffee'));
console.log(Factory.buy('MachaCoffee'));
console.log(Factory.buy('AmericanoCoffee'));

5.抽象工厂  

/**
 * 产品抽象类或接口
 */

abstract class MacProdoct { }
abstract class IWatchProdoct { }
abstract class PhoneProdoct { }

/**
 * 具体产品实现类
 */
class AppleMacProdoct extends MacProdoct { }
class HuaweiMacProdoct extends MacProdoct { }

class AppleIWatchProdoct extends IWatchProdoct { }
class HuaweiIWatchProdoct extends IWatchProdoct { }

class ApplePhoneProdoct extends PhoneProdoct { }
class HuaweiPhoneProdoct extends PhoneProdoct { }

/**
 * 抽象工厂
 */
abstract class AbstractProdoctFactory {
  abstract createMacProdoct(): MacProdoct;
  abstract createIWatchProdoct(): IWatchProdoct;
  abstract createPhoneProdoct(): PhoneProdoct;
}

/**
 * 具体产品工厂 - 苹果
 */
class AppleProdoct extends AbstractProdoctFactory {
  createMacProdoct() {
    return new AppleMacProdoct();
  }
  createIWatchProdoct() {
    return new AppleIWatchProdoct();
  }
  createPhoneProdoct() {
    return new ApplePhoneProdoct();
  }
}
/**
 * 具体产品工厂 - 华为
 */
class HuaweiProdoct extends AbstractProdoctFactory {
  createMacProdoct() {
    return new HuaweiMacProdoct();
  }
  createIWatchProdoct() {
    return new HuaweiIWatchProdoct();
  }
  createPhoneProdoct() {
    return new HuaweiPhoneProdoct();
  }
}

let huaweiProduct = new HuaweiProdoct();
console.log(huaweiProduct.createMacProdoct()); // 购买华为电脑
console.log(huaweiProduct.createIWatchProdoct()); // 购买华为手表
console.log(huaweiProduct.createPhoneProdoct()); // 购买华为手机

简单工厂: 

 唯一工厂类,一个产品抽象类,工厂类的创建方法依据入参判断并创建具体产品对象。 

 适用情况包括:工厂类负责创建的对象比较少;客户端只知道传入工厂类的参数,对于如何创建对象不关心。 

工厂方法: 

 多个工厂类,一个产品抽象类,利用多态创建不同的产品对象,避免了大量的 if-else 判断。 

适用情况包括:一个类不知道它所需要的对象的类;一个类通过其子类来指定创建哪个对象;将创建对象的任务委托给多个工厂子类中的某一个,客户端在使用时可以无须关心是哪一个工厂子类创建产品子类,需要时再动态指定。 

抽象工厂: 

 多个工厂类,多个产品抽象类,产品子类分组,同一个工厂实现类创建同组中的不同产品,减少了工厂子类的数量。 

 适用情况包括:一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节;系统中有多于一个的产品族,而每次只使用其中某一产品族;属于同一个产品族的产品将在一起使用;系统提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于具体实现。

二、结构型模式 

1.代理

包装对象,为了增强原对象功能,但又不允许直接修改原对象时,使用代理模式。 

应用:修改原对象的属性时,修改之前和修改之后都需要加上打印日志,但不允许直接修改原对象。

const obj = {
  name: "john",
  age: 20
};
const proxyObj = new Proxy(obj, {
  get: (target, key) => {
    return target[key];
  },
  set: (target, key, value) => {
    if (key === "age") {
      console.log("age before change===", target[key]);
      target[key] = value;
      console.log("age after change===", value);
      return;
    }
    target[key] = value;
  }
});
proxyObj.age = 18;

输出结果:

age before change=== 20
age after change=== 18

2.装饰器

包装对象,可以不侵入原有代码内部的情况下修改类代码的行为,处理一些与具体业务无关的公共功能,常见:日志,异常,埋码等。

举例:ES7中的decorator

// log()函数接受Calculator类作为参数,并返回一个新函数替换 Calculator类的构造函数。
function log(name) {
  return function decorator(Class) {
    return (...args) => {
      console.log(`Arguments for ${name}: ${args}`);
      return new Class(...args);
    };
  }
}
@log('Multiply')
class Calculator {
    constructor (x,y) { }
}  
calculator = new Calculator(10,10);
// Arguments for Multiply: [10, 10]console.log(calculator);
// Calculator {}

3.适配器

包装对象,为了解决两个对象之间不匹配的问题,而原对象又不适合直接修改,此时可以使用适配器模式进行一层转换。

应用:

  • 使用一个已经存在的对象,但其方法或属性不符合我们的要求。
  • 统一多个类的接口设计。
  • 适配不同格式的数据。
  • 兼容老版本的接口。

举例:vue的计算属性

4.外观(门面) 

包装对象,提供一个统一的接口去访问多个子系统的多个不同的接口,为子系统中的一组接口提供统一的高层接口。使得子系统更容易使用,不仅简化类中的接口,而且实现调用者和接口的解耦。

与适配器模式的区别

适配器模式是将一个对象包装起来以改变其接口,而外观模式是将一群对象包装起来以简化其接口。
适配器是将接口转换为不同接口,而外观模式是提供一个统一的接口来简化接口。

举例:

// a.js
export default {
  getA (params) {
    // do something...
  }
}

// b.js 
export default {
  getB (params) {
    // do something...
  }
}

// app.js  外观模式为子系统提供同一的高层接口
import A from './a'
import B from './b'
export default {
  A,
  B
}

// 通过同一接口调用子系统

import app from './app'

app.A.getA(params);
app.B.getB(params);

5.组合

组合对象,将一组相关的对象,组合为‘部分-整体’的结构。 

举例:文件夹扫描

// 树对象 - 文件目录
class CFolder {
    constructor(name) {
        this.name = name;
        this.files = [];
    }
 
    add(file) {
        this.files.push(file);
    }
 
    scan() {
        for (let file of this.files) {
            file.scan();
        }
    }
}
 
// 叶对象 - 文件
class CFile {
    constructor(name) {
        this.name = name;
    }
 
    add(file) {
        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
*/

举例:万能遥控器

// 创建一个宏命令
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 );
//把登录QQ命令装进这个宏命令里
macroCommand2.add( openQQCommand );
 
<!--把各宏命令装进一个超级命令中去-->
var macroCommand = MacroCommand();
macroCommand.add( openAcCommand );
macroCommand.add( macroCommand1 );
macroCommand.add( macroCommand2 );

6.享元 

组合对象,将一群类似的对象抽离出内部状态和外部状态,内部状态为共享,外部状态根据实际情况和内部状态进行连接。 

举例:一个工厂需要 20 个男模特和 20 个女模特穿上 40 件新款衣服拍照做宣传。

不使用享元模式:

        let Model = function(sex, underwear) {
            this.sex = sex;
            this.underwear = underwear;
        }
        Model.prototype.takePhoto = function () {
            console.log('sex=' + this.sex + 'underwear = ' + this.underwear);
        }
 
        for(let i = 0; i < 20 ; i++){
            new Model('男',  'underwear' + i).takePhoto();
        }
        for(let i = 0; i < 20 ; i++){
            new Model('女',  'underwear' + i).takePhoto();
        }

使用享元模式重构:

        let ModelR = function( sex ) {
            this.sex = sex;
        }
        let ModelF = new ModelR( '女' );
        let ModelM = new ModelR('男');
 
        ModelR.prototype.takePhoto = function () {
            console.log('sex=' + this.sex + 'underwear = ' + this.underwear);
        }
        for(let i = 0; i < 20 ; i++) {
            ModelF.underwear = 'underwear' + i;
            ModelF.takePhoto();
        }
        for(let i = 0; i < 20 ; i++) {
            ModelM.underwear = 'underwear' + i;
            ModelM.takePhoto();
        }

分析:利用所有对象相同的属性来初始化创建对象,上述例子中利用人的性别这个属性来创建对象,而性别这个属性只有男女这两种,因此我们只需要创建两个对象,将衣服作为其他不同的属性添加到对象中便完成了对象的替换,相当于拥有 40 个不同的对象,但是实际只创建了两个。

7.桥接

组合对象,分离抽象部分和实现部分,通过桥接的方式连接对象;沿着多个维度变化不会增加系统的复杂度,还可以达到抽象和实现解耦的目的。 

举例:页面有 Toast、Message 两种形态的弹窗,弹窗都有出现和隐藏等行为,这些行为可以使用不同风格的动画。

function Toast(node, animation) {
    this.node = node
    this.animation = animation
}
//调用 animation 的show方法
Toast.prototype.show = function() {
    this.animation.show(this.node)   
}
//调用 animation 的hide方法
Toast.prototype.hide = function() {
    this.animation.hide(this.node)   
}
 
function Message(node, animation) {
    this.node = node
    this.animation = animation
}
//调用 animation 的show方法
Message.prototype.show = function() {
    this.animation.show(this.node)   
}
//调用 animation 的hide方法
Message.prototype.hide = function() {
    this.animation.hide(this.node)   
}
 
const Animations = {
    bounce: {
        show: function(node) { console.log(node + '弹跳着出现') }
        hide: function(node) { console.log(node + '弹跳着消失') }
    },
    slide: {
        show: function(node) { console.log(node + '滑着出现') }
        hide: function(node) { console.log(node + '滑着消失') }        
    }
}
 
let toast = new Toast('元素1', Animations.bounce )
toast.show()
 
let messageBox = new Message('元素2', Animations.slide)
messageBox.hide()

三、行为型模式 

观察者、模板方法、迭代器、责任链、策略、状态、备忘录、解释器、命令、访问者、中介者

 一、单个对象行为封装: 

1.策略

受外部状态影响改变对象行为;封装不同的对象行为,返回一个行为接口 。 

应用:重构大量if-else逻辑或switch-case逻辑。  

2.状态

受内部状态影响改变对象行为;封装不同的状态行为(包含对象行为)。

应用:重构大量if-else逻辑或switch-case逻辑,且分支逻辑中修改同一个状态。

举例:手写Promise

3.模板方法

将不同功能组合在一起,只提供框架,具体实现还需要调用者传进来。  

举例:把东西放进冰箱

// 把东西放进冰箱
function putIntoIcebox(openFunc, pickFunc, putFunc){
   openFunc()
   pickFunc()
   putFunc()
}
// 打开冰箱
function openIcebox(){}
// 抱起大象
function pickElephant(){}
// 放大象
function putElephant(){}
// 把大象放进冰箱
putIntoIcebox(openIcebox, pickElephant, putElephant)
// 抱起老虎
function pickTiger(){}
// 放老虎
function putTiger(){}
// 把老虎放进冰箱
putIntoIcebox(openIcebox, pickTiger, putTiger)

4.迭代器

提供一个接口,访问一个对象内的所有行为(属性),接口可提供多种访问顺序。 

应用:数组遍历、对象遍历。  

5.备忘录

为对象行为提供一个快照功能,能随时提供返回。 

应用:状态管理数据持久化和还原,保证刷新页面应用状态不丢失。 

 二、依赖对象行为封装: 

6.命令

对象行为一对一,分离请求和接收对象行为的分离,并可在其中添加其他操作,因为命令对象已经持有接受者的引用。 

7.发布订阅(广义观察者)

对象行为一对多(发散:多个对象行为无关联),分离发布和多个订阅对象的行为。 

应用:对象通信解耦。

举例:

export class EventBus {  
    static map = new Map();
    static publish(key, data) {
        const funcArr = this.map.get(key) || [];
        for (let index = 0; index < funcArr.length; index++) {
            funcArr[index](data);    
        }
    }
    static subscribe(key, callback) {
        const funcArr = this.map.get(key) || [];
        this.map.set(key, funcArr.push(callback));
    }
}

8.职责链

对象行为一对多(串行:多个对象行为有关联),分离请求和多个接收对象的行为。 

应用:try-catch的处理、作用域链、原型链、DOM节点中的事件冒泡。

class Chain {
  construct (fn) {
    this.fn = fn
    this.successor = null
  }

  setNextSuccessor (successor) {
    return this.successor = successor
  }

  next () {
    return this.successor && this.successor.passRequest.apply(this.successor, arguments)
  }

  passRequest () {
    const res = this.fn.apply(this, arguments)

    if (res === 'nextSuccessor') {
      return this.successor && this.successor.passRequest.apply(this.successor, arguments)
    }
    return res
  }
}

看一个异步使用的例子:

const fn1 = new Chain(function () {
  console.log(1)
  return 'nextSuccessor'
})

const fn1 = new Chain(function () {
  console.log(2)
  setTimeout(() => {
    this.next()
  }, 1000)
})

const fn3 = new Chain(function () {
  console.log(3)
})

fn1.setNextSuccessor(fn2).setNextSuccessor(fn3)
fn1.passRequest()

职责链优点:

  1. 符合单一职责,使每个方法中都只有一个职责。
  2. 符合开放封闭原则,在需求增加时可以很方便的扩充新的责任。
  3. 使用时候不需要知道谁才是真正处理方法,减少大量的 ifswitch 语法。

职责链缺点:

  1. 团队成员需要对责任链存在共识,否则当看到一个方法莫名其妙的返回一个 next 时一定会很奇怪。
  2. 出错时不好排查问题,因为不知道到底在哪个责任中出的错,需要从链头开始往后找。
  3. 就算是不需要做任何处理的方法也会执行到,因为它在同一个链中,如果有异步请求的话,执行时间也许就会比较长。

9.中介者

对象行为多对多,使网状的多对多关系变成了相对简单的一对多关系,本质上是对多个对象模块之间复杂交互的封装。  

封装子系统间各模块之间的直接交互,松散模块间的耦合。

class A {
  constructor () {
    this.number = 0
  }
  setNumber (num, middleman) {
    this.number = num
    if (middleman) {
      middleman.setB()
    }
  }
}
class B {
  constructor () {
    this.number = 0
  }
  setNumber (num, middleman) {
    this.number = num
    if (middleman) {
      middleman.setA()
    }
  }
}
class Middleman {
  constructor (a, b) {
    this.a = a
    this.b = b
  }
  setA () {
    let number = this.b.number
    this.a.setNumber(number * 100)
  }
  setB () {
    let number = this.a.number
    this.b.setNumber(number / 100)
  }
}
const buyer = new A()
const seller = new B()
const middleman = new Middleman(buyer, seller)
buyer.setNumber(100, middleman)
console.log(buyer.number, seller.number) // 100 1
seller.setNumber(100, middleman)
console.log(buyer.number, seller.number) //10000 100

10.访问者

访问者模式先把一些可复用的行为抽象到一个函数(对象)里,这个函数我们就称为访问者(Visitor)。如果另外一些对象要调用这个函数,只需要把那些对象当作参数传给这个函数,在js里我们经常通过call或者apply的方式传递 this对象给一个Visitor函数。

// 接收者类,拥有accept方法
class Employee {
  constructor(name, salary) {
    this.name = name;
    this.salary = salary;
  }

  getSalary() {
    return this.salary;
  }

  setSalary(salary) {
    this.salary = salary;
  }

  // accept方法接收访问者对象,将自身引用传递给visitor
  accept(visitor) {
    visitor.visit(this);
  }
}

// 访问者类,具有visit方法,在接收者的accpet方法被调用
class Visitor {
  constructor(times) {
    this.times = times;
  }
  visit(employee) {
    employee.setSalary(employee.getSalary() * this.times);
  }
}

const emp = new Employee("小明", 3000);
const dobule = new Visitor(2);
const triple = new Visitor(3);
emp.accept(dobule);
console.log(emp.getSalary()); //6000
emp.accept(triple);
console.log(emp.getSalary()); //18000

11.解释器

对于一种语言,给出其文法表示形式,并定义一种解释器,通过使用这种解释器来解释语言中定义的句子。

class Context {
    constructor() {
      this._list = []; // 存放 终结符表达式
      this._sum = 0; // 存放 非终结符表达式(运算结果)
    }
  
    get sum() {
      return this._sum;
    }
    set sum(newValue) {
      this._sum = newValue;
    }
    add(expression) {
      this._list.push(expression);
    }
    get list() {
      return [...this._list];
    }
  }
  
  class PlusExpression {
    interpret(context) {
      if (!(context instanceof Context)) {
        throw new Error("TypeError");
      }
      context.sum = ++context.sum;
    }
  }
  class MinusExpression {
    interpret(context) {
      if (!(context instanceof Context)) {
        throw new Error("TypeError");
      }
      context.sum = --context.sum;
    }
  }
  
 
  const context = new Context();
  
 
  context.add(new PlusExpression());//依次添加: 加法 | 加法 | 减法
  context.add(new PlusExpression());
  context.add(new MinusExpression());
  
 
  context.list.forEach(expression => expression.interpret(context));//依次执行: 加法 | 加法 | 减法
  console.log(context.sum);