本文主要探讨前端开发中的各种设计模式,主要分类有:
- 单例模式
- 建造者模式
- 代理模式
- 装饰器模式
- 适配器模式
- 策略模式
- 观察者模式
- 发布订阅模式
通过对他们实际开发中的使用场景的解析,深入浅出的一起更全面直观的进行学习:
一、单例模式
介绍:
单例模式确保一个类只有一个实例,并提供一个全局访问点。
实际使用场景:
实现全局唯一的状态管理,如全局配置对象、日志记录器等。
优点:
- 减少系统资源开销,因为只创建一个实例。
- 提供全局访问点,方便在不同部分的代码中使用。
缺点:
- 违反单一职责原则,因为单例类可能承担过多的职责。
- 可能会导致代码的紧耦合,因为其他部分的代码都依赖于这个单例。
代码实现1:
class Singleton {
private static instance: Singleton;
private constructor() {}
public static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
public someMethod(): void {
console.log('Singleton method called.');
}
}
const singleton1 = Singleton.getInstance();
const singleton2 = Singleton.getInstance();
console.log(singleton1 === singleton2); // true
通过静态方法 getInstance
来获取唯一的实例。如果实例不存在则创建一个新的实例并保存起来,下次调用时直接返回已有的实例。
代码实例2
element-ui对于全局loading的处理,使用的就是单例模式进行控制,每次只能触发一个全局loading
let fullscreenLoading;
const loading = (options = {}) =>{
// options不传的话默认是fullscreen
options = merge({}, defaults, options);
if(options.fullscreen && fullscreenLoading){
return fullscreenLoading; // 存在直接return
}
let parent = options.body? document.body: options.target;
let instance = new LoadingConstrutor({
el: document.createElement('div')
});
if (options.fullscreen) {
fullscreenLoading = instance
}
return instance
}
这样通过Element
的loading
的时候,如果同时调用两次,只有一个loading
的遮罩层,不会有两个
二、建造者模式
介绍:
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。目的是为了生成对象,把复杂的创建过程从构造函数分离出来,然后在不改变原有构造函数的基础上,创建各种各样的对象。
实际使用场景:
构建复杂的表单对象、配置对象等。
优点:
- 使得对象的创建过程更加清晰,易于理解和维护。
- 可以方便地创建不同表示的对象,提高了代码的灵活性。
缺点:
- 增加了代码的复杂性,需要创建多个类来实现建造者模式。
- 对于简单的对象创建,可能会显得过于繁琐。
代码实现:
class Product {
parts: string[] = [];
addPart(part: string): void {
this.parts.push(part);
}
}
class Builder {
buildPartA(): void {
// 构建部分 A 的逻辑
}
buildPartB(): void {
// 构建部分 B 的逻辑
}
getResult(): Product {
const product = new Product();
product.addPart('Part A');
product.addPart('Part B');
return product;
}
}
const builder = new Builder();
const product = builder.getResult();
console.log(product.parts);
Builder
类负责构建复杂对象 Product
,通过不同的方法逐步构建产品的各个部分,最后返回构建好的产品。
三、代理模式
介绍:
为其他对象提供一种代理以控制对这个对象的访问。
实际使用场景:
- 图片懒加载、数据预加载。
- 权限控制等。
优点:
- 可以在不改变目标对象的情况下,为其添加额外的功能。
- 可以控制对目标对象的访问,提高安全性和性能。
缺点:
- 增加了代码的复杂性,需要创建代理对象。
- 可能会影响性能,因为代理对象需要进行额外的处理。
代码实现:
class RealImage {
loadImage(): void {
console.log('Loading real image.');
}
}
class ImageProxy {
private realImage: RealImage | null = null;
loadImage(): void {
if (!this.realImage) {
this.realImage = new RealImage();
}
this.realImage.loadImage();
}
}
const proxy = new ImageProxy();
proxy.loadImage();
ImageProxy
代理了 RealImage
的加载操作,在实际需要加载图像时才创建真正的图像对象,实现了延迟加载。
四、装饰器模式
介绍:
动态地给一个对象添加一些额外的职责,而不改变其结构。
实际使用场景:
- 为函数或类添加日志记录、性能监控等功能。
优点:
- 可以在不改变原有代码结构的情况下,为对象添加新的功能。
- 可以方便地组合多个装饰器,实现更复杂的功能。
缺点:
- 装饰器的使用可能会使代码变得难以理解,尤其是当装饰器嵌套过多时。
- 可能会增加代码的复杂性,因为需要创建装饰器类。
代码实现:
function logDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Calling method ${propertyKey}`);
const result = originalMethod.apply(this, args);
console.log(`Method ${propertyKey} finished`);
return result;
};
return descriptor;
}
class MyClass {
@logDecorator
myMethod(): void {
console.log('Inside myMethod.');
}
}
const myObj = new MyClass();
myObj.myMethod();
装饰器函数 logDecorator
被安装到myMethod方案上,在myMethod调用前后添加了日志记录功能。
五、适配器模式
介绍:
将一个类的接口转换成客户希望的另外一个接口。
实际使用场景:
- 不同库之间的接口适配。
- 旧系统与新系统的接口整合等。
优点:
- 提高了代码的复用性,使得不同接口的类可以协同工作。
- 可以将复杂的接口转换为简单的接口,方便使用。
缺点:
- 增加了代码的复杂性,需要创建适配器类。
- 可能会降低系统的性能,因为适配器需要进行额外的处理。
代码实现:
class OldLibrary {
oldMethod(): string {
return 'Old library output';
}
}
class Adapter {
private oldLibrary: OldLibrary;
constructor() {
this.oldLibrary = new OldLibrary();
}
newMethod(): string {
return `Adapted: ${this.oldLibrary.oldMethod()}`;
}
}
const adapter = new Adapter();
console.log(adapter.newMethod());
Adapter
类将旧库的接口转换为新的接口,使得可以在新的代码中使用旧库的功能。
六、策略模式
介绍:
定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
实际使用场景:
- 表单验证、排序算法选择等。
- 当出现很多if-else或者switch时可以考虑使用策略模式
优点:
- 易于扩展和维护,当需要添加新的算法时,只需要创建一个新的策略类。
- 可以在运行时动态地切换算法。
缺点:
- 增加了代码的复杂性,需要创建多个策略类。
- 客户端需要了解不同的策略,增加了客户端的复杂度。
代码实现:
interface Strategy {
execute(): void;
}
class ConcreteStrategyA implements Strategy {
execute(): void {
console.log('Executing strategy A.');
}
}
class ConcreteStrategyB implements Strategy {
execute(): void {
console.log('Executing strategy B.');
}
}
class Context {
private strategy: Strategy;
constructor(strategy: Strategy) {
this.strategy = strategy;
}
setStrategy(strategy: Strategy): void {
this.strategy = strategy;
}
executeStrategy(): void {
this.strategy.execute();
}
}
const context = new Context(new ConcreteStrategyA());
context.executeStrategy();
context.setStrategy(new ConcreteStrategyB());
context.executeStrategy();
Context
类根据不同的策略对象执行不同的算法,通过设置不同的策略可以在运行时动态切换算法。
观察者模式
介绍:
定义了一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都将得到通知并自动更新。
实际使用场景:
- 事件处理、状态管理等。
优点:
- 实现了松耦合,观察者和被观察者之间的依赖关系很弱。
- 可以方便地添加和删除观察者。
缺点:
- 当观察者数量较多时,通知所有观察者可能会导致性能问题。
- 观察者模式可能会导致循环依赖的问题。
代码实现:
class Subject {
private observers: Observer[] = [];
addObserver(observer: Observer): void {
this.observers.push(observer);
}
removeObserver(observer: Observer): void {
this.observers = this.observers.filter(obs => obs!== observer);
}
notifyObservers(): void {
this.observers.forEach(observer => observer.update());
}
}
interface Observer {
update(): void;
}
class ConcreteObserver implements Observer {
update(): void {
console.log('Observer notified.');
}
}
const subject = new Subject();
const observer1 = new ConcreteObserver();
const observer2 = new ConcreteObserver();
subject.addObserver(observer1);
subject.addObserver(observer2);
subject.notifyObservers();
Subject
维护一组观察者,当状态变化时通知所有观察者进行更新。
八、发布订阅模式
介绍:
发布者和订阅者之间通过事件进行通信,发布者发布事件,订阅者订阅感兴趣的事件并在事件发生时做出响应。
实际使用场景:
- 消息通知、事件总线等。
优点:
- 实现了松耦合,发布者和订阅者之间不需要直接知道对方的存在。
- 可以方便地添加和删除订阅者。
缺点:
- 当事件过多时,管理事件可能会变得复杂。
- 可能会导致内存泄漏,如果订阅者没有正确地取消订阅。
代码实现:
class EventEmitter {
private events: { [eventName: string]: ((...args: any[]) => void)[] } = {};
on(eventName: string, callback: (...args: any[]) => void): void {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
}
emit(eventName: string,...args: any[]): void {
if (this.events[eventName]) {
this.events[eventName].forEach(callback => callback(...args));
}
}
}
const eventEmitter = new EventEmitter();
eventEmitter.on('eventName', (data) => {
console.log(`Received event with data: ${data}`);
});
eventEmitter.emit('eventName', 'Some data');
EventEmitter
类提供了订阅和发布事件的方法,订阅者通过 on
方法订阅事件,发布者通过 emit
方法发布事件,触发订阅者的回调函数。