【速学速记】《设计模式》这样学就对了!

共 23778字,需浏览 48分钟

 ·

2021-04-30 18:11

李运通,微医前端技术部前端工程师。最怕你一生碌碌无为,还安慰自己平凡可贵。

设计模式解决什么痛点?

它是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,可以反复使用。其目的是为了提高代码的可复用性、可读性、可维护性
设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解。
不要重复造轮子

什么是面向对象编程

  • 面向对象编程是一种编程范式或编程风格。它以类或对象作为组织代码的基本单元,并将封装、抽象、继承、多态四大特性,作为代码设计和实现的基石。
  • 面向对象编程语言是支持类或对象的语法机制,并有现成的语法机制,能方便的实现面向对象编程四大特性的编程语言
  • 面向对象开发包括面向对象分析 OOA、面向对象设计 OOD、面向对象编程 OOP

10大设计原则

1.单一职责原则 SRP

实现类要职责单一:如果一段代码块(函数 类 模块)负责多个功能,那么当 A 功能需求发生改变的时候改动了代码,就有可能导致 B 功能出现问题,所以一段代码块只应该负责一个职责。

2.开放-封闭原则 OCP

要对扩展开放,对修改关闭:通过修改老代码来实现新功能可能导致老模块出现 BUG,所以我们应该通过开发新代码块来实现新功能

3.里氏替换原则 LSP

不要破坏继承体系:程序中的子类应该可以替换父类出现的任何地方并保持预期不变。所以子类尽量不要改变父类方法的预期行为

4.接口隔离原则 ISP

设计接口的时候要精简单一:当类 A 只需要接口 B 中的部分方法时,因为实现接口需要实现其所有的方法,于是就造成了类 A 多出了部分不需要的代码。这时应该将 B 接口拆分,将类A需要和不需要的方法隔离开来

5.依赖倒置原则 DIP

面向接口编程:抽象不应该依赖细节,细节应该依赖于抽象。核心是面向接口编程,我们应该依赖于抽象接口,而不是具体的接口实现类或具体的对象

注意:上面的 SOLID 又称为5大设计原则

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

降低耦合度:一个类或对象应该对其它对象保持最少的了解。只与直接的朋友(耦合)通信。

7.组合/聚合复用原则 CRP

多用组合少用继承:尽可能通过组合已有对象(借用他们的能力)来实现新的功能,而不是使用继承来获取这些能力

8.不要重复你自己 DRY

功能语义重复应该合并,代码执行重复应该删减,代码逻辑重复但语义不同应该保留

9.尽量保持简单 KISS

尽可能使用简单可读性高的代码实现功能,而不用逻辑复杂、实现难度高、可读性差的方式

10.不要过度设计你暂时用不到的逻辑 YAGNI

不要过度优化、不要过度预留扩展点、不要设计同事看不懂的代码。

23种设计模式速记

  • 速记:5+7=11
    • 5种创建型
    • 7种结构型
    • 11种行为型
  • 创建型:抽工单建原型
    • 抽象工厂、工厂、单例、建造者、原型
  • 结构型:桥代理装饰适配器,享元组合成门面
    • 桥接、代理、装饰器、适配器、享元、组合、门面(外观)
  • 行为型:观察模板迭代的状态,命令中介解释职责链,访问策略备忘录
    • 观察者、模板、迭代、状态、命令、中介者、解释器、职责链、访问者、策略、备忘录

创建型设计模式

封装对象创建过程,将对象的创建和使用解耦

单例模式

应用场景

处理资源访问冲突、用来创建全局唯一类

解决方案

  • 懒汉式:用到的时候才创建(场景:不一定需要用到、创建代价大、延迟加载、需要快速启动系统)
  • 饿汉式:系统启动时创建(场景:必定用到、临时创建影响响应速度)
  • 多例:同一类的固定个数相同实例

工厂模式

应用场景

用来创建继承同一父类、实现同一接口的子类对象,由给定的类型参数创建具体的对象。

解决方案

enum HelloType {
  A,
  B
}

interface Hello {
  sayHello()
}

class A implements Hello {
  sayHello() {
    console.log('A');
  }
}

class B implements Hello {
  sayHello() {
    console.log('B');
  }
}

class HelloFactory {
  static list = new Map<HelloType, Hello>([
    [HelloType.A, new A()],
    [HelloType.B, new B()]
  ])

  static getHello(type: HelloType) {
    return HelloFactory.list.get(type)
  }
}

// test
HelloFactory.getHello(HelloType.A).sayHello()
HelloFactory.getHello(HelloType.B).sayHello()

抽象工厂模式

应用场景

继承同一父类、实现同一接口的子类对象,由给定的多个类型参数创建具体的对象。

解决方案

enum Type {
  A,
  B
}

enum Occupation {
  TEACHER,
  STUDENT
}

interface Hello {
  sayHello()
}

class TA implements Hello {
  sayHello() {
    console.log('Teacher A say hello')
  }
}

class TB implements Hello {
  sayHello() {
    console.log('Teacher B say hello')
  }
}

class SA implements Hello {
  sayHello() {
    console.log('Student A say hello')
  }
}

class SB implements Hello {
  sayHello() {
    console.log('Student B say hello')
  }
}

class AFactory {
  static list = new Map<Occupation, Hello>([
    [Occupation.TEACHER, new TA()],
    [Occupation.STUDENT, new SA()]
  ])

  static getHello(occupation: Occupation) {
    return AFactory.list.get(occupation)
  }
}

class BFactory {
  static list = new Map<Occupation, Hello>([
    [Occupation.TEACHER, new TB()],
    [Occupation.STUDENT, new SB()]
  ])

  static getHello(occupation: Occupation) {
    return BFactory.list.get(occupation)
  }
}

class HelloFactory {
  static list = new Map<Type, AFactory | BFactory>([
    [Type.A, AFactory],
    [Type.B, BFactory]
  ])

  static getType(type: Type) {
    return HelloFactory.list.get(type)
  }
}

// test
HelloFactory.getType(Type.A).getHello(Occupation.TEACHER).sayHello()
HelloFactory.getType(Type.A).getHello(Occupation.STUDENT).sayHello()
HelloFactory.getType(Type.B).getHello(Occupation.TEACHER).sayHello()
HelloFactory.getType(Type.B).getHello(Occupation.STUDENT).sayHello()

建造者模式

应用场景

  • 创建时有很多必填参数需要验证
  • 创建时参数求值有先后顺序、相互依赖
  • 创建有很多步骤,全部成功才能创建对象

解决方案

class Programmer {
  age: number
  username: string
  color: string
  area: string

  constructor(p) {
    this.age = p.age
    this.username = p.username
    this.color = p.color
    this.area = p.area
  }

  toString() {
    console.log(this)
  }
}

class Builder {
  age: number
  username: string
  color: string
  area: string

  build() {
    if (this.age && this.username && this.color && this.area) {
      return new Programmer(this)
    } else {
      throw new Error('缺少信息')
    }
  }

  setAge(age: number) {
    if (age > 18 && age < 36) {
      this.age = age
      return this
    } else {
      throw new Error('年龄不合适')
    }
  }

  setUsername(username: string) {
    if (username !== '小明') {
      this.username = username
      return this
    } else {
      throw new Error('小明不合适')
    }
  }

  setColor(color: string) {
    if (color !== 'yellow') {
      this.color = color
      return this
    } else {
      throw new Error('yellow不合适')
    }
  }

  setArea(area: string) {
    this.area = area
    return this
  }
}

// test
const p = new Builder()
  .setAge(20)
  .setUsername('小红')
  .setColor('red')
  .setArea('hz')
  .build()
  .toString()

原型模式

应用场景

  • 原型模式是基于已有的对象克隆数据,而不是修改原型链!
  • 创建对象的代价太大,而同类的不同实例对象属性值基本一致。通过原型克隆的方式节约资源
  • 不可变对象通过浅克隆实现
  • 可变对象通过深克隆实现,深克隆占用资源多
  • 同一对象不同时间版本,可以对比没变化的浅克隆,变化的深克隆,然后新版本替换旧版本。

结构型设计模式

总结了一些类或对象组合在一起的经典结构,这些经典结构可以解决特定应用场景的问题,将类或对象的结构和使用解耦

桥接模式

应用场景

  • 将抽象和实现解耦,让它们可以独立变化
  • 一个类存在多个独立变化的维度,我们通过组合的方式,让多个维度可以独立进行扩展
  • 非常类似于组合优于继承原则

解决方案

enum MsgLevel {
  ERROR,
  WARN,
}

enum MsgType {
  EMAIL,
  PHONE
}

interface MsgContent {
  content()
}

class ErrorMsg implements MsgContent {
  content() {
    return 'ERROR'
  }
}

class WarnMsg implements MsgContent {
  content() {
    return 'WARN'
  }
}

interface MsgSender {
  send()
}

class PhoneSend implements MsgSender {
  msgContent: MsgContent

  constructor(msgContent: MsgContent) {
    this.msgContent = msgContent
  }

  send() {
    console.log(`phone send ${this.msgContent.content()}`)
  }
}

class EmailSend implements MsgSender {
  msgContent: MsgContent

  constructor(msgContent: MsgContent) {
    this.msgContent = msgContent
  }

  send() {
    console.log(`email send ${this.msgContent.content()}`)
  }
}

// test 此处还可以做成map结构继续优化(略)
new PhoneSend(new WarnMsg()).send()
new PhoneSend(new ErrorMsg()).send()
new EmailSend(new WarnMsg()).send()
new EmailSend(new ErrorMsg()).send()

代理模式

应用场景

  • 给原类添加非功能性需求,为了将代码与原业务解耦
  • 业务系统的非功能性需求开发:监控、统计、鉴权、限流、日志、缓存

解决方案

  • 通过继承实现(不推荐)
class User{
  login(){
    console.log('user login...')
  }
}

class UserProxy extends User{
  login() {
    console.log('login before')
    super.login()
    console.log('login after')
  }
}
  • 通过接口实现(推荐)
interface Login {
  login()
}

class User implements Login {
  login() {
    console.log('user login...')
  }
}

class UserProxy implements Login {
  user = new User()

  login() {
    console.log('login before')
    this.user.login()
    console.log('login after')
  }
}

装饰器模式

应用场景

  • 装饰器类是对原始功能的增强
  • 装饰器类和原始类继承同样的父类,这样我们可以对原始类嵌套多个装饰器类
  • 主要解决继承关系过于复杂的问题,通过组合来替代继承
  • 可以通过对原始类嵌套使用多个装饰器

解决方案

  • 通过 AOP 实现
Function.prototype.before = function (beforeFn{
  return (...arg) => {
    beforeFn(...arg);
    return this(...arg);
  }
};
Function.prototype.after = function (afterFn{
  return (...arg) => {
    const result = this(...arg);
    afterFn(...arg);
    return result;
  }
};

function ImportEvent1 {
  console.log('重要的事情说三遍 1')
}

function ImportEvent2 {
  console.log('重要的事情说三遍 2')
}

function ImportEvent3 {
  console.log('重要的事情说三遍 3')
}

// test
ImportEvent2.before(ImportEvent1).after(ImportEvent3)()

适配器模式

应用场景

  • 适配器模式用于补救设计上的缺陷,将不兼容的接口变得兼容
  • 封装有缺陷的接口设计
  • 统一多个类的接口设计
  • 替换依赖的外部系统
  • 兼容老版本接口
  • 适配不同格式的数据

解决方案

  • 原接口方法不多,类适配器和对象适配器都可以
  • 如果原类方法很多,并且和目标接口差异小,用类适配器减少代码量
  • 如果原类方法很多,并且和目标接口差异大,用对象适配器,组合优于继承
// 目标接口格式
interface ITarget {
  f1()

  f2()

  f3()
}

// 原有类与目标接口不兼容
class Origin {
  fa() {
  }

  fb() {
  }

  f3() {
  }
}

// 使用适配器来兼容
class Adaptor implements ITarget {
  origin = new Origin()

  f1() {
    this.origin.fa()
  }

  f2() {
    this.origin.fb()
  }

  f3() {
    this.origin.f3()
  }
}

享元模式

应用场景

  • 共享的单元。复用对象,节省内存,前提是享元对象是不可变对象(初始化之后不再改变)。

解决方案

  • 比如网上象棋游戏有1000个房间,每个房间有1个棋盘,棋盘当前状态(棋子位置)各不相同,但棋子的大小、颜色、名字是相同且固定的,可以设计成享元

组合模式

应用场景

将一组对象组织成树形结构,以表示一种“部分-整体”的层次结构。组合模式让客户端可以统一单个对象和组合对象的处理逻辑(递归遍历)

解决方案

abstract class FileSystemNode {
  path: string

  abstract getFilesCount()

  abstract getFilesSize()
}

class FileNode extends FileSystemNode {
  constructor(path) {
    super();
    this.path = path
  }

  getFilesCount() {
    return 1
  }

  getFilesSize() {
    return require(this.path).length
  }
}

class Directory extends FileSystemNode {
  subNodes = []

  constructor(path) {
    super();
    this.path = path
  }

  getFilesCount() {
    return this.subNodes.reduce(item => item.getCount(), 0)
  }

  getFilesSize() {
    return this.subNodes.reduce(item => item.getSize(), 0)
  }
}

门面(外观)模式

应用场景

  • 将多个后端接口请求合并为一个(冗余接口),提高响应速度,解决性能问题
  • 通过封装细粒度接口,提供组合各个细粒度接口的高层次接口,来提高接口的易用性

行为型设计模式

总结了一些类或对象交互的经典方式,将该行为相关的类或对象解耦

观察者模式

应用场景

  • 将观察者与被观察者解耦
  • 发布订阅模式有发布订阅调度中心(中间商),观察者模式没有!

解决方案

// 目标对象
class Subject {
  observerList: Observer[]

  constructor() {
    this.observerList = [];
  }

  addObserver(observer) {
    this.observerList.push(observer);
  }

  notify() {
    this.observerList.forEach((observer) => {
      observer.update();
    });
  }
}

// 观察者
class Observer {
  cb: Function

  constructor(cb: Function) {
    if (typeof cb === "function") {
      this.cb = cb;
    } else {
      throw new Error("Observer构造器必须传入函数类型!");
    }
  }

  update() {
    this.cb();
  }
}


// test
const observerCallback = function ({
  console.log("我被通知了");
};
const observer = new Observer(observerCallback);
const subject = new Subject();
subject.addObserver(observer);
subject.notify();

模板模式

应用场景

  • 在一个方法里定义一个算法(业务逻辑)骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。
  • 复用 扩展

解决方案

abstract class Drinks {
  firstStep() {
    console.log('烧开水')
  }

  abstract secondStep()

  thirdStep() {
    console.log('倒入杯子')
  }

  abstract fourthStep()

  drink() {
    this.firstStep()
    this.secondStep()
    this.thirdStep()
    this.fourthStep()
  }
}

class Tea extends Drinks {
  secondStep() {
    console.log('浸泡茶叶')
  }

  fourthStep() {
    console.log('加柠檬')
  }
}

class Coffee extends Drinks {
  secondStep() {
    console.log('冲泡咖啡')
  }

  fourthStep() {
    console.log('加糖')
  }
}

// test
const tea = new Tea()
tea.drink()
const coffee = new Coffee()
coffee.drink()

策略模式

应用场景

  • 定义一族算法族,将每个算法分别封装起来,让他们可以相互替换。
  • 避免冗长的 if-else 或 switch 分支判断

解决方案

enum StrategyType {
  S,
  A,
  B
}

const strategyFn = {
  'S'function (salary: number{
    return salary * 4
  },
  'A'function (salary: number{
    return salary * 3
  },
  'B'function (salary: number{
    return salary * 2
  }
}

const calculateBonus = function (level: StrategyType, salary: number{
  return strategyFn[level](salary)
}

calculateBonus(StrategyType.A, 10000// 30000

职责链模式

应用场景

  • 多个处理器 ABC 依次处理同一个请求,形成一个链条,当某个处理器能处理这个请求,就不会继续传递给后续处理器了
  • 过滤器 拦截器 处理器

解决方案

const order500 = function (orderType, pay, stock{
  if (orderType === 1 && pay === true) {
    console.log("500 元定金预购, 得到 100 元优惠券");
    return true;
  } else {
    return false;
  }
};

const order200 = function (orderType, pay, stock{
  if (orderType === 2 && pay === true) {
    console.log("200 元定金预购, 得到 50 元优惠券");
    return true;
  } else {
    return false;
  }
};

const orderCommon = function (orderType, pay, stock{
  if ((orderType === 3 || !pay) && stock > 0) {
    console.log("普通购买, 无优惠券");
    return true;
  } else {
    console.log("库存不够, 无法购买");
    return false;
  }
};

class chain {
  fn: Function
  nextFn: Function

  constructor(fn: Function) {
    this.fn = fn;
    this.nextFn = null;
  }

  setNext(nextFn) {
    this.nextFn = nextFn
  }

  init(...arguments) {
    const result = this.fn(...arguments);
    if (!result && this.nextFn) {
      this.nextFn.init(...arguments);
    }
  }
}

const order500New = new chain(order500);
const order200New = new chain(order200);
const orderCommonNew = new chain(orderCommon);

order500New.setNext(order200New);
order200New.setNext(orderCommonNew);

order500New.init(3true500); // 普通购买, 无优惠券

状态模式

应用场景

  • 将事物内部的每个状态分别封装成类, 内部状态改变会产生不同行为

解决方案

class weakLight {
  light: Light

  constructor(light: Light) {
    this.light = light
  }

  press() {
    console.log('打开强光')
    this.light.setState(this.light.strongLight)
  }
}

class strongLight {
  light: Light

  constructor(light: Light) {
    this.light = light
  }

  press() {
    console.log('关灯')
    this.light.setState(this.light.offLight)
  }
}

class offLight {
  light: Light

  constructor(light: Light) {
    this.light = light
  }

  press() {
    console.log('打开弱光')
    this.light.setState(this.light.weakLight)
  }
}


class Light {
  weakLight: weakLight
  strongLight: strongLight
  offLight: offLight
  currentState: offLight | weakLight | strongLight //当前状态: 默认关灯状态

  constructor() {
    this.weakLight = new weakLight(this)
    this.strongLight = new strongLight(this)
    this.offLight = new offLight(this)
    this.currentState = this.offLight
  }

  press() {
    this.currentState.press()
  }

  setState(state) {
    this.currentState = state
  }
}

// test
const light = new Light()
light.press()
light.press()
light.press()
light.press()
light.press()
light.press()

迭代器模式

应用场景

  • 遍历集合对象

访问者模式

应用场景

  • 允许一个或多个操作应用到一组对象上,解耦操作和对象本身

备忘录模式

应用场景

  • 在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态

解决方案

class Programmer {
  age: number
  username: string
  color: string
  area: string

  constructor(p) {
    this.age = p.age
    this.username = p.username
    this.color = p.color
    this.area = p.area
  }

  // 创建一个快照
  createSnapshot() {
    return {
      age: this.age,
      username: this.username,
      color: this.color,
      area: this.area
    }
  }

  // 通过快照恢复对象状态
  restoreSnapshot(snapshot: Programmer) {
    this.age = snapshot.age
    this.username = snapshot.username
    this.color = snapshot.color
    this.area = snapshot.area
  }
}

命令模式

应用场景

  • 命令模式的主要作用和应用场景,是用来控制命令的执行,比如,异步、延迟、排队执行命令、撤销重做命令、存储命令、给命令记录日志等。将命令的发起者和执行者解耦。

解决方案

interface Command {
  execute()
}

class closeDoorCommand implements Command {
  execute() {
    console.log('close door');
  }
}

class openPcCommand implements Command {
  execute() {
    console.log('open pc');
  }
}

class openQQCommand implements Command {
  execute() {
    console.log('login qq');
  }
}

class CommandManager {
  commandList: Command[] = []

  addCommand(command: Command) {
    this.commandList.push(command)
  }

  execute() {
    this.commandList.forEach(command => {
      command.execute()
    })
  }
}

//test
const commandManager = new CommandManager();
commandManager.addCommand(new closeDoorCommand());
commandManager.addCommand(new openPcCommand());
commandManager.addCommand(new openQQCommand());
commandManager.execute();

解释器模式

应用场景

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

中介模式

应用场景

  • 中介模式的设计思想跟中间层很像,通过引入中介这个中间层,将一组对象之间的交互关系(依赖关系)转换成一对多(星状关系)。原本一个对象要跟n个对象交互,现在只需要跟一个中介对象交互,从而最小化对象间的交互关系,降低了代码复杂度,提高了代码的可读性和可维护性。

如何评价代码的质量?

  • 可读性、可扩展性、可维护性、可复用性、可测试性...
  • 高内聚低耦合
  • 善战者无赫赫之功善医者无煌煌之名,大智若愚大巧若拙,真正的好代码并不是用了多少厉害的技术与奇技淫巧,而是看尽人世繁华后的返璞归真,寥寥几笔实现了功能的同时却没有任何个人风格的痕迹,小白都能看得懂的代码才是好代码。

怎样形成长期记忆?

  • 想办法把零散的知识点串联起来记忆
    • 自顶向下形成金字塔结构记忆
    • 编成关键字口诀记忆
  • 得意忘形
    • 将知识的精华枝干提取出来强化记忆,去粗取精
  • 学而不思则罔,思而不学则殆
    • 深度思考能将他人的知识真正转化成自己的
  • 学而时习之,不亦说乎
    • 第一次学会只是脑海中的短时记忆,需要多次复习强化才能形成长期记忆

注意事项

知识是死的,而代码是活的,不要用固化的设计模式实现硬套在活的业务逻辑里。
能学以致用是我们的学习目标,但是如果写出来的代码同组的其他人都看不懂,更加影响项目的可维护性和开发效率。所以我们可以少用慎用,但是我们必须掌握其思想。
牢牢掌握设计模式,拿去面试、面试别人、组内分享还是可以震慑群雄的,啊哈哈哈

别忘记对我素质三连,点赞、关注、评论



最后


  • 欢迎加我微信,拉你进技术群,长期交流学习...

  • 欢迎关注「前端Q」,认真学前端,做个专业的技术人...

点个在看支持我吧


浏览 36
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报