😶‍🌫️前端开发中应该了解的设计模式分享

219 阅读7分钟

一、单例模式

概念总结:保证一个类仅有一个实例,并提供一个访问它的全局访问点

class Singleton {
    // 私有构造函数
    constructor() {
        // 实例为空时,新建实例
        if (!Singleton.instance) {
            this.name = 'Singleton';
        }
    }

    // 公有获取实例方法
    static getInstance() {
        // 若实例为空,新建实例
        if (!Singleton.instance) {
            Singleton.instance = new Singleton();
        }
        // 返回实例
        return Singleton.instance;
    }

    // 实例方法
    showName() {
        console.log(this.name); // Singleton
    }
}

// 获取实例
let instance1 = Singleton.getInstance();
// 获取实例
let instance2 = Singleton.getInstance();

// 打印实例1和实例2
console.log(instance1 === instance2); // true

// 调用实例方法
instance1.showName(); // Singleton

上述代码实现了单例模式的类写法,关键在于

  1. 私有构造函数,限制外部直接 new
  2. 公有静态方法 getInstance 获取实例
  3. 在 getInstance 方法中判断实例是否存在,不存在则新建
  4. getInstance 每次都返回同一个实例
  5. 这样确保了整个程序中只有一个实例存在

二、观察者模式

概念总结:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新

// 发布者类
class Publisher {
    constructor() {
        // 存储订阅者列表
        this.observers = [];
    }

    // 添加订阅者
    add(observer) {
        // 将订阅者添加到列表
        this.observers.push(observer);
    }

    // 删除订阅者
    remove(observer) {
        // 从订阅者列表中删除订阅者
        this.observers = this.observers.filter((o) => o !== observer);
    }

    // 通知订阅者
    notify() {
        // 遍历订阅者列表,调用每个订阅者的update方法
        this.observers.forEach((o) => o.update());
    }
}

// 订阅者类
class Observer {
    // 构造函数接收订阅者名称
    constructor(name) {
        this.name = name;
    }

    // 取消订阅
    unsubscribe() {
        // 从发布者的订阅者列表中删除自己
        pub.remove(this);
    }

    // 更新方法,订阅发布者通知后被调用
    update() {
        console.log(`已通知:${this.name} 告退!`);
    }
}

// 新的发布者实例
const pub = new Publisher();

// 两个订阅者实例
const obs1 = new Observer('订阅者一号:obs1');
const obs2 = new Observer('订阅者二号:codewhite');

// 将两个订阅者添加到发布者
pub.add(obs1);
pub.add(obs2);

// 删除订阅者obs1
obs1.unsubscribe();

// 发布者通知订阅者
pub.notify(); // 已通知:订阅者二号:codewhite 告退!

我添加了详细注释帮助理解这段代码的实现原理。它包含以下:

  1. 发布者:添加、删除订阅者和通知订阅者
  2. 订阅者:取消订阅和更新方法
  3. 演示了如何删除订阅者obs1,之后只有obs2接收到了通知

三、代理模式

概念总结:为另一个对象提供一个代理或者占位符,并控制对这个对象的访问。代理控制对目标对象的访问,并可以执行相关操作,比如延迟加载、访问控制、缓存等

// 目标对象
const target = {
    // 目标方法
    doSomething: function () {
        console.log('Target: doSomething 目标方法');
    },
};

// 代理类
class Proxy {
    constructor() {
        // 维护对目标对象的引用
        this.target = target;
    }

    // 代理方法
    doSomething() {
        console.log('Proxy: doSomething 代理方法');
        // 调用目标对象方法
        this.target.doSomething();
    }
}

// 代理对象
const proxy = new Proxy();

// 通过代理对象调用方法
proxy.doSomething();

// Proxy: doSomething 代理方法
// Target: doSomething 目标方法

上述代码实现了代理模式,主要包含:

  1. 目标对象:需要代理的目标方法
  2. 代理类:维护对目标对象的引用并提供代理方法
  3. 代理方法:预处理操作,调用目标方法,后续操作
  4. 通过代理对象调用方法实际执行的是代理方法
  5. 代理方法负责在目标方法前后执行需要的操作,并中转调用目标方法
  6. 代理模式的关键在于代理类代理了目标对象,通过代理对象调用方法实现了对目标对象方法的间接访问,并扩展了额外功能

四、工厂模式

概念总结:工厂模式解决的主要问题是实例化对象。它提供一种机制,可以让子类决定应当创建哪个类的实例
工厂模式有以下主要特征

  1. 工厂类:用于创建对象的类,包含判断逻辑和实例化对象的方法
  2. 产品类:需要被工厂类实例化的类,包含实例方法
  3. 创建方法:工厂类中的方法,根据传入的参数创建对应的产品类实例
  4. 调用方法:通过工厂类的创建方法实例化产品类,然后调用产品类实例的方法

工厂模式的主要优点是:

  1. 隐藏对象创建的复杂性。调用者只需要传入正确的参数,工厂方法会返回正确的对象
  2. 新加入的产品类不会影响已有的调用方法。我们只需要在工厂类中增加判断逻辑和对象创建代码即可
  3. 更容易扩展产品类。我们可以增加新的产品类,并在工厂方法中进行创建,而不影响已有的调用方法

实现方式主要有简单工厂工厂方法两种模式:

  • 简单工厂:工厂类包含判断逻辑和所有产品类的实例化方法
  • 工厂方法:将产品类实例化推迟到子类。工厂类中只包含抽象创建方法,子类实现并实例化相关产品类

综上所述,工厂模式通过引入工厂类和创建方法,实现了实例化对象的解耦。调用者只需传入正确的参数,工厂方法会返回对应的对象实例,屏蔽对象创建的细节与复杂性

简单工厂模式

// 工厂类
class Factory {
    // 简单工厂模式
    static create(type) {
        // 判断类型并实例化对象
        if (type === 'A') {
            return new ProductA();
        } else if (type === 'B') {
            return new ProductB();
        }
    }
}

// 产品A
class ProductA {
    doSomething() {
        console.log('ProductA');
    }
}

// 产品B
class ProductB {
    doSomething() {
        console.log('ProductB');
    }
}

// 调用工厂方法,得到产品A实例并调用方法
const product = Factory.create('A');
product.doSomething(); // ProductA

工厂方法

// 工厂类
class Factory {
    // 工厂方法
    static create(type) {
        // 子类实现
    }
}

// 具体工厂1
class ConcreteFactory1 extends Factory {
    static create(type) {
        // 实例化产品1
        if (type === 'product1') {
            return new Product1();
        }
    }
}

// 具体工厂2
class ConcreteFactory2 extends Factory {
    static create(type) {
        // 实例化产品2
        if (type === 'product2') {
            return new Product2();
        }
    }
}

// 产品1
class Product1 {
    doSomething() {
        console.log('Product1');
    }
}

// 产品2
class Product2 {
    doSomething() {
        console.log('Product2');
    }
}

// 调用第一个工厂方法得到产品1实例
const product = ConcreteFactory1.create('product1');
product.doSomething(); // Product1

五、装饰器模式

概念总结:具体装饰对象通过调用父类方法,间接执行了源对象方法

// 源对象接口
class Source {
    doSomething() {}
}

// 源对象实现
class ConcreteSource extends Source {
    doSomething() {
        console.log('Source doSomething');
    }
}

// 装饰对象接口
class Decorator {
    // 构造函数接收源对象
    constructor(source) {
        this.source = source;
    }

    doSomething() {
        // 直接调用源对象方法
        this.source.doSomething();
    }
}

// 具体装饰对象
class ConcreteDecorator extends Decorator {
    doSomething() {
        console.log('Decorator doSomething');
        // 调用父类的方法,间接调用源对象方法
        super.doSomething();
    }
}

// 源对象实例
const source = new ConcreteSource();

// 装饰对象实例,接收源对象
const decorator = new ConcreteDecorator(source);

// 通过装饰对象调用方法
decorator.doSomething();

// Decorator doSomething
// Source doSomething

在这个实现中:

  1. 装饰对象接口中,我添加了直接调用源对象方法的代码
  2. 具体装饰对象通过调用父类方法,间接执行了源对象方法
  3. 这样,我们实现了装饰对象 decorator 完整地装饰了源对象 source

这是装饰者模式完整的实现,需要同时具有:

  1. 直接访问源对象方法
  2. 在此基础上提供额外的装饰与扩展

六、命令模式

// 命令接口
class Command {
    execute() {}
}

// 具体命令1
class ConcreteCommand1 extends Command {
    // 构造函数接收命令执行函数
    constructor(func) {
        super();
        this.func = func;
    }

    execute() {
        // 执行命令函数
        this.func();
    }
}

// 具体命令2
class ConcreteCommand2 extends Command {
    constructor(func) {
        super();
        this.func = func;
    }

    execute() {
        this.func();
    }
}

// 执行命令1
const executeFunc1 = function () {
    console.log('Execute Command1');
};

// 生成命令1
var command1 = new ConcreteCommand1(executeFunc1);

// 执行命令2
const executeFunc2 = function () {
    console.log('Execute Command2');
};

// 生成命令2
const command2 = new ConcreteCommand2(executeFunc2);

// 使用命令1
command1.execute(); // Execute Command1

// 使用命令2
command2.execute(); // Execute Command2

这个实现包含:

  1. 命令接口:定义执行方法
  2. 具体命令:实现命令接口,构造函数接收真实命令函数
  3. 执行函数:需要被封装成命令的函数
  4. 生成命令:通过具体命令传入执行函数,生成命令对象
  5. 执行命令:调用命令对象的 execute 方法,间接执行真实函数

命令模式的关键在于命令对象封装真实需要执行的函数。通过调用命令对象的方法,实现了对函数的间接调用。这使得调用者和真实命令执行者解耦

写在最后:

以上内容,便是这次记录分享的几种设计模式,通过简单案例可以加深对这几种设计模式的记忆和理解,当然,在我们开发中,设计模式更是无处不在,观察订阅者模式等等都是老朋友了,日久积累时非常重要的持久化输出手段,加油!各位同学~