设计模式是一种被广泛应用于软件开发中的经验总结,它是对软件设计中常见问题的解决方案的描述。设计模式提供了一种通用的解决方案,可以帮助开发人员在软件开发过程中更加高效地解决问题,提高软件的可维护性、可扩展性和可重用性。
设计模式通常包括三个要素:模式名称、问题描述和解决方案。模式名称是对该模式的简单描述,问题描述是该模式所解决的问题的描述,解决方案则是该模式的具体实现方式。设计模式通常被分为三类:创建型模式、结构型模式和行为型模式。
创建型模式:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式等。这些模式主要关注如何创建对象,以及如何避免重复创建对象,提高系统的灵活性和可扩展性。
结构型模式:适配器模式、桥接模式、装饰器模式、组合模式、外观模式、享元模式、代理模式等。这些模式主要关注如何组合类和对象以形成更大的结构,以及如何简化类和对象之间的交互。
行为型模式:模板方法模式、策略模式、命令模式、职责链模式、状态模式、观察者模式、中介者模式、备忘录模式、访问者模式、解释器模式等。这些模式主要关注对象之间的交互和职责分配,以及如何在系统中实现松耦合和高内聚。
创建型
单例模式
概念
单例模式(Singleton Pattern)是一种创建型设计模式,用于限制一个类只能创建一个实例的模式。它确保全局只有一个实例,并提供全局访问点。
单例模式通常包括一个私有的构造函数和一个静态的实例变量,以及一个静态的访问方法。在访问方法中,如果实例变量不存在,则创建一个新的实例并返回,否则直接返回现有的实例。
单例模式的优点包括:
- 提供全局访问点,方便了对象的访问和管理。
- 可以控制对象的创建过程,例如可以延迟对象的创建时间,或者根据需要动态地创建对象。
单例模式的缺点包括:
- 单例模式可能会导致代码的耦合度增加,因为它将对象的创建和管理都集中在一个类中。
- 单例模式可能会导致代码的可测试性降低,因为它难以模拟和替换单例对象。
使用场景
- 需要确保全局只有一个实例的场景,例如全局的配置对象、全局的日志对象等。
- 需要提供全局访问点的场景,例如全局的缓存对象、全局的数据库连接对象等。
- 需要控制对象的创建过程的场景,例如需要延迟对象的创建时间、需要根据需要动态地创建对象等。
- 需要避免重复创建对象的场景,例如创建对象的开销较大、创建对象的数量较多等。
总之,单例模式适用于需要确保全局只有一个实例,并提供全局访问点的场景。它可以帮助我们控制对象的创建过程,避免重复创建对象,提高代码的性能和可维护性。
代码案例
class Singleton {
private static instance: Singleton;
private constructor() {}
public static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
public doSomething(): void {
console.log("Doing something...");
}
}
// 使用示例
const singleton1 = Singleton.getInstance();
const singleton2 = Singleton.getInstance();
console.log(singleton1 === singleton2); // true
singleton1.doSomething(); // Doing something...
使用单例模式来创建Redis客户端,以确保全局只有一个Redis客户端实例,并提供全局访问点。以下是一个示例:
import redis from 'redis';
type RedisClientConstructor = new (...args: any[]) => redis.RedisClient;
function createRedisClientSingleton(Constructor: RedisClientConstructor): redis.RedisClient {
let instance: redis.RedisClient;
return function (this: any, ...args: any[]) {
if (!instance) {
instance = new Constructor(...args);
}
return instance;
} as any;
}
// 创建Redis客户端单例
const RedisClientSingleton = createRedisClientSingleton(redis.createClient);
// 客户端代码
const client1 = RedisClientSingleton();
const client2 = RedisClientSingleton();
console.log(client1 === client2); // 输出 true
结构性
适配器模式
概念
适配器模式是一种结构型设计模式,它允许将不兼容的对象包装在适配器中,以便它们可以共同工作。适配器模式通常用于将现有的类或接口转换为另一个接口,以便客户端代码可以使用它们而不需要修改其代码。
适配器模式包括以下几个角色:
- 目标接口(Target):客户端代码期望的接口,它定义了客户端代码可以使用的方法。
- 适配器(Adapter):将不兼容的对象包装在适配器中,以便它们可以共同工作。适配器实现了目标接口,并将客户端代码的请求转换为被适配对象的请求。
- 被适配对象(Adaptee):需要被适配的对象,它定义了客户端代码不能直接使用的方法。
适配器模式的优点包括:
- 可以将现有的类或接口转换为另一个接口,以便客户端代码可以使用它们而不需要修改其代码。
- 可以提高代码的复用性和可维护性,因为它可以将不同的类或接口组合在一起,以实现新的功能。
- 可以提高代码的灵活性和可扩展性,因为它可以在不修改现有代码的情况下添加新的功能。
适配器模式的缺点包括:
- 可能会增加代码的复杂性,因为它需要引入新的类或接口来实现适配器。
- 可能会影响代码的性能,因为它需要进行额外的转换和处理。
总之,适配器模式是一种非常有用的设计模式,它可以将不兼容的对象包装在适配器中,以便它们可以共同工作。它可以提高代码的复用性、可维护性、灵活性和可扩展性,但也可能会增加代码的复杂性和影响代码的性能。
使用场景
适配器模式的使用场景包括:
- 将一个类的接口转换成客户端所期望的另一个接口。例如,当一个类的接口与另一个类的接口不兼容时,可以使用适配器模式将其转换为可兼容的接口。
- 在不修改已有代码的情况下,使得已有的类能够与其他类一起工作。例如,当需要使用一个已经存在的类,但是它的接口与系统的其他部分不兼容时,可以使用适配器模式来适配该类。
- 将多个类的接口统一成一个接口。例如,当需要使用多个类的功能,但是它们的接口不一致时,可以使用适配器模式将它们的接口统一成一个接口。
- 在系统中引入一个新的类,以与已有的类协同工作。例如,当需要引入一个新的类来与已有的类一起工作,但是它们的接口不兼容时,可以使用适配器模式来适配新的类。
总之,适配器模式适用于需要将不兼容的接口转换为可兼容的接口的情况,以实现不同类之间的协同工作。
代码示例
interface Rectangle {
setWidth(width: number): void;
setHeight(height: number): void;
getArea(): number;
}
class LegacyRectangle {
constructor(private width: number, private height: number) {}
getWidth(): number {
return this.width;
}
getHeight(): number {
return this.height;
}
}
class RectangleAdapter implements Rectangle {
private legacyRectangle: LegacyRectangle;
constructor(width: number, height: number) {
this.legacyRectangle = new LegacyRectangle(width, height);
}
setWidth(width: number) {
this.legacyRectangle = new LegacyRectangle(width, this.legacyRectangle.getHeight());
}
setHeight(height: number) {
this.legacyRectangle = new LegacyRectangle(this.legacyRectangle.getWidth(), height);
}
getArea(): number {
return this.legacyRectangle.getWidth() * this.legacyRectangle.getHeight();
}
}
// 客户端代码
let rectangle: Rectangle = new RectangleAdapter(10, 20);
console.log(rectangle.getArea()); // 输出 200
rectangle.setWidth(5);
console.log(rectangle.getArea()); // 输出 100
rectangle.setHeight(10);
console.log(rectangle.getArea()); // 输出 50
组合模式
概念
组合模式是一种结构型设计模式,它将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
组合模式包含以下角色:
- 抽象构件(Component):定义组合中所有对象的通用接口,可以是抽象类或接口。抽象构件中定义了一些通用的操作,例如添加子节点、删除子节点、获取子节点等。
- 叶子构件(Leaf):表示组合中的叶子节点对象,叶子节点没有子节点。
- 组合构件(Composite):表示组合中的非叶子节点对象,组合节点包含一个或多个子节点,子节点可以是叶子节点或组合节点。
在组合模式中,叶子节点和组合节点都实现了抽象构件中定义的通用接口,这样用户就可以对单个对象和组合对象进行统一处理。组合模式使得用户可以忽略对象与组合对象之间的差异,从而更加方便地进行操作。同时,组合模式也提高了系统的灵活性和可扩展性,因为可以很容易地添加新的叶子节点或组合节点。
应用场景
- 树形结构:当需要处理树形结构数据时,可以使用组合模式来实现。例如文件系统、菜单系统等。
- 部分-整体关系:当需要处理部分-整体关系时,可以使用组合模式来实现。例如汽车由引擎、车轮、座椅等部件组成,可以使用组合模式来表示。
- 统一接口:当需要对单个对象和组合对象进行统一处理时,可以使用组合模式来实现。例如对于文件系统中的文件和文件夹,我们可以使用相同的接口来进行操作。
递归结构:当需要处理递归结构时,可以使用组合模式来实现。例如 HTML 文档中的 DOM 树结构,可以使用组合模式来表示。
代码示例
文件系统的简单示例
// 抽象节点类
abstract class Node {
protected name: string;
constructor(name: string) {
this.name = name;
}
// 打印节点信息
public abstract print(): void;
// 获取节点名称
public getName(): string {
return this.name;
}
}
// 叶子节点类
class File extends Node {
constructor(name: string) {
super(name);
}
// 打印文件信息
public print(): void {
console.log(`File: ${this.name}`);
}
}
// 组合节点类
class Folder extends Node {
private children: Node[] = [];
constructor(name: string) {
super(name);
}
// 添加子节点
public add(node: Node): void {
this.children.push(node);
}
// 删除子节点
public remove(node: Node): void {
const index = this.children.indexOf(node);
if (index !== -1) {
this.children.splice(index, 1);
}
}
// 打印文件夹信息及其子节点信息
public print(): void {
console.log(`Folder: ${this.name}`);
for (const node of this.children) {
node.print();
}
}
}
// 使用示例
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");
root.add(folder1);
root.add(folder2);
folder1.add(file1);
folder2.add(file2);
root.print();
菜单系统的简单示例
// 抽象菜单项类
abstract class MenuItem {
protected name: string;
constructor(name: string) {
this.name = name;
}
// 打印菜单项信息
public abstract print(): void;
// 获取菜单项名称
public getName(): string {
return this.name;
}
}
// 菜单项类
class MenuLeaf extends MenuItem {
constructor(name: string) {
super(name);
}
// 打印菜单项信息
public print(): void {
console.log(`MenuLeaf: ${this.name}`);
}
}
// 菜单类
class MenuComposite extends MenuItem {
private children: MenuItem[] = [];
constructor(name: string) {
super(name);
}
// 添加子菜单或菜单项
public add(item: MenuItem): void {
this.children.push(item);
}
// 删除子菜单或菜单项
public remove(item: MenuItem): void {
const index = this.children.indexOf(item);
if (index !== -1) {
this.children.splice(index, 1);
}
}
// 打印菜单信息及其子菜单信息
public print(): void {
console.log(`MenuComposite: ${this.name}`);
for (const item of this.children) {
item.print();
}
}
}
// 使用示例
const rootMenu = new MenuComposite("root");
const fileMenu = new MenuComposite("file");
const editMenu = new MenuComposite("edit");
const openItem = new MenuLeaf("open");
const saveItem = new MenuLeaf("save");
const copyItem = new MenuLeaf("copy");
const pasteItem = new MenuLeaf("paste");
rootMenu.add(fileMenu);
rootMenu.add(editMenu);
fileMenu.add(openItem);
fileMenu.add(saveItem);
editMenu.add(copyItem);
editMenu.add(pasteItem);
rootMenu.print();
外观模式
概念
外观模式是一种结构型设计模式,它为子系统中的一组接口提供一个统一的高层接口,从而简化了子系统的使用。
外观模式包含以下角色:
- 外观(Facade):为子系统中的一组接口提供一个统一的高层接口,外观模式中只有一个外观类。
- 子系统(Subsystem):实现子系统的功能,子系统中可以包含多个类。
在外观模式中,外观类封装了子系统中的一组接口,为客户端提供了一个简单的接口。客户端只需要与外观类进行交互,而不需要直接与子系统中的类进行交互。外观类将客户端的请求转发给子系统中的类进行处理,从而实现了客户端与子系统之间的解耦。
使用场景
外观模式的使用场景包括:
- 简化复杂接口:当子系统中的接口比较复杂时,可以使用外观模式来简化接口。外观类可以封装子系统中的一组接口,为客户端提供一个简单的接口。
- 解耦客户端和子系统:当客户端需要与多个子系统进行交互时,可以使用外观模式来解耦客户端和子系统。外观类可以封装多个子系统的接口,从而简化客户端的使用。
- 提高安全性:当需要限制客户端对子系统的访问时,可以使用外观模式来提高系统的安全性。外观类可以限制客户端对子系统的访问,从而保护子系统的安全。
- 统一接口:当需要为多个子系统提供一个统一的接口时,可以使用外观模式来实现。外观类可以封装多个子系统的接口,为客户端提供一个统一的接口。
总之,外观模式适用于需要简化复杂接口、解耦客户端和子系统、提高安全性、统一接口等场景。
代码示例
下面是一个简化复杂接口的实际代码示例。
假设我们要设计一个计算机系统,该系统包含 CPU、内存、硬盘等多个子系统。每个子系统都有自己的接口,客户端需要调用多个接口才能完成一个操作。为了简化客户端的使用,我们可以使用外观模式来封装这些接口,为客户端提供一个简单的接口。
下面是一个简单的示例代码:
// CPU 子系统
class CPU {
public start(): void {
console.log("CPU started");
}
public stop(): void {
console.log("CPU stopped");
}
}
// 内存子系统
class Memory {
public load(): void {
console.log("Memory loaded");
}
public unload(): void {
console.log("Memory unloaded");
}
}
// 硬盘子系统
class HardDrive {
public read(): void {
console.log("Hard drive read");
}
public write(): void {
console.log("Hard drive write");
}
}
// 计算机系统外观类
class Computer {
private cpu: CPU;
private memory: Memory;
private hardDrive: HardDrive;
constructor() {
this.cpu = new CPU();
this.memory = new Memory();
this.hardDrive = new HardDrive();
}
public start(): void {
this.cpu.start();
this.memory.load();
this.hardDrive.read();
}
public stop(): void {
this.cpu.stop();
this.memory.unload();
this.hardDrive.write();
}
}
// 使用示例
const computer = new Computer();
computer.start();
computer.stop();
在这个示例中,外观模式的优点体现在以下几个方面:
- 简化了客户端的使用:客户端只需要创建一个 Computer 对象,并调用它的 start() 和 stop() 方法即可完成一系列操作,而不需要了解 CPU、内存、硬盘等多个子系统的具体实现细节。这样可以大大简化客户端的使用,降低了使用门槛。
- 解耦了客户端和子系统:客户端和子系统之间的解耦使得系统更加灵活,可以方便地进行维护和扩展。如果需要修改子系统的实现,只需要修改子系统类的代码,而不需要修改客户端的代码。这样可以降低系统的耦合度,提高了系统的可维护性和可扩展性。
- 提高了安全性:外观类可以限制客户端对子系统的访问,从而提高了系统的安全性。在这个示例中,客户端只能通过 Computer 类来访问 CPU、内存、硬盘等子系统,而不能直接访问这些子系统的类。这样可以避免客户端对子系统的误操作,提高了系统的稳定性和安全性。
综上所述,外观模式可以简化客户端的使用,解耦客户端和子系统,提高系统的安全性,从而提高了系统的可维护性、可扩展性和可靠性。
享元模式
享元模式是一种结构型设计模式,它通过共享对象来减少内存的使用和提高性能。享元模式将对象分为两种类型:内部状态和外部状态。内部状态是可以共享的,而外部状态是不可以共享的。通过共享内部状态,可以减少内存的使用,提高系统的性能。
享元模式包含以下角色:
- 享元工厂(Flyweight Factory):负责创建和管理享元对象。
- 享元对象(Flyweight):包含内部状态和外部状态,内部状态可以共享,外部状态不可以共享。
- 客户端(Client):使用享元对象的客户端。
使用场景
- 当需要创建大量相似的对象时,可以使用享元模式。享元模式可以共享内部状态,从而减少内存的使用,提高系统的性能。
- 当需要缓存对象时,可以使用享元模式。享元模式可以将对象缓存起来,从而提高系统的效率和性能。
- 当需要对对象进行细粒度控制时,可以使用享元模式。享元模式可以将对象分为内部状态和外部状态,从而实现对对象的细粒度控制。
代码示例
假设我们需要创建大量的文本编辑器对象,并且这些文本编辑器对象的字体和颜色是相同的,那么我们可以使用享元模式来共享字体和颜色对象,从而减少内存的使用。
// 字体类
class Font {
private name: string;
constructor(name: string) {
this.name = name;
}
public getName(): string {
return this.name;
}
}
// 颜色类
class Color {
private name: string;
constructor(name: string) {
this.name = name;
}
public getName(): string {
return this.name;
}
}
// 文本编辑器类
class TextEditor {
private text: string;
private font: Font;
private color: Color;
constructor(text: string, font: Font, color: Color) {
this.text = text;
this.font = font;
this.color = color;
}
public draw(): void {
console.log(`Drawing text "${this.text}" with font ${this.font.getName()} and color ${this.color.getName()}`);
}
}
// 文本编辑器工厂类
class TextEditorFactory {
private fonts: { [key: string]: Font } = {};
private colors: { [key: string]: Color } = {};
public getTextEditor(text: string, fontName: string, colorName: string): TextEditor {
let font = this.fonts[fontName];
if (!font) {
font = new Font(fontName);
this.fonts[fontName] = font;
}
let color = this.colors[colorName];
if (!color) {
color = new Color(colorName);
this.colors[colorName] = color;
}
return new TextEditor(text, font, color);
}
}
// 使用示例
const factory = new TextEditorFactory();
const editor1 = factory.getTextEditor("Hello, world!", "Arial", "red");
const editor2 = factory.getTextEditor("Goodbye, world!", "Arial", "red");
const editor3 = factory.getTextEditor("Hello, world!", "Times New Roman", "blue");
editor1.draw(); // Drawing text "Hello, world!" with font Arial and color red
editor2.draw(); // Drawing text "Goodbye, world!" with font Arial and color red
editor3.draw(); // Drawing text "Hello, world!" with font Times New Roman and color blue
console.log(editor1 === editor2); // false
console.log(editor1 === editor3); // false
在使用示例中,我们创建了一个 TextEditorFactory 对象,并使用它来创建了三个文本编辑器对象。其中,前两个文本编辑器对象使用了相同的字体对象和颜色对象,而第三个文本编辑器对象使用了不同的字体对象和颜色对象。我们可以看到,前两个文本编辑器对象的字体和颜色是相同的,而第三个文本编辑器对象的字体和颜色是不同的。这样,我们就成功地使用享元模式来共享字体和颜色对象,从而减少内存的使用。
// 数据类
class Data {
private id: number;
private value: string;
constructor(id: number, value: string) {
this.id = id;
this.value = value;
}
public getId(): number {
return this.id;
}
public getValue(): string {
return this.value;
}
}
// 数据缓存类
class DataCache {
private cache: { [key: number]: Data } = {};
public getData(id: number): Data {
let data = this.cache[id];
if (!data) {
// 从远程服务器获取数据
const value = `value_${id}`;
data = new Data(id, value);
this.cache[id] = data;
}
return data;
}
}
// 使用示例
const cache = new DataCache();
const data1 = cache.getData(1);
const data2 = cache.getData(2);
const data3 = cache.getData(1);
console.log(data1 === data2); // false
console.log(data1 === data3); // true
在上面示例中,我们创建了一个 DataCache 对象,并使用它来获取了三个数据对象。其中,前两个数据对象的 ID 是不同的,而第三个数据对象的 ID 与第一个数据对象的 ID 相同。我们可以看到,前两个数据对象是不同的,而第三个数据对象与第一个数据对象是相同的。这样,我们就成功地使用缓存对象来缓存已经获取的数据,从而提高系统的效率和性能。
行为设计模式
状态模式
概念
状态模式是一种行为型设计模式,它允许对象在内部状态发生改变时改变它的行为。状态模式将对象的行为封装在不同的状态类中,使得对象在不同的状态下可以有不同的行为。
在状态模式中,有三个主要的角色:
- 上下文(Context):维护一个对当前状态对象的引用,并将所有与状态相关的请求委托给它。
- 状态(State):定义了一个接口,用于封装与上下文的一个特定状态相关的行为。
- 具体状态(Concrete State):实现状态接口,并提供与状态相关的行为。
状态模式的核心思想是将对象的行为封装在不同的状态类中,使得对象在不同的状态下可以有不同的行为。这种模式可以帮助我们实现复杂的状态机,使得代码更加清晰、易于维护。
应用场景
状态模式主要应用于对象的状态转换和行为随状态改变的场景。当一个对象的行为取决于其内部状态,并且需要在运行时根据状态改变行为时,可以使用状态模式。
以下是一些状态模式的应用场景:
- 订单状态:当我们需要实现订单状态的转换和相应的行为时,可以使用状态模式。例如,在电商网站中,订单可以有多种状态,如待支付、待发货、已发货、已完成等,每种状态对应着不同的行为,如支付、发货、确认收货等。
- 线程状态:当我们需要实现线程状态的转换和相应的行为时,可以使用状态模式。例如,在操作系统中,线程可以有多种状态,如就绪、运行、阻塞、挂起等,每种状态对应着不同的行为,如调度、等待、唤醒等。
- 游戏角色状态:当我们需要实现游戏角色状态的转换和相应的行为时,可以使用状态模式。例如,在角色扮演游戏中,角色可以有多种状态,如正常、中毒、昏迷、死亡等,每种状态对应着不同的行为,如攻击、治疗、复活等。
在状态模式中,我们通常会定义一个状态接口或抽象类来表示状态,多个具体状态类来实现状态的不同行为,以及一个上下文类来维护状态对象并调用其方法。当需要改变状态时,我们可以调用上下文对象的方法来改变状态对象,并在状态对象中实现相应的行为。这样,我们可以将状态的转换和行为的实现分离开来,使得代码更加清晰、易于维护。
代码案例
以下是一个电视机遥控器的状态模式的 JavaScript 实现,包括上下文、状态和具体状态三个角色:
// 上下文
class RemoteControl {
constructor() {
this.state = new OffState();
}
// 设置状态
setState(state) {
this.state = state;
}
// 按下电源键
pressPowerButton() {
this.state.pressPowerButton();
}
// 按下音量键
pressVolumeButton() {
this.state.pressVolumeButton();
}
// 按下频道键
pressChannelButton() {
this.state.pressChannelButton();
}
}
// 状态
class State {
pressPowerButton() {}
pressVolumeButton() {}
pressChannelButton() {}
}
// 具体状态:开机状态
class OnState extends State {
pressPowerButton() {
console.log("关闭电视机");
this.context.setState(new OffState());
}
pressVolumeButton() {
console.log("调整音量");
}
pressChannelButton() {
console.log("切换频道");
}
}
// 具体状态:关机状态
class OffState extends State {
pressPowerButton() {
console.log("打开电视机");
this.context.setState(new OnState());
}
pressVolumeButton() {
console.log("电视机已关闭,无法调整音量");
}
pressChannelButton() {
console.log("电视机已关闭,无法切换频道");
}
}
// 使用示例
const remoteControl = new RemoteControl();
// 按下电源键,打开电视机
remoteControl.pressPowerButton();
// 按下音量键,调整音量
remoteControl.pressVolumeButton();
// 按下频道键,切换频道
remoteControl.pressChannelButton();
// 按下电源键,关闭电视机
remoteControl.pressPowerButton();
// 按下音量键,电视机已关闭,无法调整音量
remoteControl.pressVolumeButton();
// 按下频道键,电视机已关闭,无法切换频道
remoteControl.pressChannelButton();
在这个示例中,我们创建了一个 RemoteControl 类来表示遥控器,一个 State 类来表示遥控器的状态,以及两个具体状态类 OnState 和 OffState。当电视机处于开机状态时,我们将遥控器的状态设置为 OnState;当电视机处于关机状态时,我们将遥控器的状态设置为 OffState。在遥控器的行为发生改变时,我们只需要调用当前状态对象的方法即可。
在使用状态模式时,我们首先需要创建一个遥控器对象,并将其初始状态设置为一个具体状态对象。然后,我们可以通过调用遥控器对象的方法来触发状态的改变。在状态改变时,我们只需要将遥控器对象的状态设置为一个新的具体状态对象即可。
这种设计可以使得遥控器的行为更加清晰、易于维护。同时,如果我们需要添加新的状态,例如静音状态,我们只需要创建一个新的具体状态类即可,而不需要修改遥控器的代码。
备忘录模式
概念
备忘录模式是一种行为型设计模式,它允许在不暴露对象实现细节的情况下保存和恢复对象的内部状态。备忘录模式通常用于需要撤销操作或恢复先前状态的场景。
在备忘录模式中,有三个主要的角色:
- 发起人(Originator):负责创建一个备忘录,以记录当前时刻它的内部状态,并可以使用备忘录恢复内部状态。
- 备忘录(Memento):存储发起人的内部状态,并可以防止发起人以外的其他对象访问备忘录。
- 管理者(Caretaker):负责保存备忘录,并在需要时将其返回给发起人。
备忘录模式的核心思想是将对象的状态保存到备忘录中,以便在需要时可以恢复状态。这种模式可以帮助我们实现撤销和重做操作,或者在某些情况下恢复先前的状态。
应用场景
备忘录模式主要应用于需要保存和恢复对象状态的场景。当我们需要在不破坏对象封装性的前提下,保存对象的内部状态,并在需要时恢复该状态时,可以使用备忘录模式。
以下是一些备忘录模式的应用场景:
- 撤销操作:当我们需要实现撤销操作时,可以使用备忘录模式来保存对象的历史状态。例如,在文本编辑器中,我们可以使用备忘录模式来保存文本的历史版本,以便用户可以撤销操作并恢复到之前的版本。
- 数据库事务:当我们需要实现数据库事务时,可以使用备忘录模式来保存数据库操作的历史状态。例如,在银行系统中,我们可以使用备忘录模式来保存用户账户的历史余额,以便在用户进行转账等操作时可以回滚到之前的状态。
- 游戏存档:当我们需要实现游戏存档功能时,可以使用备忘录模式来保存游戏状态。例如,在角色扮演游戏中,我们可以使用备忘录模式来保存角色的属性、装备、技能等信息,以便在需要时可以恢复到之前的状态。
在备忘录模式中,我们通常会定义一个备忘录类来保存对象的状态,一个发起人类来创建备忘录并恢复状态,以及一个负责人类来管理备忘录。当需要保存对象状态时,我们可以创建一个备忘录对象,并将其保存到负责人对象中;当需要恢复对象状态时,我们可以从负责人对象中获取备忘录对象,并将其传递给发起人对象来恢复状态。
代码示例
以下是备忘录模式的 TypeScript 实现,包括发起人、备忘录和管理者三个角色:
// 发起人
class Originator {
private state: string;
constructor(state: string) {
this.state = state;
}
// 创建备忘录
createMemento(): Memento {
return new Memento(this.state);
}
// 恢复状态
restoreMemento(memento: Memento) {
this.state = memento.getState();
}
// 修改状态
setState(state: string) {
this.state = state;
}
// 显示状态
getState() {
return this.state;
}
}
// 备忘录
class Memento {
private state: string;
constructor(state: string) {
this.state = state;
}
getState() {
return this.state;
}
}
// 管理者
class Caretaker {
private mementos: Memento[] = [];
// 添加备忘录
addMemento(memento: Memento) {
this.mementos.push(memento);
}
// 获取最后一个备忘录
getLastMemento(): Memento {
return this.mementos.pop();
}
}
// 使用示例
const originator = new Originator("State 1");
const caretaker = new Caretaker();
// 保存状态
caretaker.addMemento(originator.createMemento());
// 修改状态
originator.setState("State 2");
// 保存状态
caretaker.addMemento(originator.createMemento());
// 修改状态
originator.setState("State 3");
// 恢复状态
originator.restoreMemento(caretaker.getLastMemento());
console.log(originator.getState()); // 输出 "State 2"
在这个示例中,我们创建了一个 Originator 类来表示发起人,一个 Memento 类来表示备忘录,以及一个 Caretaker 类来表示管理者。我们使用这些类来实现状态的保存和恢复。
在使用备忘录模式时,我们首先需要创建一个发起人对象,并将其状态保存到备忘录中。然后,我们可以修改发起人的状态,并再次将其保存到备忘录中。如果需要恢复先前的状态,我们可以从管理者中获取最后一个备忘录,并将其传递给发起人进行恢复。
策略模式
概念
策略模式是一种行为型设计模式,它允许在运行时选择算法的行为。它定义了一系列算法,将每个算法封装起来,并使它们可以相互替换。这使得算法可以独立于使用它们的客户端代码而变化。
策略模式通常由三个部分组成:上下文、策略接口和具体策略。上下文是客户端代码使用的对象,它包含一个策略接口的引用,并在需要时调用策略接口的方法。策略接口定义了一组算法的公共接口,具体策略实现了策略接口,并提供了算法的具体实现。
策略模式的优点包括:
- 算法可以独立于客户端代码而变化,从而提高代码的灵活性和可维护性。
- 策略模式可以避免使用大量的条件语句,从而提高代码的可读性和可维护性。
- 策略模式可以提高代码的复用性,因为不同的算法可以共享相同的接口。
策略模式的缺点包括:
- 策略模式可能会增加代码的复杂性,因为它需要定义多个策略类。
- 策略模式可能会增加代码的开销,因为它需要在运行时选择算法。
使用场景
策略模式适用于以下场景:
- 当一个类有多种行为或算法,并且这些行为或算法可以在运行时动态切换时,可以使用策略模式。
- 当一个类有多个条件语句,并且这些条件语句会影响类的行为时,可以使用策略模式来避免使用大量的 if else 语句。
- 当一个类需要根据不同的数据或输入来执行不同的操作时,可以使用策略模式来将这些操作封装成不同的策略类。
- 当一个类需要在不同的环境中使用不同的算法时,可以使用策略模式来将算法独立于客户端代码而变化。
- 当一个类需要在运行时动态地选择算法时,可以使用策略模式来提高代码的灵活性和可维护性。
总之,策略模式可以将算法独立于客户端代码而变化,提高代码的灵活性和可维护性。策略模式可以避免使用大量的 if else 语句,提高代码的可读性和可维护性。策略模式可以提高代码的复用性,因为不同的算法可以共享相同的接口。
代码示例
// 定义策略接口
interface DiscountStrategy {
getDiscount(price: number): number;
}
// 具体策略类:不打折
class NoDiscount implements DiscountStrategy {
getDiscount(price: number): number {
return 0;
}
}
// 具体策略类:打九折
class TenPercentDiscount implements DiscountStrategy {
getDiscount(price: number): number {
return price * 0.1;
}
}
// 具体策略类:打八折
class TwentyPercentDiscount implements DiscountStrategy {
getDiscount(price: number): number {
return price * 0.2;
}
}
// 上下文类
class DiscountCalculator {
private strategy: DiscountStrategy; // 持有策略接口的引用
constructor(strategy: DiscountStrategy) {
this.strategy = strategy; // 通过构造函数注入具体策略类的实例
}
setStrategy(strategy: DiscountStrategy) {
this.strategy = strategy; // 通过 setStrategy 方法动态地切换具体策略类的实例
}
calculate(price: number): number {
const discount = this.strategy.getDiscount(price); // 调用策略接口的方法来计算折扣
return price - discount; // 返回折扣后的价格
}
}
// 客户端代码
const calculator = new DiscountCalculator(new NoDiscount()); // 创建上下文对象,并使用具体策略类 NoDiscount 来实现它
console.log(calculator.calculate(100)); // 输出 100
calculator.setStrategy(new TenPercentDiscount()); // 切换具体策略类为 TenPercentDiscount
console.log(calculator.calculate(100)); // 输出 90
calculator.setStrategy(new TwentyPercentDiscount()); // 切换具体策略类为 TwentyPercentDiscount
console.log(calculator.calculate(100)); // 输出 80
观察者模式
概念
观察者模式是一种行为型设计模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象,当主题对象发生变化时,它的所有观察者都会收到通知并进行相应的处理。
观察者模式包含以下角色:
- 抽象主题(Subject):定义了一个抽象接口,用于添加、删除和通知观察者对象。
- 具体主题(ConcreteSubject):实现了抽象主题接口,维护了一个观察者列表,并在状态发生变化时通知观察者。
- 抽象观察者(Observer):定义了一个抽象接口,用于接收主题对象的通知。
- 具体观察者(ConcreteObserver):实现了抽象观察者接口,当接收到主题对象的通知时进行相应的处理。
观察者模式的优点包括:
- 观察者模式可以实现对象之间的松耦合,主题对象和观察者对象之间没有直接的依赖关系,可以独立地进行扩展和修改。
- 观察者模式可以实现动态的发布-订阅机制,主题对象可以动态地添加和删除观察者对象,观察者对象也可以动态地订阅和取消订阅主题对象。
观察者模式可以实现广播通信,主题对象可以同时通知多个观察者对象,观察者对象也可以同时接收来自多个主题对象的通知。
使用场景
- 当一个对象的状态发生变化时,需要通知其他对象进行相应的处理时,可以使用观察者模式。
- 当一个对象需要在不同的时间点通知其他对象进行相应的处理时,可以使用观察者模式。
- 当一个对象需要与多个对象进行协作,但又不希望与这些对象直接耦合时,可以使用观察者模式。
- 当一个对象需要在运行时动态地添加和删除观察者对象时,可以使用观察者模式。
- 当一个对象需要实现动态的发布-订阅机制时,可以使用观察者模式。
总之,观察者模式是一种非常常用的设计模式,它可以帮助我们实现对象之间的松耦合,提高代码的可维护性和可扩展性。观察者模式可以实现动态的发布-订阅机制,广泛应用于事件驱动的编程模型中。在 React 和 Vue 等前端框架中,也广泛使用了观察者模式来实现组件之间的通信和状态管理。
代码示例
// 定义抽象主题接口
interface Subject {
registerObserver(observer: Observer): void; // 注册观察者
removeObserver(observer: Observer): void; // 移除观察者
notifyObservers(): void; // 通知观察者
}
// 定义抽象观察者接口
interface Observer {
update(data: any): void; // 更新方法
}
// 定义具体主题类
class ConcreteSubject implements Subject {
private observers: Observer[] = []; // 观察者列表
private data: any; // 主题对象的状态
// 注册观察者
registerObserver(observer: Observer): void {
this.observers.push(observer);
}
// 移除观察者
removeObserver(observer: Observer): void {
const index = this.observers.indexOf(observer);
if (index !== -1) {
this.observers.splice(index, 1);
}
}
// 通知观察者
notifyObservers(): void {
for (const observer of this.observers) {
observer.update(this.data);
}
}
// 设置主题对象的状态,并通知观察者
setData(data: any): void {
this.data = data;
this.notifyObservers();
}
}
// 定义具体观察者类
class ConcreteObserver implements Observer {
private name: string; // 观察者的名称
constructor(name: string) {
this.name = name;
}
// 更新方法
update(data: any): void {
console.log(`${this.name} received data: ${data}`);
}
}
// 创建具体主题对象
const subject = new ConcreteSubject();
// 创建具体观察者对象
const observer1 = new ConcreteObserver('Observer 1');
const observer2 = new ConcreteObserver('Observer 2');
// 注册观察者
subject.registerObserver(observer1);
subject.registerObserver(observer2);
// 设置主题对象的状态,并通知观察者
subject.setData('Hello, world!');
// 移除观察者
subject.removeObserver(observer2);
// 设置主题对象的状态,并通知观察者
subject.setData('Goodbye, world!');