JavaScript设计模式 | 青训营笔记

96 阅读6分钟

这是我参与「第四届青训营 」笔记创作活动的的第9天,总结前端中常见的设计模式。

设计模式 overview

每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动。 —— Christopher Alexander

SOLID设计原则

"SOLID" 是由罗伯特·C·马丁在 21 世纪早期引入的记忆术首字母缩略字,指代了面向对象编程和面向对象设计的五个基本原则。

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

工厂模式

在 JavaScript 中,我们使用构造函数去初始化对象,就是应用了构造器模式
构造器就是将一些属性赋值给对象的过程封装,确保每个对象都具有这些属性,保证共性不变,同时可以对这些属性的取值操作开放,保证了个性的灵活。

简单工厂

function User(name , age, career, work) {
    this.name = name
    this.age = age
    this.career = career 
    this.work = work
}

function Factory(name, age, career) {
    let work
    switch(career) {
        case 'coder':
            work =  ['写代码','写系分', '修Bug'] 
            break
        case 'product manager':
            work = ['订会议室', '写PRD', '催更']
            break
        case 'boss':
            work = ['喝茶', '看报', '见客户']
        case 'xxx':
            // 其它工种的职责分配
            ...
            
    return new User(name, age, career, work)
}

把这个承载了共性的 User 类和个性化的逻辑判断写入同一个函数:这样对于有新添加的不同职位就直接改 Factory 而不用去修改 User 类了。
什么是工厂模式?:工厂模式其实就是将创建对象的过程单独封装
工厂模式的目的: 无脑传参

抽象工厂

对于上面的简单工厂的示例存在这样的问题,如果每一次去在 Factory 中添加,就会导致 Factory 变得会异常庞大。主要原因是我们没有遵守开放封闭原则

开放封闭原则:对拓展开放,对修改封闭。说得更准确点,软件实体(类、模块、函数)可以扩展,但是不可修改。所以,我们并没有真正在做添加,而是一直疯狂地修改。

通常抽象工厂一共包括四个部分:抽象工厂具体工厂抽象产品具体产品

抽象说明并不确定具体要实现的功能,只知道一定会包括这些方法,并且这些方法必须进行重载。

class MobilePhoneFactory {
    // 提供操作系统的接口
    createOS(){
        throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!");
    }
    // 提供硬件的接口
    createHardWare(){
        throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!");
    }
}

抽象工厂不干活,具体工厂(ConcreteFactory)来干活!

// 具体工厂继承自抽象工厂
class FakeStarFactory extends MobilePhoneFactory {
    createOS() {
        // 提供安卓系统实例
        return new AndroidOS()
    }
    createHardWare() {
        // 提供高通硬件实例
        return new QualcommHardWare()
    }
}

因此不管我们后续需要如何修改都不用改变 MobilePhoneFactory,需要新建继承 MobilePhoneFactory就可以了。

因此我们可以用一个抽象产品(AbstractProduct)类来声明这一类产品应该具有的基本功能。

// 定义操作系统这类产品的抽象产品类
class OS {
    controlHardWare() {
        throw new Error('抽象产品方法不允许直接调用,你需要将我重写!');
    }
}

// 定义具体操作系统的具体产品类
class AndroidOS extends OS {
    controlHardWare() {
        console.log('我会用安卓的方式去操作硬件')
    }
}

class AppleOS extends OS {
    controlHardWare() {
        console.log('我会用🍎的方式去操作硬件')
    }
}
// 定义手机硬件这类产品的抽象产品类
class HardWare {
    // 手机硬件的共性方法,这里提取了“根据命令运转”这个共性
    operateByOrder() {
        throw new Error('抽象产品方法不允许直接调用,你需要将我重写!');
    }
}

// 定义具体硬件的具体产品类
class QualcommHardWare extends HardWare {
    operateByOrder() {
        console.log('我会用高通的方式去运转')
    }
}

class MiWare extends HardWare {
    operateByOrder() {
        console.log('我会用小米的方式去运转')
    }
}
...

因此,如果我们需要生产一台手机就可以如下操作

// 这是我的手机
const myPhone = new FakeStarFactory()
// 让它拥有操作系统
const myOS = myPhone.createOS()
// 让它拥有硬件
const myHardWare = myPhone.createHardWare()
// 启动操作系统(输出‘我会用安卓的方式去操作硬件’)
myOS.controlHardWare()
// 唤醒硬件(输出‘我会用高通的方式去运转’)
myHardWare.operateByOrder()

单例模式

单例的意思是一个类只有一个实例,并且提供一个访问它的全局访问点。
要实现单例模式,那么就要求构造函数具备判断自己是否已经创建过一个实例的能力。

静态方法实现

class SingleDog {
    show() {
        console.log('我是一个单例对象')
    }
    static getInstance() {
        // 判断是否已经new过1个实例
        if (!SingleDog.instance) {
            // 若这个唯一的实例不存在,那么先创建它
            SingleDog.instance = new SingleDog()
        }
        // 如果这个唯一的实例已经存在,则直接返回
        return SingleDog.instance
    }
}

const s1 = SingleDog.getInstance()
const s2 = SingleDog.getInstance()

// true
s1 === s2

闭包实现

SingleDog.getInstance = (function() {
    // 定义自由变量instance,模拟私有变量
    let instance = null
    return function() {
        // 判断自由变量是否为null
        if(!instance) {
            // 如果为null则new出唯一实例
            instance = new SingleDog()
        }
        return instance
    }
})()

原型模式

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

观察者模式

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

  • 被观察者拥有所有观察者的完整数组
  • 事件发布时便利这个完整的列表通知每一个观察者
class Student{
    constructor(name){
        this.name = name;
    }
    notice(msg){
        console.log(`我${this.name},接收到了这条消息`);
    }
}

class Teacher{
    constructor(){
        this.observer = []; // 记录所有的观察者
    }
    addObeserver(ob){
        this.observer.push(ob);
        return this;
    }
    sendMsg(msg){
        this.observer.forEach(ele=>ele.notice(msg));
    }
}
// 使用
const MissLiu = new Teacher();
const Zhangsan = new Student('zhangSan');
const LiSi = new Student('LiSi');
const WangWu = new Student('WangWu');
MissLiu.addObeserver(Zhangsan).addObeserver(LiSi).addObeserver(WangWu);
MissLiu.sendMsg('歪比歪比');

发布订阅者模式

  • 订阅者订阅对应事件
  • 事件发布时所有对应消息订阅回调全部执行
class Manager{
    // 所有消息的消息池
    messages = {}

    // 订阅函数
    on(msgName, callbck){
        if(this.messages[msgName]){
            // 如果已经有人订阅
            this.messages[msgName].push(callbck);
            console.log(this.messages)
        }else{
            // 如果暂时没有
            this.messages[msgName] = [callbck];
            console.log(this.messages)
        }
    }

    // 发布函数
    emit(msgName,...param){
        if(this.messages[msgName]){
            this.messages[msgName]
            .forEach(callback => callback.call(this,...param))
        }
    }
}
// 使用
const manager = new Manager();
manager.on('click',(msg)=>{
    console.log(`event1:接收到事件${msg}`);
})
manager.on('click',(msg)=>{
    console.log(`event2:接收到事件${msg}`);
})
manager.on('click',(msg)=>{
    console.log(`event3:接收到事件${msg}`);
})
setTimeout(()=>{
    manager.emit('click');
},2000)