• 【TypeScript】常见的设计模式


    设计模式就是软件开发过程中形成的套路和经验总结,熟悉设计模式能够在编程过程中更高效有自信,毕竟是前人印证过的最好的设计,同时也能够更好地掌控项目,方便预估开发时间以及对团队成员进行管理。

    通常所提的前端工程化,设计模式才是前端工程化的灵魂。

    一、类型分类

    可以将设计模式分为三种类型,分别为创建型,结构型,和行为型。

    类型说明
    创建型模式主要解决对象创建什么,由谁创建,何时创建的3w问题,对类的实例化进行了抽象,分离概念和实现,使得系统更加符合单一职责原则。
    结构型模式描述如何将类或者对象组合在一起,形成更大的数据结构,因此也可以分为类结构型和对象结构型。
    行为型模型对不同的对象划分责任和算法的抽象,关注类和对象之间的相互作用,同样也分为类和对象。

    二、创建型模式

    创建型模式一共有4个,分别为工厂(工厂,工厂方法,抽象工厂合并),建造者,原型,单例。

    工厂模式

    简而言之,工厂模式就是要 替代掉“new操作符”*

    new执行步骤:

    • 在内存中创建一个新对象
    • 让构造函数的this指针指向这个空对象
    • 执行构造函数内的操作
    • 返回这个新对象

    原因:因为有时候创建实例时需要大量的准备工作,而将这些准备工作 全部放在构造函数 中是非常危险的行为,有必要将 创建实例的逻辑 和 使用实例的逻辑 分开,方便以后扩展。

    class People {
      constructor(des) {
        // 出现异步不能使用async await
        // 函数调用时可能还未完成初始化
        get('someUrl').then(data => {
          this.name = data.name
          get('someUrl?name=' + this.name).then(data => {
            this.age = data.age
          })
        })
        // 非成员函数耦合性变大
        this.des = handleDes(des)
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    使用ts+工厂模式的实现:

    class People {
      name: string = ''
      age: number = 0
      des: string = ''
      constructor(name: string, age: number, des: string) {
        this.name = name
        this.age = age
        this.des = des
      }
    }
    
    async function peopleFactory(description:any){
      const name = await get('someUrl')
      const age = await get('someUrl?name='+name)
      const des = handle(description)
      return new People(name,age,des)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    如此封装,可以使对象的创建和使用更清楚地分离。同时,如果之后的类的定义发生了改变,可以直接修改People,创建类的准备数据发生了改变,则修改工厂函数。

    但是,选择工厂模式的原因是因为构造函数足够复杂或者对象的创建面临巨大的不确定性,只需要传入变量即可构造的情况下,用工厂函数实际上是得不偿失的。

    几乎所有的设计模式都会带来代码可读性下降的风险,因此需要找到代码可读性降低和可维护性,稳定性之间的平衡。

    建造者模式

    用于直接构建复杂对象,将一个对象的初始化和后续步骤分开,通过将初始化和不同的后续步骤结合可以创建出不同的结果。

    export class UserBuilder {
    	private _age : number;
      private _name : string;
      private _email : string;
      constructor(name : string) {
      	this._name = name;
      }
      setAge(age : number): UserBuilder {
      	this._age = age;
        return this;
      }
      get age() {
      	return this._age;
      }
      setEmail(email : string): UserBuilder {
      	this._email = email;
        return this;
      }
      get email() {
      	return this._email;
      }
      build() {
      	return new User(this)
      }
    }
    export class User {
    	private _age : number;
      private _name : string;
      private _email : string;
      constructor(builder: UserBuilder) {
      	this._age = builder.age;
        this._name = builder.name;
        this._email = builder.email;
      }
      get age() {
      	return this._age;
      }
      get name() {
      	return this._name;
      }
      get email() {
      	return this._email;
      }
    }
    
    建造者模式的使用:
    const user : User = new UserBuilder("啦啦啦啦")
    	.setAge(20)
      .setEmail("Scojing1031@163.com")
      .build();
    console.log(user.age + user.email + user.name)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    仅仅三个属性远没有达到复杂对象的程度,只有在对象十分复杂的时候才需要用到建造者模式。

    原型模式

    使用原型模式的话,创建新对象时就是基于一个对象的拷贝,而不是重新实例化一个类。就是指定一个实例对象做原型,然后通过复制这个对象创建一个新对象。

    实现:借助Object.create()方法可以直接指定一个原型,并可以通过参数对原型的属性进行重写或者添加。 注意如果没对原型上的属性进行重写,当原型上的属性变化时会影响继承对象上的属性。

    function peopleConfigPrototype (){
        return {
            name: '',
            age: 0,
            email: ''
        }
    }
    这样每次返回的都是新的对象,也可以相当于是对象的拷贝,但是如果直接拷贝对象,应该如下:
    const peopleConfigPrototype = {
        name: '',
        age: 0,
        email: ''
    }
    const peopleConfig = Object.create(peopleConfigPrototype)
    // 采用Object.create方法,当前对象将被复制到peopleConfig的__proto__上
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    单例模式

    单例模式的目的是限制一个类只能被实例化一次,防止多次实例化(即确保一个class只有一个实例提供给全局访问)。

    class Singleton {
      private static instance: Singleton;
      
      public static getInstance(): Singleton {
      	// 判断是否已经new过1个实例
        if (!Singleton.instance) {
        		// 如果这个唯一的实例不存在,那么就先创建它
          Singleton.instance = new Singleton();
        }
    		// 如果这个唯一的实例已经存在,就直接返回
        return Singleton.instance;
      }
    }
    var s1 = Singleton.getInstance();
    var s2 = Singleton.getInstance();
    
    console.log(s1 === s2);				//true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    三、结构性模式

    结构性模式一共有7种:适配器、桥接、组合、装饰、外观、享元、代理

    装饰模式

    装饰模式是在不改变原有对象的前提下,动态地给一个对象增加一些额外的功能

    直接继承car,添加颜色,一个装饰模式如下:

    class Car {
      name: string
      constructor(name: string) {
        this.name = name
      }
    }
    
    class Benz extends Car {
      color: string
      constructor(name: string, color: string) {
        super(name)
        this.color = color
      }
    }
    
    采用的继承方法是静态的,会导致在继承复用的过程中耦合,比如Car2继承Car,在创建新的子类时错把Car2作为父类,结果容易出错。
    可以采用Ts的装饰器特性来解决这个问题:
    function colorDecorator<T extends { new(...args: any[]): {} }>(color: string) {
        return function (constructor: T) {
            return class extends constructor {
                name = 'shit'
                color = color
            }
        }
    }
    
    @colorDecorator<Car>('red')
    class Car {
        name: string
        constructor(name: string) {
            this.name = name
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    装饰器会拦截Car的构造函数,生成一个继承自Car的新的类,这样更加灵活(但是注意这个过程只发生在构造函数阶段)。

    外观模式

    外观模式就是为子系统中的一系列接口提供一个统一的接口

    为轮胎、方向盘、车这些提供一个统一的接口:

    class Tyre{
        name: string
        constructor(name: string){
            this.name = name
        }
    }
    
    class Steering{
        turnRight(){}
        turnLeft(){}
    }
    
    interface CarConfig{
        tyreName: string
        ifTurnRight: boolean
    }
    
    class Car{
        tyre:Tyre
        steering:Steering
        constructor(carConfig: CarConfig){
            this.tyre = new Tyre(carConfig.name)
            this.steering = new Steering()
            if(carConfig.ifTurnRight){
                this.steering.turnRight
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    代理模式

    代理模式就是为某个对象提供一个代理,并由这个代理对象控制对原对象的访问。封装复杂类。

    比如说car有很多属性,而我们只需要一个简单的版本:

    class Car{
        a: number = 1
        b: number = 2
        c: number = 3
        d: number = 4
        name: string = 'name'
        test(){
            console.log('this is test')
        }
    }
    
    class CarProxy{
        private car: Car
        name: number
        constructor(){
            if(this.car === null){
                this.car = new Car
            }
            this.name = this.car.name
        }
        test(){
            this.car.test()
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    四、行为型模式

    行为型模式主要有5种:命令、中介者、观察者、状态、策略

    观察者模式

    观察者模式是定义对象间的一种「一对多依赖关系」,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式是一种对象行为型模式。

    观察者模式的目的是为了“检测变更”,既然要检测变更,自然需要记录之前的信息。

    典型应用:在 Vue 中,每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。

    class Observer{
        states: string[] = []
        update(state: string){
            this.states.push(state)
        }
    }
    
    class People{
        state: string = ''
        observer: Observer
        
        setState(newState: string){
            if(this.state !== newState){
                this.state = newState
                this.notify(this.state)
            }
        }
        notify(state: string){
            if(this.observer !== null){
                this.observer.update(state)
            }
        }
        setObserver(observer: Observer){
            this.observer = observer
        }
    }
    
    const observer = new Observer()
    const people = new People().serObserver(observer)
    
    people.setState('shit')
    console.log(observer.state)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    可以把观察者模式看成是“记录事件”。实际上前端很多事件处理,就是基于观察者模式的,在上例中的update中的state,就是事件名称,js的事件循环会轮流处理states的状态变化。

    拓展:观察者模式与发布-订阅模式的区别是什么?

    • 发布者直接触及到订阅者的操作,叫观察者模式。
      在这里插入图片描述
    • 发布者不直接触及到订阅者、而是由统一的第三方来完成实际的通信的操作,叫做发布-订阅模式。
      在这里插入图片描述

    状态模式

    状态模式指对象的行为根据状态改变而改变。

    与观察者模式相对,表示的是“记录状态”,只要状态变更,表现即不同,这是设计数据驱动的基础。

    class State{
        tmp: string
        set store(state: string){
            if(this.tmp !== state){
                // do something
                this.tmp = state
            }
        }
        get store(): string{
            return this.tmp
        }
    }
    
    class People{
        state: State
        constructor(state: State){
            this.state = state
        }
    }
    
    const state = new State()
    const people = new People(state)
    
    state.store = 1
    console.log(people.state.store)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    当然,如果一个数据接口既能记录事件,又能记录状态,这就是传说中的响应式数据流,也就是大家平时使用的ReactiveX。

    参考文章:

    ts实现的23种设计模式和原则

    设计模式应该这样学

    js常见的设计模式

    浅析ts设计模式

  • 相关阅读:
    21 世纪什么最贵?那必须得是“人才”啊,一本书带你读懂 TCP-IP 协议
    为什么产品经理都在考PMP?一文透析
    基于 Bresenham 算法画圆
    frp内网穿透—将kali代理在公网中进行渗透测试
    数据库整理
    Golang 自定义时间结构体支持Json&Gorm
    nodejs卸载和安装教程
    c# 容器变换
    python+pytest接口自动化(15)-日志管理模块loguru简介
    【docker】Dockerfile
  • 原文地址:https://blog.csdn.net/m0_47109503/article/details/125868896