写文章点击打开程序员玫玫的主页前端设计模式知识体系(三): 前端常用的15种设计模式之工厂、单例、发布订阅、观察者、装饰者模式、命令模式

在这篇文章中,我们将把焦点转向前端开发领域,探讨在实际开发中广泛使用的 15 种设计模式。

前端开发与其他软件工程领域不同,其面临的挑战和需求具有独特性。例如,用户界面的动态变化、组件的重用、以及复杂的状态管理等问题,都需要特定的设计模式来优化解决。通过了解和应用这些前端设计模式,您可以更高效地处理常见的开发任务,提升代码的可维护性和扩展性,并应对不断变化的需求。

我们将从实际应用出发,详细介绍这些设计模式的背景、核心概念和实际使用场景,帮助您在前端开发中更加得心应手。无论您是新手还是有经验的前端工程师,这些设计模式都将为您的开发实践提供宝贵的指导和支持。

工厂模式

工厂模式是一种创建对象的设计模式,它通过定义一个接口来创建对象,但允许子类决定实例化哪一个类。这使得对象的创建过程可以被子类灵活调整,而不会改变客户端代码。

工厂模式的好处

解耦对象创建:客户端不需要知道创建对象的具体类,只需要知道一个通用接口或基类,从而降低了系统的耦合性。

提高代码可扩展性:可以在不修改现有代码的情况下,添加新的产品类。

简化代码:将对象的创建逻辑封装在工厂方法中,简化了客户端的代码结构。

符合开闭原则:新增对象时,不需要修改工厂接口,而是通过扩展增加对象类型,增强了代码的稳定性和灵活性。

工厂模式的应用场景

对象的创建复杂且逻辑较多时:当对象的创建过程较复杂,且可能涉及到不同的配置或初始化时,可以使用工厂模式简化客户端代码。

处理多种对象创建:当系统需要处理多种不同类型的对象时,工厂模式能够根据条件创建不同的对象。

动态生成类实例:在前端开发中,如根据用户输入或其他动态条件生成特定类型的组件,工厂模式非常适合。

// 动物工厂,根据不同类型生成不同的实例
function AnimalFactory(type, name) {
    switch (type) {
        case 'cat':
            return new Cat(name);
        case 'dog':
            return new Dog(name);
        case 'human':
            return new Human(name);
        default:
            throw new Error(`Type ${type} is not recognized`);
    }
}

// Cat 类
function Cat(name) {
    this.type = 'cat';
    this.name = name;
    this.speak = function() {
        console.log(`${this.name} says: Meow!`);
    }
}

// Dog 类
function Dog(name) {
    this.type = 'dog';
    this.name = name;
    this.speak = function() {
        console.log(`${this.name} says: Woof!`);
    }
}

// Human 类
function Human(name) {
    this.type = 'human';
    this.name = name;
    this.speak = function() {
        console.log(`${this.name} says: Hello!`);
    }
}

// 使用工厂创建不同的对象
const myCat = AnimalFactory('cat', 'Kitty');
myCat.speak(); // Kitty says: Meow!

const myDog = AnimalFactory('dog', 'Rex');
myDog.speak(); // Rex says: Woof!

const myHuman = AnimalFactory('human', 'John');
myHuman.speak(); // John says: Hello!

单例模式

确保一个类只有一个实例,并提供全局访问点。在前端开发中, 可以使用单例模式来管理全局状态和资源。 例如在Vue.js应用中,Vuex用于管理应用的全局状态,只存在一个实例。

前端使用单例模式实现的比较出名的库: Vuex、Redux 等全局状态管理库。

实现方式

  • 使用字面量可以轻松地创建单例对象。(区别于 Java 因为 Java 需要先定义 class)
const singleton = {
   property: "value1",
   property2: "value2",
   methods1: function(){}
   methods2: function(){}
}
  • 在 Javascript 中, 每个构造函数都可以用于创建创建单例对象
class Singleton {
    static instance: Singleton;
    prop1 = "111"
    prop2 = "222"
    constructor() {
        if (Singleton.instance != null) {
            return Singleton.instance
        }
        Singleton.instance = this
    }
}

const instance1 = new Singleton()
instance1.prop1 = "000"
const instance2 = new Singleton()
const instance3 = new Singleton()

console.log(instance1, instance2, instance3)
// Singleton { prop1: '000', prop2: '222' } 
// Singleton { prop1: '000', prop2: '222' } 
// Singleton { prop1: '000', prop2: '222' }
  • 通过私有构造函数和静态方法 getInstance() 确保类只能有一个实例,并在首次调用时延迟实例化。
// 不能够使用 new 实例化
// 直接使用 Singleton2.getInstance() 来调用
class Singleton2 {
    static instance: Singleton2;
    prop1 = "111"
    prop2 = "222"
    static getInstance() {
        if (Singleton2.instance != null) {
            return Singleton2.instance
        }
        Singleton2.instance = new Singleton2()
        return Singleton2.instance
    }
    private constructor() {

    }
}
const obj = Singleton2.getInstance()
const obj2 = Singleton2.getInstance()
obj === obj2 // true

// 不允许
const obj3 = new Singleton2() // 类“Singleton2”的构造函数是私有的,仅可在类声明中访问。ts(2673)

发布-订阅模式

前端中用的比较多 (event、 dom event)

发布订阅模式 (Publish-Subscribe Pattern) 也叫消息队列模式,它是一种将发布者和订阅者解耦的设计模式。在前端开发中, 可以使用发布订阅模式来实现组件之间的通信。

经典案例: vue 的事件总线, Redux 中的 store, Publish.js、Eventbus

应用场景: 大文件上传、总线通信等。

document.addEventListener('click',()=>{}) // 发布订阅模式, 订阅的 API

// 创建一个 EventListener
class MyEvent {
    private eventMap: Record<string, Array<Function>> = {}
    // 仿 addEventListener
    addEvent(eventName: string, callback: Function) {
        if (!this.eventMap[eventName]) {
            this.eventMap[eventName] = []
        }
        this.eventMap[eventName].push(callback)
    }
    removeEvent(eventName: string, callback: Function) {
        if (!this.eventMap[eventName]) {
            return
        }
        this.eventMap[eventName] = this.eventMap[eventName].filter(fn => fn !== callback)
    }
    triggerEvent(eventName: string, ...data: any) {
        if (!this.eventMap[eventName]) {
            return
        }
        this.eventMap[eventName].forEach(fn => fn(...data)) // 约定展开参数
    }

}

const myEvent = new MyEvent()

myEvent.addEvent("click", (...data: any) => console.log("click1", data))
myEvent.addEvent("click", (...data: any) => console.log("click2"))
const click3Cb = () => console.log("click3")

myEvent.addEvent("click", click3Cb) // 注意 两个相同的箭头函数是不相等的, 要用变量来保存, 内存地址比较
myEvent.removeEvent("click", click3Cb)

myEvent.addEvent("move", (...data: any) => console.log("move1"))
myEvent.addEvent("move", (...data: any) => console.log("move2"))
myEvent.addEvent("move", (...data: any) => console.log("move3"))

myEvent.triggerEvent("click", 1, 2, 3) // click1 [ 1, 2, 3 ] click2 

实现了一个简单的自定义事件监听器 MyEvent,类似于浏览器的 addEventListener 和 removeEventListener 功能。MyEvent 使用 eventMap 存储事件名称与回调函数的映射。你可以通过 addEvent 方法为某个事件添加多个回调,通过 removeEvent 方法移除指定的回调,并通过 triggerEvent 触发对应的事件,传递参数给所有注册的回调函数。

观察者模式

观察者模式和发布订阅模式在前端开发中都非常常见,但两者有细微区别。观察者模式不需要 eventName,可以理解为一种简化版的发布订阅模式。观察者模式中的 subject 只关注一个单一的 channel,即所有事件类型只有一种,没有复杂的事件分类。而发布订阅模式则支持多个 eventName,允许更复杂的事件管理。在一些数据流管理场景下,Rx.js 等工具对业务逻辑的处理显得尤为重要,特别是在需要处理异步流的复杂应用中。

观察者模式 Observer Pattern: 当对象间存在一对多的关系时, 使用观察者模式。当被观察的对象发生变化时, 其所有的观察者都会收到通知并进行相应的操作。在 JavaScript 中, 可以使用回调函数或事件来监听实现观察者模式。

在前端开发中, 观察者模式常备用来实现组件间的数据传递和事件处理。比如, 当一个组件的状态发生改变时, 可以通过观察者模式来通知其他组件更新自身的状态或视图。

在观察者模式中,通常会定义两种角色: 观察者(Observer)和 被观察者/主题(Subject)

class Subject {
  constructor() {
    this.observers = [];
  }
  addObserver(observer) {
    this.observers.push(observer);
  }
  removeObserver(observer) {
    this.observers = this.observers.filter((obs) => obs !== observer);
  }
  notify(data) {
    this.observers.forEach((obs) => obs.update(data));
  }
}

class Observer {
  update(data) {
    console.log(`Received data: ${data}`);
  }
}

const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();

subject.addObserver(observer1);
subject.addObserver(observer2);

subject.notify('Hello, observers!');
subject.removeObserver(observer1);
subject.notify('Goodbye, observers!');

装饰者模式

注解(Annotation)是一种用于为代码提供元数据的特殊语法标记,通常不影响程序的运行,但可用于编译时、运行时进行额外处理或行为控制。

前端常用较多: Nest.js、Angular

装饰者模式 (Decorator Pattern): 动态地给一个对象添加额外的职责, 在前端开发中, 可以使用装饰者模式来动态修改组件的行为和样式。

JavaScript 中的装饰者模式可以通过以下几种方式实现:

对象的装饰器的典型实现

const obj = {
    foo(){
        console.log("foo")
    }
}

function barDecorator(obj){
    obj.bar = function(){
        console.log("bar")
    }
    return obj
}
const decorateObj = barDecorator(obj) // @barDecorator
decorateObj.foo()
decorateObj.bar()

nest.js 代码节选

import { Controller, Get, Param } from '@nestjs/common';
import { UserService } from './user.service';

// 控制器的基本定义,`@Controller()` 装饰器定义了路由的前缀,这里是 "users"
@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  // 处理 HTTP GET 请求的路由,路径为 `/users`
  @Get()
  findAll() {
    // 调用服务中的 `findAll` 方法,返回用户列表
    return this.userService.findAll();
  }

  // 处理动态路由,路径为 `/users/:id`
  @Get(':id')
  findOne(@Param('id') id: string) {
    // 获取请求中的 `id` 参数,并调用服务中的 `findOne` 方法,查找特定用户
    return this.userService.findOne(id);
  }
}

命令模式

命令模式 (Command Pattern) 是一种将操作封装成对象的设计模式,这样你可以把方法调用、参数都打包在一起。简单来说,就是把操作变成一个可以传来传去的小对象,方便存储、执行、甚至撤销和重做。

在前端开发中,命令模式特别适合那些需要 撤销 和 重做 的场景。比如在文本编辑器里,每次用户修改文本,你可以创建一个命令对象,把这个操作记录下来。需要撤销时,从历史记录里找到最新的命令,执行它的“反向操作”就行了。这样,不管用户做了多少次修改,你都能轻松处理撤销和重做。

这背后还可以有个 命令处理器,专门用来管理这些命令,像一个聪明的小管家,帮你处理操作记录,让系统更灵活更强大。

class Command {
    executed = false
    constructor(private receiver: Receiver, private args: any) {

    }
    execute() {
        if (this.executed) return
        this.receiver.execute(this.args)
        this.executed = true
    }
    undo() {
        if (!this.executed) return
        this.receiver.undo(this.args)
        this.executed = false
    }
}

class Receiver {
    private value = 0
    execute(args?: any) {
        this.value += args
    }
    undo(args?: any) {
        this.value -= args
    }
}

const receiver = new Receiver()
const command1 = new Command(receiver, 1)
const command2 = new Command(receiver, -1)
command1.execute()
console.log(receiver)
command2.execute()
console.log(receiver)

结语

通过本篇文章的探索,我们已经初步掌握了前端开发中常用的几种设计模式。在接下来的学习中,我们会继续深入,解锁更多实用的设计模式,如迭代器模式、原型模式、职责链模式等,看看它们是如何帮助我们优化复杂的业务逻辑和项目架构。前端开发是个不断积累的过程,设计模式也像是开发中的一把把利器,学会了,你就能更从容地应对各种挑战。准备好,下一篇我们继续“开箱”新工具吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值