解密编程世界的秘籍:设计模式之道

前言

大家好,我是见信,一个喜欢学以致用,喜欢分享的boy,希望大家能看了我的文章能有所获,想要点赞,收藏,关注

为什么要学习设计模式?

开发中,沟通成本是很高的,但如果人人都懂设计模式,不仅能提升可读性降低我们的沟通成本,而且能很好地提升可维护性与可扩展性。

  • 学完这篇文章可以得到什么?

    1. 看完这篇文章之后,相信你能理解各种源码中的设计
    2. 你可以更好的设计自己的项目,函数,模块间的关系等等

下面将以Typescript结合oop的方式,与大家一同理解前端中常见的设计模式。

注意:设计模式不是特定于面向对象,以及语言的,它更多的是一种思想。它就像空气,你看不见摸不到,但是你依赖它。

预备

  • 设计模式不常用,但它们的设计思想很有指导意义
  • ts和oop(面向对象)非常相配
  • 要学设计模式,先学面向对象
  • ts(思维逻辑) + UML(视觉映像) = 设计模式

面向对象

  • OOP - Object Oriented Program
  • 将抽象的编程概念,想象成一个个对象,更好理解
  • 现在依然是主流
  • 类:模板(如:React、Vue的组件模板)
  • 对象:实例(组件实例)

三要素

  1. 封装-高内聚,低耦合
    • public
    • protected class内部,子class内部可访问
    • private class内部可访问
  2. 继承-抽离公共代码,实现代码复用
  3. 多态-更好的扩展性,复写、重载

UML类图

实现关系

  1. class实现interface

    ts 复制代码
    interface Interface1 {
       method1(): void;
     }
    
     interface Interface2 {
       method2(): void;
     }
    
     class MyClass implements Interface1, Interface2 {
       method1(): void {
         // 实现 Interface1 中的 method1 的具体逻辑
       }
    
       method2(): void {
         // 实现 Interface2 中的 method2 的具体逻辑
       }
     }
  2. 类实现抽象类

    ts 复制代码
    abstract class MyAbstractClass {
      abstract method1(): void;
      abstract method2(): void;
    }
    
    class MyClass extends MyAbstractClass {
      method1(): void {
        // 实现 method1 的具体逻辑
      }
    
      method2(): void {
        // 实现 method2 的具体逻辑
      }
    }

泛化关系

  1. class的泛化关系

    ts 复制代码
    class Animal {
      name: string;
    
      constructor(name: string) {
        this.name = name;
      }
    
      eat(): void {
        console.log("Eating");
      }
    }
    
    class Cat extends Animal {
      meow(): void {
        console.log("Meowing");
      }
    }
    
    const cat = new Cat("Kitty");
    cat.eat(); // 输出 "Eating"
    cat.meow(); // 输出 "Meowing"
  2. 接口的泛化关系

    ts 复制代码
    interface Animal {
      name: string;
      eat(): void;
    }
    
    interface Cat extends Animal {
      meow(): void;
    }
    
    const cat: Cat = {
      name: "Kitty",
      eat() {
        console.log("Eating");
      },
      meow() {
        console.log("Meowing");
      }
    };
    
    cat.eat(); // 输出 "Eating"
    cat.meow(); // 输出 "Meowing"

关联关系

雇员,有一张工卡

聚合关系

整体包含部分,部分可以脱离整体而单独存在

ts 复制代码
 class Engine {
  start() {
    console.log("Engine started");
  }
}

class Car {
  engine: Engine;

  constructor(engine: Engine) {
    this.engine = engine;
  }

  start() {
    this.engine.start();
    console.log("Car started");
  }
}

const engine = new Engine();
const car = new Car(engine);
car.start(); // Engine started  Car started

组合关系

整体包含部分,部分不可以脱离整体

ts 复制代码
class Mouth() {
    eat() {
        console.log('吃东西')
    }
}

class Face() {
    mouth: Mouth
    constructor() {
        this.mouth = new Mouth()
    }
}

依赖关系

不是属性关系,而是函数参数或返回值

ts 复制代码
class Driver {
  name: string
  drivingLicence: DrivingLicence
  constructor(name: string) {
    this.name = name
    this.drivingLicence = new DrivingLicence(105)
  }
  drive(car: Car) {
    console.log(`${this.name}驾驶${car.name}`)
  }
  sendDrivingLicence() {
    return this.drivingLicence
  }
}

class Car {
  name: string
  constructor(name: string) {
    this.name = name
  }
}

class DrivingLicence {
  id: number
  constructor(id: number) {
    this.id = id
  }
}

const driver = new Driver('老王')
driver.drive(new Car('奥托')) // 老王驾驶奥托
console.log(driver.sendDrivingLicence().id) // 105

ESM的import,CommonJS的require本身就是依赖关系的一种体现

设计原则

  • SOLID五大设计原则
  • Unix/Linux设计哲学
  • 重点关注开放封闭原则

S单一职责原则

  • 每个程序都做好一件事
  • 功能太多了就要拆分
  • 每个部分保持相互独立

O开放封闭原则

  • 对扩展开放
  • 对修改封闭
  • 需求发生变化时,通过扩展来解决,而非改动

L李氏置换原则

  • 子类能够覆盖父类
  • 父类出现的地方,子类也能出现
  • TS的interface就能很好体现这个设计原则,只要满足了interface,就能使用

I接口隔离原则

  • 保持接口的单一独立
  • 避免出现"胖接口"
  • (和单一职责原则类似),一个接口,一个api只做一个功能

D依赖倒置原则

  • 面向接口编程
  • 而非面向实例

介绍设计模式

经典设计模式有23种之多,但我们只要掌握其中常见的那些设计模式,就可以做到万变不离其宗,看到别的设计模式也能很快掌握。

  1. 创建型

    • 工厂模式
    • 单例模式
    • 原型模式
  2. 结构型

    • 代理模式
    • 装饰器模式
  3. 行为型

    • 观察者模式
    • 迭代器模式
    • 策略模式

工厂模式

如果遇到一个new Class的地方,就要考虑工厂模式

工厂模式解决了什么问题?

  • 创建对象的一种方式。不用每次都亲自创建对象,而是通过一个既定的"工厂"来生产对象。把创建者和class分离,符合开放封闭原则
  • 创建的class来自哪,由工厂本身决定,外部无需知道,外部只需要让工厂知道创建什么
  • 工厂和类分离,解耦
  • 可以扩展多个类(派生类,平行类)
  • 工厂的创建逻辑也可以自由扩展

实现

UML关系:Creator 依赖 Product

简易版本

js 复制代码
/**
 * @name:工厂模式
 * @description: 分离创建者与class
 */

/**
 * 产品
*/
class Product {
  public name: string
  constructor(name: string) {
    this.name = name
  }

  fn1() {
    console.info('执行了 fn1')
  }

  fn2() {
    console.info('执行了 fn2')
  }
}


/**
 * 工厂
*/
class Factory {
  create(name: string): Product {
    return new Product(name)
  }
}

// test
const factory = new Factory()
const hamburger = factory.create('hamburger')
const hotdot = factory.create('hotdog')

console.log(hamburger, hotdot)

标准版本

js 复制代码
/**
 * @name:标准工厂模式
 * @description: 分离创建者与class
 */

interface IProduct {
  name: string
  fn1: () => void
  fn2: ()=> void
}

/**
 * 汉堡
*/
export class Hamburger  implements IProduct {
  public name: string
  constructor(name: string) {
    this.name = name
  }

  fn1() {
    console.info('执行了 fn1')
  }

  fn2() {
    console.info('执行了 fn2')
  }
}

/**
 * 热狗
*/
export class Hotdog implements IProduct {
  public name: string
  constructor(name: string) {
    this.name = name
  }

  fn1() {
    console.info('执行了 fn1')
  }

  fn2() {
    console.info('执行了 fn2')
  }
}

type CreateType = 'hamburger' | 'hotdog'

/**
 * 标准工厂
*/
class Factory {
  private typeMethod: Record<CreateType, () => Hamburger | Hotdog> 
  constructor() {
     this.typeMethod = {
      hamburger: () => new Hamburger('hamburger'),
      hotdog: () => new Hotdog('hotdog')
    }
  }
  create(type: CreateType): IProduct {
    return this.typeMethod[type]()
  }
}

// test
const factory = new Factory()
const hamburger = factory.create('hamburger')
const hotdot = factory.create('hotdog')

应用场景

  • Jquery的$
  • Vue的createElementVNode
  • React的createElementt
  • 日常项目开发中,遇到 new class 的场景,要考虑是否可用工厂模式。

单例模式

  • 前端对单例模式并不常用,但单例的思想随处可见
  • 一个对象/实例 只能被创建一次
  • 创建之后缓存起来,以后继续使用
  • 即,一个系统中只有一个

单例模式解决了什么问题?

一个系统中就只有一个,保证唯一性

实现

class内部使用私有静态属性(类属性)保存单例,通过静态方法(类方法)获取单例

TS版本

js 复制代码
/**
 * @name: 单例模式
 * @description: 一个系统中仅能有一个
 */

class SingleTon {
  name: string
  // 私有化constructor,防止new操作
  private constructor(name: string) {
    this.name = name
  }

  private static instance: SingleTon | null

  static getInstance(name: string): SingleTon {
    if (SingleTon.instance == null) {
      SingleTon.instance = new SingleTon(name)
    }
    return SingleTon.instance
  }
}

const s1 = SingleTon.getInstance('张三') //正确获取单例对象的方式
const s2 = SingleTon.getInstance('李四') 
console.log(s1 === s2) // true
// const s1 = new SingleTon('zhangsan') // 报错,类"SingleTon"的构造函数是私有的,仅可在类声明中访问(ts 2673)

JS版本

js 复制代码
function genGetInstance() {
  let instance

  class SingleTon {

  }

  return () => {
    if (instance == null) {
      instance = new SingleTon()
    }
    return instance
  }
}

const getInstance = genGetInstance()
const s1 = getInstance()
const s2 = getInstance()

console.log(s1 === s2)

借助模块化实现

js 复制代码
// 使用模块化实现 - commonjs ES6 Module

// 假设这是一个文件,getInstance.js
let instance

class SingleTon {

}

export default () => {
  if (instance == null) {
    instance = new SingleTon()
  }
  return instance
}

模块化后,该模块加载到浏览器中的时候,会以函数的形式挂载到全局上面,因此instance实际也是存在于一个函数作用域内

应用场景

  • 登录框,当前用户,购物车,遮罩
  • Vuex,Redux,EventBus

设计原则验证

内部封装getInstance,高内聚,低耦合,分离使用者与class

观察者模式

观察者模式后面又引出了发布订阅模式

观察者模式解决了什么问题?

前端最常见,交互,生命周期的触发

实现

js 复制代码
/**
 * @name: 观察者模式
 * @description: 对某个主题创建观察者,当发生变化的时候,通知观察者
 */

/**
 * @description: 主题
 */
class Subject {
  private state = 0
  private observers: Observer[] = []

  getState(): number {
    return this.state
  }

  setState(newState: number) {
    this.state = newState
    // state变化时,通知所有观察者
    this.notify()
  }

  // 添加 观察者
  attach(observer: Observer) {
    this.observers.push(observer)
  }

  private notify() {
    this.observers.forEach((observer) => {
      observer.update(this.state)
    })
  }
}
/**
 * @description: 观察者
 */
class Observer {
  name: string
  constructor(name: string) {
    this.name = name
  }

  // 当发生变化时,subject调用notify,内部触发各个观察者的update,观察者得到通知
  update(state: number) {
    console.log(`${this.name}接收到了新的变化,当前state变化为了${state}`)
  }
}

const subject = new Subject()

const xiaoming = new Observer('小明')
const xiaohong = new Observer('小红')
const xiaogang = new Observer('小刚')

subject.attach(xiaogang)
subject.attach(xiaohong)
subject.attach(xiaoming)

subject.setState(1)
subject.setState(2)
subject.setState(3)

应用场景

  • DOM事件
  • Vue React组件生命周期
  • Vue的watch
  • Vue的组件更新过程,React不是观察者模式,它是通过setState主动触发的
  • 各种异步回调,如:setTimeoutPromise.then
  • MutationObserver,监听DOM节点,DOM树变化的
  • NodeJS的stream

设计原则验证

  • Observer 和 Subject 分离、解耦
  • Subject 可以自由扩展
  • Observer 可以自由扩展

发布订阅模式

  • 观察者模式是被动的,无法主动触发。
  • 发布订阅模式,是可以主动触发的。
  • 是观察者模式的另一种实现模式。

区别

  • 观察者模式:Subject 和 Observer 直接绑定的,中间无媒介
  • 发布订阅模式:Publisher 和 Subscriber 互不相识,中间有媒介
  • 看是否需要手动触发emit

应用场景

  • 自定义事件,Vue2本身就是一个EventBus,Vue3不再自带EventBus功能,推荐使用mitt
  • postMessage通讯
  • webWorker通讯
  • webSocket通讯

迭代器模式

  • for循环不是迭代器
  • 迭代器是用来解决for循环的问题的

迭代器模式解决了什么问题?

  • for循环的触发,需要知道数组长度,需要知道如何获取元素
  • forEach VS for循环,不需要数据的长度,不需要知道元素的结构,不需要知道数据的结构,forEach是一个简易的迭代器

迭代器解决了如何更加方便、简易地遍历一个有序的数据集合的问题

  • 顺序访问有序结构(如:数组、NodeList)
  • 不知道数据的长度、内部结构
  • 高内聚、低耦合

js中有序的数据结构

  • 数组
  • 字符串
  • NodeList等DOM集合
  • Map
  • Set
  • arguments 类数组

注意:Object是无序结构

实现

UML类图关系:依赖关系

js 复制代码
/**
 * @name: 迭代器模式
 */

class DataIterator {
  private data: number[]
  private index = 0
  constructor(container: DataContainer) {
    this.data = container.data
  }

  next(): number | null {
    const index = this.index
    this.index++
    return this.data[index]
  }

  hasNext(): boolean {
    if (this.index >= this.data.length) return false
    return true
  }

}

class DataContainer {
  data: number[] = [10, 20, 30, 40, 50]
  
  // 获取迭代器
  getIterator() {
    return new DataIterator(this)
  }
}

const dataContainer = new DataContainer()

const iterator = dataContainer.getIterator()

while (iterator.hasNext()) {
  const num = iterator.next()
  console.log(num)
}

应用场景

  • Symbol.iterator。所有的有序数据结构,都内置了Symbol.iterator这个key,使用它可以获得该数据结构的迭代器
  • 自定义迭代器
  • 用于for of,只要内置了Symbol.iterator这个key,都可以使用for of来进行遍历
  • 用于数组的解构、扩展操作符、Array.from
  • 用于Promise.allPromise.race
  • 用于生成器yield*

设计原则验证

  • 使用者和目标分离,解耦
  • 目标能自行控制其内部逻辑
  • 使用者不关心目标的内部结构

Generator 生成器

  • 基本使用
  • yield*语法
  • yield遍历DOM树

基本使用 + 语法

js 复制代码
/**
 * @name: 生成器
 * @description: 这个文件里讲的并非设计模式,而是学习完'迭代器模式'后,对生成器的一个理解 
 */

//#region 使用 yield 生成迭代器 ---------------------------------------- 

function* genNums() {
  yield 10
  yield 20
  yield 30
}

// 生成器的本质就是返回一个迭代器
const numsIterator = genNums()

// 所以我们可以通过迭代器的方式去应用
console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator)

// 也可以通过for of去使用
for (const num of numsIterator) {
  console.log(num)
}

// 也可以使用扩展操作符
console.log([...numsIterator])

//#endregion

//#region 使用 yield* 生成迭代器-----------------------------------

function* genNums2() {
  // yield* 后面跟的需要是一个有序结构,这个有序结构本身已经实现了[Symbol.Iterator]
  yield* [11, 21, 31]
}

const numsIterator2 = genNums2()

// 也可以通过for of去使用
for (const num of numsIterator2) {
  console.log(num)
}

// 也可以使用扩展操作符
console.log([...numsIterator2])

console.log(numsIterator2.next())
console.log(numsIterator2.next())
console.log(numsIterator2.next())
console.log(numsIterator2.next())
console.log(numsIterator2)
//#endregion

使用generator + yield 遍历DOM树

js 复制代码
function* traverse(elemList: Array<Element>): any {
  for (const elem of elemList) {
    yield elem
    
    const children = Array.from(elem.children)
    if (children.length) {
      yield* traverse(children)
    }
  }
}

原型模式

  • 原型模式不常用,但原型链是JS基础,必须掌握
  • 属性描述符日常不会直接使用,但它是理解对象属性的重要基础

原型模式解决了什么问题?

原型模式解决了快速克隆的问题。

实现

js 复制代码
/**
 * @name: 原型模式
 * @description: 快速克隆自己
 */

class CloneDemo {
  name = 'clone demo'

  clone(): CloneDemo {
    return new CloneDemo()
  }
}

JS原型的基础知识 prototype__proto__

  • 函数、class都有显示原型 prototype
  • 对象都有隐式原型__proto__
  • 对象__proto__指向其构造函数的prototype

应用场景

最符合原型模式的应用场景就是Object.create,它可以指定原型

装饰器模式

  • 装饰器本身就是一个函数,它可以针对对象、class、函数
  • 动态的添加新功能
  • 但不改变它原有的功能

装饰器模式解决了什么问题?

装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。

实现

js实现

js 复制代码
/**
 * @name: 装饰器模式
 */

const phone: Record<string, any> = {
  name: 'phone1',
  fn1() {
    console.log('fn1')
  }
}

function decorator(phone: Record<string, any>) {
  phone.fn2 = () => {
    console.log('fn2')
  }
}

decorator(phone)

phone.fn1()
phone.fn2()

class实现

UML关系:关联

js 复制代码
/**
 * @name: 装饰器模式(面向对象)
 * @description: 为circle添加一个装饰器
 */

class Circle {
  draw() {
    console.log('画一个圆形')
  }
}

class Decorator {
    private circle: Circle
    constructor(circle: Circle) {
        this.circle = circle
    }
    draw() {
        this.circle.draw() // 原有功能
        this.setBorder() // 装饰
    }
    private setBorder() {
        console.log('设置边框颜色')
    }
}

const circle = new Circle()
circle.draw()

const de = new Decorator(circle)
de.draw()

装饰 class

js 复制代码
/**
 * @name: 装饰Class
 * @description: 使用ts的装饰器语法糖装饰class
 */

function testable(target: any) {
  target.isTestable = false
}

@testable
class Foo {
  static isTestable?: boolean
}

console.log(Foo.isTestable) // false

接收参数的装饰器

js 复制代码
/**
 * @name: 装饰Class
 * @description: 使用ts的装饰器语法糖装饰class
 */

function testable(val: boolean) {
  return function (target: any) {
    target.isTestable = val
  }
}


@testable(true)
class Foo {
  static isTestable?: boolean
}

console.log(Foo.isTestable) // true

装饰 class中的method

js 复制代码
function testable(val: boolean) {
  return function (target: any) {
    target.isTestable = val
  }
}

// 添加一个头衔
function honor(target: any , key: string, descriptor: PropertyDescriptor) {

  console.log(target === Foo.prototype)

  const oldValue = descriptor.value

  descriptor.value = function () {
    // 先打印头衔
    console.log('添加头衔 大大王')
    // eslint-disable-next-line prefer-rest-params
    return oldValue.apply(this, arguments)
  } 
}


@testable(true)
class Foo {
  private name = '张三'
  private age = 20
  static isTestable?: boolean
  constructor() {
    console.log(1)
  }

  @honor
  getName() {
    return this.name
  }

  getAge() {
    return this.age
  }
}

console.log(Foo.isTestable) // true
const foo = new Foo()
console.log(foo.getName())

设计原则验证

  • 装饰器和目标分离,解耦
  • 装饰器可以自由扩展
  • 目标也可以自由扩展
  • 高内聚,低耦合

代理模式

  • 针对一个对象
  • 设置代理,控制对这个对象的访问
  • 为其他对象提供一种代理以控制对这个对象的访问。在直接访问对象时带来的问题

代理模式解决了什么问题?

凡事不经自己手,转由他人代劳

与装饰器模式的区别

装饰器模式不允许改变原有行为,而代理模式可以改变原有行为

实现

UML类图关系: 关联。

class实现

js 复制代码
/**
 * @name:代理模式
 * @description:
 */

class RealImg {
  fileName: string
  
  constructor(fileName: string) {
    this.fileName = fileName
  }

  display() {
    this.loadFromDist()
    console.log('display...', this.fileName)
  }

  private loadFromDist() {
    console.log('loading...', this.fileName)
  }

}


class ProxyImg {
  realImg: RealImg

  constructor(fileName: string) {
    this.realImg = new RealImg(fileName)
  }

  display() {
    // 这里还可以加很多限制
    this.realImg.display()
  }
}

const proxyImg = new ProxyImg('ljx.jpg')

proxyImg.display()

ES6 Proxy

js 复制代码
/**
 * @name: ES6的Proxy使用 
 */

const star = {
  name: '张三',
  age: 25,
  phone: '18454653654',
  price: 0 // 明星不谈钱
}

const agent = new Proxy(star, {
  get(target, key) {
    if (key === 'phone') {
      return '经纪人电话' + 18154657811
    }
    if (key === 'price') {
      return 100 * 1000 //报价
    }
    // 使用 '反射' 操作target
    return Reflect.get(target, key)
  },

  set(target, key, val): boolean {
    if (key === 'price') {
      if (val < 100 * 1000) {
        throw new Error('价格太低了')
      } else {
        console.log('报价成功,合作愉快!')
        return Reflect.set(target, key, val)
      }
    }
    // 其他属性不可设置
    return false
  }
})

console.log(agent.name) // 张三
console.log(agent.phone) // 经纪人电话18154657811
console.log(agent.age) // 25
console.log(agent.price) // 100000

agent.price = 100 *  10000

console.log(agent.price) // 100000
ES6 Proxy使用场景
  • 跟踪属性访问------vue3 数据响应式 Proxy
  • 隐藏属性
  • 验证属性
  • 记录实例
Proxy 可能遇到的坑
  • 捕获其不变式------不允许通过代理进行会对原有的属性描述符产生改变的行为,如:将属性由number转为string
  • 关于this,对一个对象进行了Proxy行为,那么this指向new proxy生成的对象,this只有执行的时候才知道是什么

应用场景

  • DOM事件代理(事件委托,冒泡给顶层处理)
    • 事件绑定到父容器上,而非目标节点
    • 适合目标较多或数量不确定(如无限加载的瀑布流图片列表)
  • Webpack DevServer proxy 正向代理(客户端代理)
  • Nginx 反向代理(服务器代理)

设计原则验证

  • 代理和目标分离,解耦
  • 代理可以自行扩展
  • 目标也可以自行扩展

职责链模式

  • 一个流程,需要多个步骤来处理
  • 把多个步骤分开,通过一个"链"串联起来
  • 各个步骤相互分离,互不干扰

前端最常见的就是链式操作

场景

jQuery 链式操作

js 复制代码
$('#div1')
    .show()
    .css('color', 'red')
    .append($('#p1'))

Promise的链式操作

js 复制代码
// 加载图片
function loadImg(src: string) { 
    const promise = new Promise((resolve, reject) => {
        const img = document.createElement('img')
        img.onload = () => { 
            resolve(img)
        }
        img.onerror = () => { 
            reject('图片加载失败')
        }
        img.src = src
    })
    return promise
}

const src = 'https://www.imooc.com/static/img/index/logo_new.png'

const result = loadImg(src)
result.then((img: HTMLImageElement) => {
    console.log('img.width', img.width)
    return img
}).then((img: HTMLImageElement) => {
    console.log('img.height', img.height)
}).catch((err) => {
    console.log(err)
})

策略模式

  • 针对多个条件分支
  • 不用很多if...elseswitch...case
  • 将每个分支单独处理,相互隔离

场景

js 复制代码
class User {
    private type: string
    constructor(type: string) {
        this.type = type
    }
    buy() {
        const { type } = this
        if (type === 'ordinary') {
            console.log('普通用户购买')
        }
        if (type === 'member') {
            console.log('会员购买')
        }
        if (type === 'vip') {
            console.log('VIP 用户购买')
        }
    }
}

使用策略模式

js 复制代码
interface IUser {
    buy: () => void
}

class OrdinaryUser implements IUser {
    buy() {
        console.log('普通用户购买')
    }
}

class MemberUser implements IUser {
    buy() {
        console.log('会员购买')
    }
}

class VipUser implements IUser {
    buy() {
        console.log('VIP 用户购买')
    }
}

const u1 = new OrdinaryUser()
u1.buy()
const u2 = new MemberUser()
u2.buy()
const u3 = new VipUser()
u3.buy()

适配器模式

  • 我们需要使用某种特定格式、类型的数据
  • 而我们获取到的原始数据,并非这种格式
  • 因此我们需要转换,如vue的computed

场景

js 复制代码
// 电源插口
class Source {
    supply() {
        return '220V 电源'
    }
}

// 适配器
class Adapter {
    source = new Source()
    adaptedSupply() {
        const sourceRes = this.source.supply()
        return `${sourceRes} --> 12V 电源`
    }
}

// 手机使用
const adapter = new Adapter()
const res = adapter.adaptedSupply()
console.log(res)

vue的computed本身就是对适配器模式的一种实践

js 复制代码
// Vue 组件配置
{
    data() {
        return {
            userList: [
                { id: 1, name: '张三' },
                { id: 2, name: '李四' },
                { id: 3, name: '王五' },
            ]
        }
    },
    computed: {
        userNameList() {
            this.userList.map(user => user.name) // ['张三', '李四', ... ]
        }
    }
}

MVC 和 MVVM

  • MVC 和 MVVM 不属于经典的 23 种设计模式,但也可以说他们是设计模式。
  • 本来设计模式就是一种抽象的定义,而且随着时代的发展,它也需要慢慢的改变。
  • 如何称呼无所谓,关键是理解它们的内容

MVC原理

  • View 传送指令到 Controller
  • Controller 完成业务逻辑后,要求 Model 改变状态
  • Model 将新的数据发送到 View,用户得到反馈

MVVM原理

  • View 即 Vue template
  • Model 即 Vue data
  • VM 即 Vue 其他核心功能,负责 View 和 Model 通讯,即各种指令,如v-bind,v-model

如何设计?

  • 先详细审查需求,抓住细节和重点
  • 分析
    • 抽象数据模型,梳理关系,定义属性和方法。我们应该定义多少个class,这些class有哪些属性和方法,class之间该如何关联?
    • 梳理流程
  • 最后画出UML类图,写代码
  • 切忌一开始就写代码
  • 关注整体设计,别计较细节,把UML类图看的重一些

真题模拟

打车模拟

  1. 打车时你可以打快车和专车
  2. 无论什么车,都有车牌号和车辆名称
  3. 价格不同,快车每公里1元,专车每公里2元
  4. 打车时,你要启动行程并显示车辆信息
  5. 结束行程时,显示价格(假定行驶了5公里)

分析数据模型

注意:行程是一个单独的class

实现

js 复制代码
// 抽象类,必须被子类实现,不能直接new Car()
abstract class Car {
    name: string
    number: string
    // 抽象属性  
    abstract price: number
    constructor(name: string, number: string) {
        this.name = name
        this.number = number
    }
}

/**
 * @description: 快车
 */
class ExpressCar extends Car {
    price = 1
    constructor(name: string, number: string) {
        super(name, number)
    }
}

/**
 * @description: 专车
 */
class SpecialCar extends Car {
    price = 2
    constructor(name: string, number: string) {
        super(name, number)
    }
}

/**
 * @description: 行程
 */
class Trip {
    car: Car // 直接指定未Car类,可以兼容所有的子类(依赖倒置原则)
    constructor(car: Car) {
        this.car = car
    }
    start() {
        console.log(`行程开始,名称: ${this.car.name}, 车牌号: ${this.car.number}`)
    }
    end() {
        console.log('行程结束,价格: ' + (this.car.price * 5))
    }
}

// const car = new ExpressCar('桑塔纳', 'A111222')
const car = new SpecialCar('迈腾', 'B333444')
const trip = new Trip(car)
trip.start()
trip.end()

停车场模拟

  • 某停车场,分3层,每层100车位
  • 每个车位可以监控车辆的进入和离开
  • 车辆进入前,显示每层的空余车位数量
  • 车辆进入时,摄像头可以识别车牌号和时间
  • 车辆出来时,出口显示器显示车牌号和停车时长

分析数据模型

注意:根据给定条件完成设计,不要自己假定条件增加难度

实现

js 复制代码
/**
 * @name:停车场模拟
 */

// 车
export class Car {
    number: string
    constructor(number: string) {
        this.number = number
    }
}

// 停车信息
interface IEntryInfo {
    number: string
    inTime: number
    place?: ParkPlace
}

// 入口摄像头
class ParkCamera {
    // 拍照
    shot(car: Car): IEntryInfo {
        return {
            number: car.number,
            inTime: Date.now()
        }
    }
}

// 出口显示器
class ParkScreen {
    show(info: IEntryInfo) {
        const { inTime, number } = info
        const duration = Date.now() - inTime
        console.log(`车牌号:${number} ,停留时间:${duration}`)
    }
}

// 车位
class ParkPlace {
    isEmpty = true
    getInto() {
        this.isEmpty = false
    }
    out() {
        this.isEmpty = true
    }
}

// 层
class ParkFloor {
    index: number
    parkPlaces: ParkPlace[]
    constructor(index: number, places: ParkPlace[]) {
        this.index = index
        this.parkPlaces = places
    }
    get emptyPlaceNum(): number {
        let num = 0
        for (const place of this.parkPlaces) {
            if (place.isEmpty) num++
        }
        return num
    }
}

// 停车场
class Park {
    parkFloors: ParkFloor[]
    parkCamera = new ParkCamera()
    parkScreen = new ParkScreen()
    entryInfoList: Map<string, IEntryInfo> = new Map() // key 是 car.number

    constructor(floors: ParkFloor[]) {
        this.parkFloors = floors
    }

    getInto(car: Car) {
        // 获取摄像头的信息:车牌号,时间
        const entryInfo = this.parkCamera.shot(car)
        // 某个车位
        const i = Math.round((Math.random() * 100) % 100)
        const place = this.parkFloors[0].parkPlaces[i] // 停在第一层的某个车位(想要第二层,第三层,也可以用随机数获取)
        // 进入车位
        place.getInto()
        // 记录停车信息
        entryInfo.place = place
        this.entryInfoList.set(car.number, entryInfo)
    }

    out(car: Car) {
        // 获取停车信息
        const entryInfo = this.entryInfoList.get(car.number)
        if (entryInfo == null) return
        const { place } = entryInfo
        if (place == null) return

        // 从车位离开
        place.out()

        // 出口显示屏,显示
        this.parkScreen.show(entryInfo)

        // 删除停车信息
        this.entryInfoList.delete(car.number)
    }

    // 当前停车场的空余车位
    get emptyInfo(): string {
        return this.parkFloors.map(floor => {
            return `${floor.index} 层还有 ${floor.emptyPlaceNum} 个车位`
        }).join('\n')
    }
}

// ---------- 初始化停车场 ----------
const floors: ParkFloor[] = []
// 3 层
for (let i = 0; i < 3; i++) {
    const places: ParkPlace[] = []
    // 每层 100 个车位
    for (let j = 0; j < 100; j++) {
        places[j] = new ParkPlace()
    }
    floors[i] = new ParkFloor(i + 1, places)
}
const park = new Park(floors)

// ---------- 模拟车辆进入、离开 ----------
const car1 = new Car('A1')
const car2 = new Car('A2')
const car3 = new Car('A3')

console.log('第一辆车即将进入')
console.log(park.emptyInfo)
park.getInto(car1)



console.log('第二辆车即将进入')
console.log(park.emptyInfo)
park.getInto(car2)



console.log('第三辆车即将进入')
console.log(park.emptyInfo)
park.getInto(car3)

console.log('第一辆车离开')
park.out(car1)

console.log('第二辆车离开')
park.out(car2)

console.log('第三辆车离开')
park.out(car3)

console.log(park.emptyInfo)

vue中的常见API用了什么设计模式?

前面已经说过,Vue种的数据响应式本身是基于代理模式观察者模式 的,computed在此基础之上,还有转换器模式 ,当观察到某个响应式数据变化时,输出特定格式。又比如v-for本身是基于迭代器模式,后面我们再看一看别的一些api。

v-model

  1. 观察者模式:当数据属性发生变化时,Vue会通过观察者模式通知相关的观察者,从而更新关联的输入元素的值。同时,当用户在输入元素中修改值时,通过观察者模式,Vue会自动更新数据属性的值。
  2. 适配器模式:v-model 指令将数据属性和用户界面中的输入元素绑定在一起,起到了适配器的作用。它将输入元素的值转化为数据属性的值,并将数据属性的改变反映到输入元素,实现了输入和数据之间的双向转换。
  3. 策略模式:v-model 指令根据不同的输入元素类型(如 inputselecttextarea 等)选择不同的策略来处理双向数据绑定。不同类型的输入元素可能有不同的值的获取方式和事件监听机制,v-model 根据类型选择合适的策略来实现数据的同步

v-if

  1. 策略模式
  2. 观察者模式:Vue使用了观察者模式来实现数据的响应式。在 v-if 指令中,当条件发生改变时,Vue会通过观察者模式通知相关的观察者,从而更新 DOM 中对应的元素。
  3. 组合模式:Vue中的模板语法支持将多个 v-if 同时应用在一个元素上,这样就可以根据多个条件的组合来判断是否渲染该元素。这种组合的方式可以看作是组合模式的应用。
  4. 模板方法模式:Vue的编译器在解析模板时,会根据 v-if 指令的条件来生成相应的渲染函数。这个过程中使用了模板方法模式,即定义一个模板方法,然后在不同的条件下使用不同的具体实现。
  5. 外观模式:v-if 可以隐藏或显示元素,这种隐藏和显示的操作可以看作是对元素的外观进行操作。v-if 提供了一个简单的语法来封装复杂的条件判断逻辑,使得操作元素的外观变得简单和易于理解。

总结

学习设计模式不仅仅是学习一些具体的代码模板,更重要的是培养一种思考和解决问题的方法。它帮助我们构建可维护、可扩展和高质量的软件,同时提高团队的协作效率。

往期文章

JS年度毒打!

react笔记

复习一遍vue

相关推荐
甜兒.36 分钟前
鸿蒙小技巧
前端·华为·typescript·harmonyos
未来可期LJ36 分钟前
【C++ 设计模式】单例模式的两种懒汉式和饿汉式
c++·单例模式·设计模式
王中阳Go4 小时前
字节跳动的微服务独家面经
微服务·面试·golang
Jiaberrr4 小时前
前端实战:使用JS和Canvas实现运算图形验证码(uniapp、微信小程序同样可用)
前端·javascript·vue.js·微信小程序·uni-app
everyStudy4 小时前
JS中判断字符串中是否包含指定字符
开发语言·前端·javascript
城南云小白4 小时前
web基础+http协议+httpd详细配置
前端·网络协议·http
前端小趴菜、4 小时前
Web Worker 简单使用
前端
web_learning_3215 小时前
信息收集常用指令
前端·搜索引擎
tabzzz5 小时前
Webpack 概念速通:从入门到掌握构建工具的精髓
前端·webpack
200不是二百5 小时前
Vuex详解
前端·javascript·vue.js