类别
- 创建型模式(Creational Patterns):主要关注对象的创建过程,目的是通过控制对象的创建方式来提高代码的灵活性和可复用性。
- 构造器模式(Constructor Pattern)
- 原型模式(Prototype Pattern)
- 简单工厂模式(Simple Factory Pattern)& 工厂模式(Factory Method Pattern)& 抽象工厂模式(Abstract Factory Pattern)
- 建造者模式(Builder Pattern)
- 单例模式(Singleton Pattern)
- 结构型模式(Structural Patterns):关注类和对象的组合,目的是通过使用继承和接口来简化设计。
- 装饰器模式(Decorator Pattern)
- 适配器模式(Adapter Pattern)
- 代理模式(Proxy Pattern)
- 外观模式(Facade Pattern)
- 桥接模式(Bridge Pattern)
- 组合模式(Composite Pattern)
- 享元模式(Flyweight Pattern)
- 行为型模式(Behavioral Patterns):关注对象之间的责任分配和交互,目的是通过定义对象之间的通信方式来提高系统的灵活性和可扩展性。
- 策略模式(Strategy Pattern)
- 模板方法模式(Template Method Pattern)
- 观察者模式(Observer Pattern)
- 发布订阅模式(Publish-Subscribe Pattern)
- 迭代器模式(Iterator Pattern)
- 职责链模式(Chain of Responsibility Pattern)
- 命令模式(Command Pattern)
- 备忘录模式(Memento Pattern)
- 状态模式(State Pattern)
- 访问者模式(Visitor Pattern)
创建型模式
构造器模式 Constructor Pattern
- 目的:构造器模式的主要目的是创建多个具有相同属性和方法的对象
- 优点:
- 可重用性:可以通过构造函数创建多个相似的对象。
- 封装性:将对象的创建逻辑封装在构造函数中,便于管理和维护。
- 类型检查:使用构造函数创建的对象可以进行类型检查(通过 instanceof 操作符)。
- 缺点:
- 内存消耗:每个实例都会复制构造函数中的方法,可能导致内存消耗较大。
- 扩展性:构造函数中的方法不能共享,每个实例都有自己的方法副本。
- 公用方法会创建多个,占用内存空间,造成不必要的资源浪费
// 定义一个构造函数
function Person(name, age) {
this.name = name;
this.age = age;
// 直接在实例上挂载方法
this.greet = function () {
console.log(
`Hello, my name is ${this.name} and I am ${this.age} years old.`
);
};
}
// 使用构造函数创建对象
const person1 = new Person("Alice", 30);
const person2 = new Person("Bob", 25);
原型模式
- 优点:
- 性能优化:通过克隆现有对象来创建新对象,避免了重复的初始化过程,提高了性能。
- 减少构造函数的使用:不需要每次都调用构造函数,简化了对象的创建过程。
- 动态扩展:可以在运行时动态地添加或修改对象的属性和方法。
- 缺点:
- 深拷贝问题:原型模式通常涉及浅拷贝,如果对象包含引用类型的属性,可能会导致修改一个对象的属性影响到其他对象。
- 复杂性:实现深拷贝可能比较复杂,需要额外处理引用类型属性的拷贝。
- 原型污染:如果不小心修改了原型对象的属性,可能会影响到所有基于该原型创建的对象。
// 定义一个构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
// 在构造函数的原型上添加方法,能够避免掉构造器模式重复创建同一函数而占用资源的缺点
Person.prototype.greet = function () {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};
// 使用构造函数创建对象
const person1 = new Person("Alice", 30);
const person2 = new Person("Bob", 25);
简单工厂模式(Simple Factory Pattern)& 工厂模式(Factory Method Pattern)& 抽象工厂模式(Abstract Factory Pattern)
- 产品等级结构:产品等级结构就是产品的继承结构,比如一个抽象类是电视机,那么其子类会有海尔电视,TCL 电视,小米电视等等,那么抽象电视机和具体品牌的电视机之间就构成了一个产品等级结构,抽象电视机是父类,具体品牌的电视机是子类。
- 产品族:在抽象工厂模式中,产品族是指由一个工厂生产的,位于不同产品等级结构中的一组产品,比如海尔电器工厂既生产海尔电视机,也生产海尔热水器,电视机和热水器位于不同的产品等级结构中,如果它们是由同一个工厂生产的,就称为产品族。
- 简单工厂模式:
- 创建对象时是直接在接口中
new 目标类出来的,需要用户明确告知目标对象种类 - 单一产品等级结构,需要根据入参选择需要的产品类,此时函数能够明确知道产品类是什么
- 创建对象时是直接在接口中
- 工厂模式:
- 对象是交由接口中
new 工厂类().创建对应实例方法()出来的,需要用户明确告知目标对象种类 - 单一产品等级结构,一个抽象工厂类,多个工厂类(一般仅负责一种产品的创建),需要根据入参选择选用对应的工厂类,此时函数能够明确知道用的是哪个工厂类,却不知道使用的是哪个产品类
- 对象是交由接口中
- 抽象工厂模式:
- 多个产品等级结构,一个抽象工厂类,多个工厂类(一般负责多种产品的创建)
- 直接调用具体工厂类的具体方法就能够获取到实例,无需根据入参判断
- 相较于工厂模式,能够构建的产品多了
简单工厂模式
/**
* 抽象咖啡类
*/
abstract class Coffee {
constructor(public name: string) { }
}
class LatteCoffee extends Coffee { }
class MachaCoffee extends Coffee { }
class AmericanoCoffee extends Coffee { }
// 简单工厂实现
class CoffeFactory {
static buy(name: string) {
switch (name) {
case 'LatteCoffee':
return new LatteCoffee('拿铁咖啡');
case 'MachaCoffee':
return new MachaCoffee('摩卡咖啡');
case 'AmericanoCoffee':
return new AmericanoCoffee('美式咖啡');
default:
throw new Error('没有您需要的咖啡')
}
}
}
console.log(CoffeFactory.buy('LatteCoffee'));
console.log(CoffeFactory.buy('MachaCoffee'));
console.log(CoffeFactory.buy('AmericanoCoffee'));
工厂模式
/**
* 抽象咖啡类
*/
abstract class Coffee {
constructor(public name: string) { }
}
class LatteCoffee extends Coffee { }
class MachaCoffee extends Coffee { }
class AmericanoCoffee extends Coffee { }
// 抽象咖啡工厂类
abstract class CoffeeFactory {
constructor() { }
}
class LatteCoffeeFactory extends CoffeeFactory {
createCoffe() {
console.log('您创建了一份拿铁咖啡');
}
}
class MachaCoffeeFactory extends CoffeeFactory {
createCoffe() {
console.log('您创建了一份摩卡咖啡');
}
}
class AmericanoCoffeeFactory extends CoffeeFactory {
createCoffe() {
console.log('您创建了一份美式咖啡');
}
}
// 在工厂方法里,不再由 Factory 来创建产品,而是先创建了具体的工厂,然后具体的工厂创建产品
class Factory {
static buy(name: string) {
switch (name) {
case 'LatteCoffee':
// 先创建了拿铁咖啡具体的工厂,然后再由具体的工厂再创建产品
return new LatteCoffeeFactory().createCoffe();
case 'MachaCoffee':
// 先创建了摩卡咖啡具体的工厂,然后再由具体的工厂再创建产品
return new MachaCoffeeFactory().createCoffe();
case 'AmericanoCoffee':
// 先创建了美式咖啡具体的工厂,然后再由具体的工厂再创建产品
return new AmericanoCoffeeFactory().createCoffe();
default:
throw new Error('没有您需要的咖啡')
}
}
}
console.log(Factory.buy('LatteCoffee'));
console.log(Factory.buy('MachaCoffee'));
console.log(Factory.buy('AmericanoCoffee'));
抽象工厂模式
/**
* 产品抽象类或接口
*/
abstract class MacProdoct { }
abstract class IWatchProdoct { }
abstract class PhoneProdoct { }
/**
* 具体产品实现类
*/
class AppleMacProdoct extends MacProdoct { }
class HuaweiMacProdoct extends MacProdoct { }
class AppleIWatchProdoct extends IWatchProdoct { }
class HuaweiIWatchProdoct extends IWatchProdoct { }
class ApplePhoneProdoct extends PhoneProdoct { }
class HuaweiPhoneProdoct extends PhoneProdoct { }
/**
* 抽象工厂
*/
abstract class AbstractProdoctFactory {
abstract createMacProdoct(): MacProdoct;
abstract createIWatchProdoct(): IWatchProdoct;
abstract createPhoneProdoct(): PhoneProdoct;
}
/**
* 具体产品工厂 - 苹果
*/
class AppleProdoct extends AbstractProdoctFactory {
createMacProdoct() {
return new AppleMacProdoct();
}
createIWatchProdoct() {
return new AppleIWatchProdoct();
}
createPhoneProdoct() {
return new ApplePhoneProdoct();
}
}
/**
* 具体产品工厂 - 华为
*/
class HuaweiProdoct extends AbstractProdoctFactory {
createMacProdoct() {
return new HuaweiMacProdoct();
}
createIWatchProdoct() {
return new HuaweiIWatchProdoct();
}
createPhoneProdoct() {
return new HuaweiPhoneProdoct();
}
}
let huaweiProduct = new HuaweiProdoct();
console.log(huaweiProduct.createMacProdoct()); // 购买华为电脑
console.log(huaweiProduct.createIWatchProdoct()); // 购买华为手表
console.log(huaweiProduct.createPhoneProdoct()); // 购买华为手机
建造者模式 Builder Pattern
- 目的:将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。建造者模式通常用于创建复杂对象,这些对象的构建过程较为复杂,包含多个步骤或多个部分。
- 优点:
- 分离构建和表示:将复杂对象的构建过程与其表示分离,使得代码更清晰、更易于维护。
- 易于扩展:可以通过添加新的建造者来扩展构建过程,符合开闭原则。
- 细粒度控制:可以逐步构建复杂对象,对每个构建步骤进行细粒度控制。
- 缺点:
- 类的数量增加:每个具体建造者都需要一个类,可能会导致类的数量增加。
- 复杂性增加:对于简单的对象,使用建造者模式可能会增加不必要的复杂性。
- 结构:指导者通过建造者类来创建复杂对象,而具体建造者类负责具体的构建过程。
- 示例:假设我们有一个复杂的 Car 对象,它有多个部分(如引擎、车轮、座椅等),我们可以使用建造者模式来构建这个对象。
// 产品类:Car
class Car {
constructor() {
this.parts = [];
}
addPart(part) {
this.parts.push(part);
}
listParts() {
console.log(`Car parts: ${this.parts.join(", ")}`);
}
}
// 抽象建造者类
class CarBuilder {
createCar() {
this.car = new Car();
}
getCar() {
return this.car;
}
buildEngine() {}
buildWheels() {}
buildSeats() {}
}
// 具体建造者类:SportsCarBuilder
class SportsCarBuilder extends CarBuilder {
buildEngine() {
this.car.addPart("V8 Engine");
}
buildWheels() {
this.car.addPart("18 inch Wheels");
}
buildSeats() {
this.car.addPart("Leather Seats");
}
}
// 指挥者类:Director
class Director {
setBuilder(builder) {
this.builder = builder;
}
constructSportsCar() {
this.builder.createCar();
this.builder.buildEngine();
this.builder.buildWheels();
this.builder.buildSeats();
}
}
// 使用示例
const director = new Director();
const builder = new SportsCarBuilder();
director.setBuilder(builder);
director.constructSportsCar();
const car = builder.getCar();
car.listParts(); // 输出: Car parts: V8 Engine, 18 inch Wheels, Leather Seats
建造者模式与工厂模式区别
- 工厂模式用于重复创建大量的不同但相关的简单对象,其关注对象的创建过程。
- 建造者模式用于构建复杂的需要多步实现的对象,其关注对象的构建过程和细节。
单例模式 Singleton Pattern
- 保证一个类只有一个实例并提供唯一的全局入口
- 优点:避免频繁的创建与销毁实例,能够有效的降低系统资源的消耗;避免了命名空间的冲突
- 缺点:多线程情况下需要注意线程安全的问题
- 使用场景:pinia 就是单例模式
let instance;
class Singleton {
constructor(name) {
if (!instance) {
console.log("Creating new instance...");
this.name = name;
instance = this;
}
return instance;
}
}
let singleton1 = new Singleton("instance1");
let singleton2 = new Singleton("instance2");
console.log(singleton1 === singleton2);
结构型模式
装饰器模式 Decorator Pattern
- 目的:主要用途是为一个原始对象添加新的功能或者修改已有的功能,同时保持原始对象的接口不变,并且可以在运行时动态地添加或删除功能
- 优点:
- 动态扩展功能:装饰器模式允许在运行时动态地为对象添加新的功能,而无需修改其原有代码。
- 可组合性:多个装饰器可以组合使用,以实现复杂的功能组合。
- 易于扩展:新增功能只需添加新的装饰器类,不影响现有代码。
- 缺点:
- 复杂性增加:随着装饰器的增多,系统的复杂性可能会增加,难以管理和调试。
- 性能问题:装饰器模式可能会引入一定的性能开销,因为每个装饰器都会增加一层间接调用。
- 接口一致性:装饰器类必须与被装饰对象具有相同的接口,这可能需要额外的接口定义工作。
- 示例
// 定义一个基础组件
class Coffee {
cost() {
return 5; // 基础价格
}
}
// 定义一个装饰器基类
class CoffeeDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() {
return this.coffee.cost();
}
}
// 具体装饰器类:加牛奶
class MilkDecorator extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 2; // 加牛奶额外费用
}
}
// 具体装饰器类:加糖
class SugarDecorator extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 1; // 加糖额外费用
}
}
// 使用装饰器模式
let coffee = new Coffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
console.log(coffee.cost()); // 输出: 8 (5 + 2 + 1)
适配器模式 Adapter Pattern
- 目的:将一个类的接口转换成客户端所期望的另一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的类可以协同工作。
- 优点:
- 兼容性:允许现有类与不兼容的接口一起工作,提高了代码的复用性。
- 透明性:客户端无需了解适配器背后的复杂性,只需与适配器交互即可。
- 灵活性:可以在不修改现有代码的情况下,动态地添加或移除适配器。
- 缺点:
- 复杂性增加:引入适配器可能会增加系统的复杂性,需要额外的适配器类。
- 性能开销:适配器模式可能会引入一定的性能开销,因为需要进行接口转换。
- 过度使用:过度使用适配器模式可能会导致系统中存在过多的适配器,难以维护。
- 结构:被适配者 → 适配器 → 目标接口:适配器作为中间层,持有一个被适配者实例,实现目标接口的方法,并利用被适配者实例的成员来获取目标接口期望的结果
- 场景:用来统一用的:统一支付接口、统一数据格式
- 示例:
// 定义目标接口
class TargetInterface {
request() {
throw new Error("This method should be implemented.");
}
}
// 创建被适配者类
class Adaptee {
specificRequest() {
console.log("Specific request is called.");
}
}
// 创建适配器类
class Adapter extends TargetInterface {
constructor(adaptee) {
super();
this.adaptee = adaptee;
}
request() {
this.adaptee.specificRequest();
}
}
代理模式 Proxy Pattern
- 目的:通过代理来操作原对象
- 场景:校验权限、前后置操作、延迟加载、缓存
// 延迟加载
class ImageProxy {
constructor(imagePath) {
this.imagePath = imagePath;
this.image = null;
}
displayImage() {
if (!this.image) {
this.image = new Image();
this.image.src = this.imagePath;
this.image.onload = () => {
this.image.onload = null;
console.log("图片加载完成:", this.imagePath);
};
}
console.log("正在加载图片:", this.imagePath);
// 显示占位符图片
console.log("显示占位符图片");
}
}
// 缓存
// 定义一个网络请求对象
const NetworkRequest = {
getData: function (url) {
// 模拟网络请求
console.log("发送网络请求:", url);
return fetch(url).then((response) => response.json());
},
};
// 定义一个缓存代理对象
const CacheProxy = {
cache: {}, // 缓存数据的对象
getData: function (url) {
if (this.cache[url]) {
console.log("从缓存中获取数据:", url);
return Promise.resolve(this.cache[url]);
} else {
return NetworkRequest.getData(url).then((data) => {
console.log("将数据存入缓存:", url);
this.cache[url] = data;
return data;
});
}
},
};
//缓存代理进行数据请求
const url1 = "https://api.example.com/data";
const url2 = "https://api.example.com/data2";
CacheProxy.getData(url1).then((data) => {
console.log("获取到的数据:", data);
});
CacheProxy.getData(url1).then((data) => {
console.log("获取到的数据:", data); // 从缓存中获取数据
});
CacheProxy.getData(url2).then((data) => {
console.log("获取到的数据:", data);
});
外观模式
- 目的:简化子系统的复杂性,隐藏内部实现细节
- 优点:
- 简化接口:外观模式提供了一个简化的接口,使得客户端不需要了解子系统的复杂性。
- 解耦:客户端与子系统之间解耦,客户端只需要与外观对象交互,不需要直接与子系统交互。
- 提高可维护性:通过将复杂性封装在外观对象中,使得子系统的修改不会影响到客户端。
- 缺点:
- 可能引入性能问题:如果外观对象需要处理大量请求,可能会引入性能开销。
- 可能过度简化:外观模式可能会过度简化接口,导致无法满足某些复杂需求。
- 示例:假设我们有一个复杂的音频播放系统,包含多个子系统(如音频解码器、音频播放器、音频控制器等)。我们可以使用外观模式来简化客户端与这些子系统的交互。
// 子系统:音频解码器
class AudioDecoder {
decode(file) {
console.log(`Decoding ${file}`);
return "decoded audio data";
}
}
// 子系统:音频播放器
class AudioPlayer {
play(data) {
console.log(`Playing ${data}`);
}
}
// 子系统:音频控制器
class AudioController {
adjustVolume(level) {
console.log(`Adjusting volume to ${level}`);
}
}
// 外观对象:音频播放系统
class AudioSystemFacade {
constructor() {
this.decoder = new AudioDecoder();
this.player = new AudioPlayer();
this.controller = new AudioController();
}
playAudio(file) {
const decodedData = this.decoder.decode(file);
this.player.play(decodedData);
this.controller.adjustVolume(10);
}
}
// 使用示例
const audioSystem = new AudioSystemFacade();
audioSystem.playAudio("song.mp3");
// 输出:
// Decoding song.mp3
// Playing decoded audio data
// Adjusting volume to 10
桥接模式 Bridge Pattern
- 目的:将抽象部分与实现部分分离,使它们可以独立变化。可以视为突破类单继承限制的方法
- 结构:涉及两个独立的层次结构:抽象部分和实现部分,抽象部分定义了高层控制逻辑,依赖于实现部分。实现部分则提供具体的实现逻辑。
- 优点
- 分离抽象与实现:桥接模式将抽象部分与实现部分分离,使它们可以独立变化,从而提高了系统的灵活性和可扩展性。
- 避免类爆炸:通过组合而非继承,桥接模式避免了多层次继承带来的类爆炸问题,使得系统结构更加清晰。
- 增强可扩展性:抽象部分和实现部分可以独立扩展,互不影响,便于系统功能的增加和修改。
- 提高可维护性:由于抽象和实现分离,代码更易于理解和维护,降低了系统的复杂度。
- 缺点
- 增加复杂性:桥接模式引入了更多的类和接口,可能会增加系统的复杂性,对开发人员的要求更高。
- 设计难度增加:正确地应用桥接模式需要对系统结构有深入的理解,设计不当可能导致系统难以维护。
- 性能开销:由于桥接模式通过组合而非继承来实现功能,可能会引入一定的性能开销,特别是在频繁调用的情况下。
- 举例:抽象部分和实现部分可以独立变化,互不影响。例如,在一个图形应用程序中,形状(如圆形、方形)可以是一个层次结构,而颜色(如红色、蓝色)可以是另一个层次结构。形状和颜色可以独立变化,通过桥接模式将它们连接起来,从而实现灵活的组合。
/* 抽象部分 */
// 形状抽象类
class Shape {
constructor(color) {
this.color = color;
}
draw() {
throw new Error("Abstract method 'draw' must be overridden");
}
}
// 具体形状实现
class Circle extends Shape {
constructor(color) {
super(color);
}
draw() {
console.log("Drawing a circle");
this.color.applyColor();
}
}
class Square extends Shape {
constructor(color) {
super(color);
}
draw() {
console.log("Drawing a square");
this.color.applyColor();
}
}
/* 实现部分 */
// 颜色接口
class Color {
applyColor() {
throw new Error("Abstract method 'applyColor' must be overridden");
}
}
// 具体颜色实现
class RedColor extends Color {
applyColor() {
console.log("Applying red color");
}
}
class BlueColor extends Color {
applyColor() {
console.log("Applying blue color");
}
}
/* 使用 */
const red = new RedColor();
const blue = new BlueColor();
const redCircle = new Circle(red);
const blueSquare = new Square(blue);
redCircle.draw(); // 输出: Drawing a circle Applying red color
blueSquare.draw(); // 输出: Drawing a square Applying blue color
桥接模式与装饰器模式
二者都可以视作突破类单单继承限制的设计模式,不过他们之间有些区别
- 桥接模式:将抽象部分与实现部分分离,使它们可以独立变化。
- 装饰器模式:动态地给对象添加额外的功能,而不需要修改对象本身。
- 桥接模式通常涉及两个独立的继承层次结构(抽象部分和实现部分),而装饰器模式通常是在同一个继承层次结构中增加功能。
- 桥接模式适用于需要在多个维度上扩展功能的情况,而装饰器模式适用于需要动态地、透明地给对象添加职责的情况。
组合模式 Composite Pattern
- 目的:是让使用者能够以一致的方式处理单个对象和组合对象,从而简化代码,并使得系统更易于扩展和维护。
- 优点:
- 一致性:客户端可以统一处理单个对象和组合对象,无需区分它们。
- 灵活性:易于添加新的组件类型,符合开闭原则。
- 简化客户端代码:客户端无需了解对象的具体类型,只需调用统一的方法。
- 缺点:
- 设计复杂性:组合模式引入了更多的类和接口,可能会增加系统的复杂性。
- 过度通用:有时组合模式可能会导致设计过于通用,使得系统难以理解和维护。
- 举例:文件系统中可能会有文件和文件夹两种类型的对象,但是我们想提供一个方法来统一显示文件和文件夹的层次结构而无需区分他们具体的类型
// 组件接口
class FileSystemComponent {
constructor(name) {
this.name = name;
}
add(component) {
throw new Error("Unsupported operation");
}
remove(component) {
throw new Error("Unsupported operation");
}
display(indent) {
throw new Error("Unsupported operation");
}
}
// 叶子节点:文件
class File extends FileSystemComponent {
display(indent = 0) {
console.log(" ".repeat(indent) + this.name);
}
}
// 组合节点:文件夹
class Folder extends FileSystemComponent {
constructor(name) {
super(name);
this.children = [];
}
add(component) {
this.children.push(component);
}
remove(component) {
const index = this.children.indexOf(component);
if (index !== -1) {
this.children.splice(index, 1);
}
}
display(indent = 0) {
console.log(" ".repeat(indent) + this.name);
this.children.forEach((child) => child.display(indent + 2));
}
}
// 客户端代码
const root = new Folder("Root");
const folder1 = new Folder("Folder1");
const folder2 = new Folder("Folder2");
const file1 = new File("File1");
const file2 = new File("File2");
const file3 = new File("File3");
root.add(folder1);
root.add(folder2);
folder1.add(file1);
folder2.add(file2);
folder2.add(file3);
root.display();
享元模式 Flyweight Pattern
- 目的:通过共享对象来减少内存占用,用于优化性能和内存使用,特别是在处理大量相似对象时。
- 特点:享元模式将对象的内部状态(intrinsic state)和外部状态(extrinsic state)分离,内部状态是可以共享的,而外部状态是每个对象特有的。
- 优点:
- 减少内存使用:通过共享内部状态,减少重复数据占用的内存。
- 提高性能:特别是在处理大量对象时,可以显著提高性能。
- 简化对象创建:通过享元工厂管理对象的创建和共享,简化客户端代码。
- 缺点:
- 复杂性增加:需要分离内部状态和外部状态,增加了设计和实现的复杂性。
- 性能开销:在查找和维护共享对象时可能引入额外的性能开销。
- 示例:假设我们有一个图形绘制系统,其中需要绘制大量的圆形和矩形。我们可以使用享元模式来减少内存占用,因为圆形和矩形的内部状态(如颜色、大小等)是可以共享的,而外部状态(如位置、旋转角度等)是每个对象特有的。
// 享元类:图形样式
class ShapeStyle {
constructor(color, size) {
this.color = color;
this.size = size;
}
}
// 享元工厂:管理图形样式
class ShapeStyleFactory {
constructor() {
this.styles = {};
}
getStyle(color, size) {
const key = `${color}-${size}`;
if (!this.styles[key]) {
this.styles[key] = new ShapeStyle(color, size);
}
return this.styles[key];
}
}
// 具体类:圆形
class Circle {
constructor(x, y, rotation, style) {
this.x = x; // 外部状态
this.y = y; // 外部状态
this.rotation = rotation; // 外部状态
this.style = style; // 内部状态(共享)
}
draw() {
console.log(
`Drawing Circle at (${this.x}, ${this.y}), rotation: ${this.rotation}, color: ${this.style.color}, size: ${this.style.size}`
);
}
}
// 具体类:矩形
class Rectangle {
constructor(x, y, rotation, style) {
this.x = x; // 外部状态
this.y = y; // 外部状态
this.rotation = rotation; // 外部状态
this.style = style; // 内部状态(共享)
}
draw() {
console.log(
`Drawing Rectangle at (${this.x}, ${this.y}), rotation: ${this.rotation}, color: ${this.style.color}, size: ${this.style.size}`
);
}
}
// 使用示例
const styleFactory = new ShapeStyleFactory();
const style1 = styleFactory.getStyle("red", 10);
const style2 = styleFactory.getStyle("blue", 20);
// 可以减少重复样式对象(内部状态)的创建,从而节省内存。
const circle1 = new Circle(10, 20, 0, style1);
const circle2 = new Circle(30, 40, 45, style1);
const rectangle1 = new Rectangle(50, 60, 90, style2);
circle1.draw(); // 输出: Drawing Circle at (10, 20), rotation: 0, color: red, size: 10
circle2.draw(); // 输出: Drawing Circle at (30, 40), rotation: 45, color: red, size: 10
rectangle1.draw(); // 输出: Drawing Rectangle at (50, 60), rotation: 90, color: blue, size: 20
行为型模式 Behavioral Patterns
策略模式 Strategy Pattern
- 目的:目的是定义一系列算法,并将每个算法封装起来,使它们可以互换。策略模式让算法独立于使用它的客户端而变化。
- 优点:
- 代码复用:策略模式将算法的实现细节封装在独立的类中,便于复用和维护。
- 扩展性:新增策略只需添加新的策略类,不影响现有代码,符合开闭原则。
- 单一职责原则:每个策略类只负责一个具体的算法,符合单一职责原则。
- 缺点:
- 复杂性增加:引入多个策略类可能会增加系统的复杂性,需要管理更多的类。
- 客户端负担:客户端需要了解不同的策略类,并负责选择合适的策略,增加了客户端的负担。
- 场景:避免条件语句、动态切换算法
const strategies = {
high: function (workHours) {
return workHours * 25;
},
middle: function (workHours) {
return workHours * 20;
},
low: function (workHours) {
return workHours * 15;
},
};
const calculateSalary = function (workerLevel, workHours) {
return strategies[workerLevel](workHours);
};
console.log(calculateSalary("high", 10)); // 250
console.log(calculateSalary("middle", 10)); // 200
模板方法模式 Template Method Pattern
- 目的:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
- 优点:
- 代码复用:通过将算法的骨架放在抽象类中,子类可以复用这些代码。
- 扩展性:子类可以重定义算法的某些步骤,而不改变算法的结构。
- 符合开闭原则:对扩展开放,对修改关闭。
- 缺点:
- 类的数量增加:每个具体实现都需要一个子类,可能会导致类的数量增加。
- 复杂性增加:对于简单的算法,使用模板方法模式可能会增加不必要的复杂性。
- 示例:假设我们有一个在线订单处理系统,不同的订单类型(如普通订单、会员订单)有不同的处理步骤,但整体流程是相似的。
// 抽象类:订单处理模板
class OrderProcessing {
processOrder() {
this.validateOrder();
this.calculateTotal();
this.applyDiscount();
this.sendConfirmation();
}
validateOrder() {
console.log("Validating order...");
}
calculateTotal() {
console.log("Calculating total...");
}
applyDiscount() {} // 抽象方法,具体实现由子类提供
sendConfirmation() {
console.log("Sending confirmation...");
}
}
// 具体类:普通订单
class RegularOrder extends OrderProcessing {
applyDiscount() {
console.log("No discount applied for regular order.");
}
}
// 具体类:会员订单
class MemberOrder extends OrderProcessing {
applyDiscount() {
console.log("Applying member discount...");
}
}
// 使用示例
const regularOrder = new RegularOrder();
regularOrder.processOrder();
// 输出:
// Validating order...
// Calculating total...
// No discount applied for regular order.
// Sending confirmation...
const memberOrder = new MemberOrder();
memberOrder.processOrder();
// 输出:
// Validating order...
// Calculating total...
// Applying member discount...
// Sending confirmation...
建造者模式和模板方法模式的区别
- 模板方法模式:定义算法骨架,子类实现具体步骤。
- 建造者模式:分离复杂对象的构建和表示,通过不同的构建步骤创建不同的对象。
- 模板类实际上内部用来构建最终产物的方法就将建造者模式的指挥者的功能给实现了
观察者模式 Observer Pattern
被观察者需要维护一个队列来存储所有的观察者,当被观察者发生变化时,通知所有的观察者
// 创建一个全局的事件总线作为主题对象
const eventBus = {
observers: {},
subscribe: function(eventName, observer) {
if (!this.observers[eventName]) {
this.observers[event] = [];
}
this.observers[eventName].push(observer);
},
notify: function(eventName, data) {
if (thiservers[eventName]) {
this.observers[event].forEach(observer => observer.update(data));
}
}
};
// 创建两个模块作为观察者对象
const module1 = {
update: function(data) {
console.log('Module 1 received data: ' data);
// 执行相应的操作
}
};
const module2 = {
update: function(data) {
console.log('Module 2 received data: ' + data);
// 执行相应的操作
}
};
// 订阅事件并在事件发生时收到通知
eventBus.subscribe('dataChange', module1);
eventBus.subscribe('dataChange', module2);
eventBus.notify('dataChange', 'Some new data has arrived!');
发布订阅模式 Publish-Subscribe Pattern
在 DOM 上绑定事件实际上就是发布订阅模式的实践,因此可以利用这个来解决异步顺序问题
let finish = new Event("finish");
function fnA() {
setTimeout(() => {
console.log("请求A完成");
window.dispatchEvent(finish);
}, 1000);
}
function fnB() {
setTimeout(() => {
console.log("请求B完成");
}, 500);
}
fnA();
window.addEventListener("finish", () => {
fnB();
});
手写发布订阅模式代码
// 发布订阅中心
class PubSub {
constructor() {
this.messages = {};
this.listeners = {};
}
publish(type, content) {
const existContent = this.messages[type];
if (!existContent) {
this.messages[type] = [];
}
this.messages[type].push(content);
}
subscribe(type, cb) {
const existListener = this.listeners[type];
if (!existListener) {
this.listeners[type] = [];
}
this.listeners[type].push(cb);
}
notify(type) {
const messages = this.messages[type];
const subscribers = this.listeners[type] || [];
subscribers.forEach((cb) => cb(messages));
}
}
// 发布者
class Publisher {
constructor(name, context) {
this.name = name;
this.context = context;
}
publish(type, content) {
this.context.publish(type, content);
}
}
// 订阅者
class Subscriber {
constructor(name, context) {
this.name = name;
this.context = context;
}
subscribe(type, cb) {
this.context.subscribe(type, cb);
}
}
// 使用
const TYPE_A = "music";
const TYPE_B = "movie";
const TYPE_C = "novel";
const pubsub = new PubSub();
const publisherA = new Publisher("publisherA", pubsub);
publisherA.publish(TYPE_A, "we are young");
publisherA.publish(TYPE_B, "the silicon valley");
const publisherB = new Publisher("publisherB", pubsub);
publisherB.publish(TYPE_A, "stronger");
const publisherC = new Publisher("publisherC", pubsub);
publisherC.publish(TYPE_B, "imitation game");
const subscriberA = new Subscriber("subscriberA", pubsub);
subscriberA.subscribe(TYPE_A, (res) => {
console.log("subscriberA received", res);
});
const subscriberB = new Subscriber("subscriberB", pubsub);
subscriberB.subscribe(TYPE_C, (res) => {
console.log("subscriberB received", res);
});
const subscriberC = new Subscriber("subscriberC", pubsub);
subscriberC.subscribe(TYPE_B, (res) => {
console.log("subscriberC received", res);
});
pubsub.notify(TYPE_A);
pubsub.notify(TYPE_B);
pubsub.notify(TYPE_C);
// 可以看到发布订阅中心实际上维护了两个对象,一个订阅者对象和消息对象,每个对象会以消息类型为键,以对应的实例数组为值,
发布订阅模式&观察者模式区别
- 发布订阅模式的发布者和订阅者之间是解耦的,双方并不知道对方的存在,而观察者模式的发布者和订阅者之间是耦合的,双方都知道对方的存在
- 观察者模式是松耦合的,因为被观察者还有需要通知观察者的职责
发布订阅模式的缺点
- 在某些情况下,由于消息传递是异步的,发布者发布消息的顺序与订阅者接收消息的顺序可能会不一致。这可能导致一些潜在的问题,特别是对于依赖于消息顺序的场景
- 由于发布-订阅模式通常是异步的,消息的传递和处理可能会引入一定程度的延迟。在某些实时性要求高的应用场景中,这可能会成为一个问题。
- 如果不加限制地使用发布-订阅模式,可能会导致系统中存在过多的订阅者,这可能会降低系统的性能和可维护性。因此,需要在设计时仔细考虑订阅者的数量和范围。
发布订阅模式的优点
- 于发布者和订阅者之间的解耦,系统可以更容易地扩展。新的发布者或订阅者可以被添加而不影响现有的组件。
- 发布-订阅模式允许任意数量的发布者和订阅者存在,并且支持多对多的通信。发布者和订阅者可以根据需求动态地添加、删除或修改,而不影响整个系统的运行。
- 由于发布者和订阅者之间的通信通常是通过消息队列或事件总线进行的,因此支持异步通信。这使得系统能够更高效地处理大量消息,并提高了响应性。
迭代器模式 Iterator Pattern
- 目的:提供了一种方法来顺序访问一个聚合对象(如数组、列表等)中的各个元素,而无需暴露其内部表示。将遍历聚合对象的责任从聚合对象中分离出来,使得聚合对象和遍历算法可以独立变化。
- 优点:
- 单一职责原则:将遍历算法从聚合对象中分离出来,使得每个类的职责更加明确。
- 开闭原则:易于添加新的遍历算法,而无需修改现有的聚合对象。
- 简化聚合对象:聚合对象无需关心遍历的实现细节,只需提供迭代器接口。
- 缺点:
- 类的数量增加:每个聚合对象都需要一个对应的迭代器类,可能会导致类的数量增加。
- 性能问题:对于某些特殊的数据结构,使用迭代器可能会引入额外的性能开销。
- 示例:利用迭代模式自定义迭代器来遍历自定义的集合对象
// 迭代器接口
class Iterator {
hasNext() {}
next() {}
}
// 具体迭代器
class ArrayIterator extends Iterator {
constructor(array) {
super();
this.array = array;
this.index = 0;
}
hasNext() {
return this.index < this.array.length;
}
next() {
if (this.hasNext()) {
return this.array[this.index++];
}
return null;
}
}
// 聚合对象
class CustomCollection {
constructor() {
this.items = [];
}
addItem(item) {
this.items.push(item);
}
createIterator() {
return new ArrayIterator(this.items);
}
}
// 使用示例
const collection = new CustomCollection();
collection.addItem("Item 1");
collection.addItem("Item 2");
collection.addItem("Item 3");
const iterator = collection.createIterator();
while (iterator.hasNext()) {
console.log(iterator.next());
}
// 输出:
// Item 1
// Item 2
// Item 3
职责链模式 Chain of Responsibility Pattern
- 目的:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。会将这些对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它为止。
- 优点
- 解耦:发送者和接收者解耦,发送者不需要知道哪个对象会处理请求。
- 灵活性:可以动态地添加或删除处理请求的对象。
- 扩展性:易于添加新的处理者,符合开闭原则。
- 缺点:
- 性能问题:请求可能需要经过多个处理者才能被处理,可能会引入性能开销。
- 不确定性:不能保证请求一定会被处理,可能会有请求未被处理的情况。
- 示例:下面是一个简单的职责链模式的示例,展示了如何使用职责链处理不同类型的请求。
// 处理者接口
class Handler {
setNext(handler) {
this.nextHandler = handler;
}
handle(request) {
if (this.nextHandler) {
return this.nextHandler.handle(request);
}
// 兜底
return null;
}
}
// 具体处理者:经理
class ManagerHandler extends Handler {
handle(request) {
if (request === "low") {
return `Manager handled ${request} priority request`;
} else {
return super.handle(request);
}
}
}
// 具体处理者:总监
class DirectorHandler extends Handler {
handle(request) {
if (request === "medium") {
return `Director handled ${request} priority request`;
} else {
return super.handle(request);
}
}
}
// 具体处理者:CEO
class CEOHandler extends Handler {
handle(request) {
if (request === "high") {
return `CEO handled ${request} priority request`;
} else {
return super.handle(request);
}
}
}
// 使用示例
const manager = new ManagerHandler();
const director = new DirectorHandler();
const ceo = new CEOHandler();
manager.setNext(director);
director.setNext(ceo);
console.log(manager.handle("low")); // 输出: Manager handled low priority request
console.log(manager.handle("medium")); // 输出: Director handled medium priority request
console.log(manager.handle("high")); // 输出: CEO handled high priority request
命令模式 Command Pattern
- 目的: 命令模式的主要目的是将请求的发送者和接收者解耦,从而使系统更灵活、可扩展和易于维护。它允许请求在不同的上下文中被复用,并且支持请求的排队、记录和撤销。
- 优点:
- 解耦发送者和接收者:发送者无需知道接收者的具体实现,只需通过命令对象进行交互。
- 易于扩展:可以轻松添加新的命令,而无需修改现有代码。
- 支持撤销和重做:命令对象可以实现撤销和重做操作。
- 支持队列和日志:命令可以被排队执行,也可以记录日志以便后续分析。
- 缺点:
- 增加复杂性:引入了更多的类和对象,可能会增加系统的复杂性。
- 性能开销:由于每个命令都需要封装成对象,可能会引入一定的性能开销。
- 结构:调用者通过接收命令实例来调用命令的执行方法,命令实例中包含了接收者和具体的操作方法。
- 示例:假设我们有一个文本编辑器,可以执行不同的文本操作(如插入、删除、撤销)。
// 命令接口
class Command {
execute() {
throw new Error("Unsupported operation");
}
undo() {
throw new Error("Unsupported operation");
}
}
// 具体命令:插入文本
class InsertTextCommand extends Command {
constructor(editor, text, position) {
super();
this.editor = editor;
this.text = text;
this.position = position;
}
execute() {
this.editor.insertText(this.text, this.position);
}
undo() {
this.editor.deleteText(this.position, this.text.length);
}
}
// 具体命令:删除文本
class DeleteTextCommand extends Command {
constructor(editor, position, length) {
super();
this.editor = editor;
this.position = position;
this.length = length;
this.deletedText = null;
}
execute() {
this.deletedText = this.editor
.getText()
.substring(this.position, this.position + this.length);
this.editor.deleteText(this.position, this.length);
}
undo() {
if (this.deletedText !== null) {
this.editor.insertText(this.deletedText, this.position);
}
}
}
// 接收者:文本编辑器
class TextEditor {
constructor() {
this.text = "";
}
getText() {
return this.text;
}
insertText(text, position) {
this.text = this.text.slice(0, position) + text + this.text.slice(position);
console.log(`Inserted "${text}" at position ${position}: "${this.text}"`);
}
deleteText(position, length) {
const deletedText = this.text.substring(position, position + length);
this.text =
this.text.slice(0, position) + this.text.slice(position + length);
console.log(
`Deleted "${deletedText}" from position ${position}: "${this.text}"`
);
}
}
// 调用者:命令管理器
class CommandManager {
constructor() {
this.history = [];
}
execute(command) {
command.execute();
this.history.push(command);
}
undo() {
const command = this.history.pop();
if (command) {
command.undo();
}
}
}
// 客户端代码
const editor = new TextEditor();
const commandManager = new CommandManager();
const insertCommand1 = new InsertTextCommand(editor, "Hello, ", 0);
const insertCommand2 = new InsertTextCommand(editor, "world!", 7);
const deleteCommand = new DeleteTextCommand(editor, 5, 1);
commandManager.execute(insertCommand1); // 输出: Inserted "Hello, " at position 0: "Hello, "
commandManager.execute(insertCommand2); // 输出: Inserted "world!" at position 7: "Hello, world!"
commandManager.execute(deleteCommand); // 输出: Deleted "o" from position 5: "Hell, world!"
commandManager.undo(); // 输出: Inserted "o" at position 5: "Hello, world!"
commandManager.undo(); // 输出: Deleted "world!" from position 7: "Hello, "
commandManager.undo(); // 输出: Deleted "Hello, " from position 0: ""
备忘录模式
- 目的:用于保存和恢复对象的内部状态,而不破坏其封装性。它通过创建一个备忘录对象来存储对象的当前状态,并在需要时恢复该状态。
- 优点:
- 保持封装性:备忘录模式允许在不暴露对象内部结构的情况下保存和恢复状态。
- 简化原发器:原发器(Originator)不需要自己管理状态的保存和恢复,而是通过备忘录对象来处理。
- 缺点:
- 内存消耗:如果需要保存大量状态,备忘录对象可能会占用较多内存。
- 性能问题:频繁的状态保存和恢复可能会影响性能。
- 示例:
// 备忘录类:保存文本状态
class TextMemento {
constructor(content) {
this.content = content;
}
getContent() {
return this.content;
}
}
// 原发器类:文本编辑器
class TextEditor {
constructor() {
this.content = "";
}
setContent(content) {
this.content = content;
}
getContent() {
return this.content;
}
createMemento() {
return new TextMemento(this.content);
}
restoreMemento(memento) {
this.content = memento.getContent();
}
}
// 管理者类:管理备忘录
class Caretaker {
constructor() {
this.mementos = [];
}
addMemento(memento) {
this.mementos.push(memento);
}
getMemento(index) {
return this.mementos[index];
}
}
// 使用示例
const editor = new TextEditor();
const caretaker = new Caretaker();
editor.setContent("Hello, World!");
caretaker.addMemento(editor.createMemento());
editor.setContent("Hello, Universe!");
caretaker.addMemento(editor.createMemento());
console.log(editor.getContent()); // 输出: Hello, Universe!
editor.restoreMemento(caretaker.getMemento(0));
console.log(editor.getContent()); // 输出: Hello, World!
状态模式 State Pattern
- 目的:将对象状态与实际行为进行分离,提高代码的可复用性和可扩展性,并允许对象在运行时根据其内部状态改变行为,而不需要使用大量的条件语句。
- 优点:
- 代码清晰:每个状态的行为被封装在独立的类中,代码结构清晰,易于理解和维护。
- 易于扩展:新增状态只需添加新的状态类,符合开闭原则。
- 缺点:
- 类膨胀:每个状态都需要一个对应的类,可能会导致类的数量增加。
- 复杂性增加:对于简单的状态切换,使用状态模式可能会增加不必要的复杂性。
- 示例:假设我们有一个电灯开关,它有两种状态:开(On)和关(Off)。我们可以使用状态模式来实现电灯开关的状态切换。
// 上下文类需要持有状态类,并在适合的时候给予上下文类正确的状态来切换状态方便实现对应的实际操作
// 状态接口
class State {
handle() {}
}
// 具体状态:开
class OnState extends State {
constructor(light) {
super();
this.light = light;
}
handle() {
console.log("Light is ON");
this.light.setState(this.light.offState);
}
}
// 具体状态:关
class OffState extends State {
constructor(light) {
super();
this.light = light;
}
handle() {
console.log("Light is OFF");
this.light.setState(this.light.onState);
}
}
// 上下文类:电灯
class Light {
constructor() {
this.onState = new OnState(this);
this.offState = new OffState(this);
this.currentState = this.offState; // 初始状态为关
}
setState(state) {
this.currentState = state;
}
pressSwitch() {
this.currentState.handle();
}
}
// 使用示例
const light = new Light();
light.pressSwitch(); // 输出: Light is OFF
light.pressSwitch(); // 输出: Light is ON
light.pressSwitch(); // 输出: Light is OFF
访问器模式 Visitor Pattern
- 目的:将数据结构和对数据结构的操作分离,使得两者可以独立变化。
- 优点:
- 易于添加新操作:通过实现新的访问者类,可以很容易地添加新的操作。
- 集中相关操作:将相关操作集中在一个访问者类中,便于管理和维护。
- 缺点:
- 增加复杂性:引入访问者模式会增加系统的复杂性,特别是对于简单的数据结构和操作。
- 难以支持所有元素:如果数据结构中的类经常变化,那么为每个新类添加访问者接口可能会变得繁琐。
- 示例:假设我们有一个图形库,包含圆形(Circle)和矩形(Rectangle)两种图形。我们希望在不修改图形类的情况下,为这些图形添加计算面积和周长的功能。
// 元素接口
class Shape {
accept(visitor) {}
}
// 具体元素:圆形
class Circle extends Shape {
constructor(radius) {
super();
this.radius = radius;
}
accept(visitor) {
visitor.visitCircle(this);
}
}
// 具体元素:矩形
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
accept(visitor) {
visitor.visitRectangle(this);
}
}
// 访问者接口
class Visitor {
visitCircle(circle) {}
visitRectangle(rectangle) {}
}
// 具体访问者:计算面积
class AreaVisitor extends Visitor {
visitCircle(circle) {
const area = Math.PI * circle.radius * circle.radius;
console.log(`Circle area: ${area}`);
}
visitRectangle(rectangle) {
const area = rectangle.width * rectangle.height;
console.log(`Rectangle area: ${area}`);
}
}
// 具体访问者:计算周长
class PerimeterVisitor extends Visitor {
visitCircle(circle) {
const perimeter = 2 * Math.PI * circle.radius;
console.log(`Circle perimeter: ${perimeter}`);
}
visitRectangle(rectangle) {
const perimeter = 2 * (rectangle.width + rectangle.height);
console.log(`Rectangle perimeter: ${perimeter}`);
}
}
// 使用示例
const shapes = [new Circle(5), new Rectangle(4, 6)];
const areaVisitor = new AreaVisitor();
const perimeterVisitor = new PerimeterVisitor();
shapes.forEach((shape) => {
shape.accept(areaVisitor);
shape.accept(perimeterVisitor);
});
// 输出:
// Circle area: 78.53981633974483
// Circle perimeter: 31.41592653589793
// Rectangle area: 24
// Rectangle perimeter: 20
访问器模式和装饰器模式区别
访问者模式主要用于分离数据结构和操作,而装饰器模式主要用于动态地扩展对象的功能。两者解决的问题和应用场景是不同的。
文章内容较多,收藏起来慢慢看~
谢谢您的鼓励!