什么是设计模式?
模式是一种可重用的解决方案,用于解决在特定环境中反复出现的问题;
设计模式则是这类解决方案在软件工程中的应用,它是一套经过验证的、针对面向对象软件设计中常见问题的通用模板。
设计模式的目的
- 提高代码的可重用性:通过使用设计模式,开发者可以复用经过验证的解决方案,而不必每次遇到相似问题时都从头开始设计。
- 提高代码的可维护性:设计模式有助于写出清晰和有组织的代码,这使得代码更容易理解和修改。
- 促进良好的编程实践:设计模式鼓励开发者遵循软件工程的最佳实践,如“封装变化点”、“针对接口编程而不是实现编程”等。
设计模式的分类
设计模式通常分为以下三大类:
-
创建型模式:这些模式处理对象的创建过程,隐藏创建逻辑而不是直接使用
new
运算符实例化对象。这包括单例模式、工厂方法模式、抽象工厂模式、建造者模式、原型模式等。 -
结构型模式:这些模式主要关注类和对象之间的组合,用于形成更大的结构。这包括适配器模式、桥接模式、组合模式、装饰器模式、门面模式、享元模式、代理模式等。
-
行为型模式:这些模式主要关注对象之间的通信,它们描述了对象和类如何交互以及分配职责。这包括责任链模式、命令模式、解释器模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式、策略模式、模板方法模式、访问者模式等。
学习设计模式的前置条件
了解基本的软件设计原则,如单一职责原则(SRP)、开闭原则(OCP)、依赖倒置原则(DIP)等。这些原则是设计模式的理论基础,这里不多赘述。
创建型模式
由于设计模式篇幅过于辽阔,本片文章只讲创建型模式
1.单例模式
使用场景:日志记录器、数据库连接池,全局配置管理器等,需要确保某个类只有一个实例,并在整个应用程序中共享该实例。
解决问题:通过单例模式,确保一个类只有一个实例,并提供一个全局访问点访问该实例。
// 单例类
class Logger {
private static instance: Logger;
private constructor() {
// 私有化构造函数,防止外部实例化
}
public static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
public log(message: string): void {
console.log(`[LOG]: ${message}`);
}
}
// 客户端代码
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();
logger1.log("This is the first log."); // 输出: [LOG]: This is the first log.
logger2.log("This is the second log."); // 输出: [LOG]: This is the second log.
console.log(logger1 === logger2); // 输出: true,证明两者是同一个实例
-
适用场景:需要保证类在整个系统中只有一个实例,且实例需要被全局共享时。
-
优点:控制实例数量,节省资源,避免重复创建相同对象。
2.原型模式
场景:游戏角色属性克隆
不使用设计模式
不使用原型模式时,我们通常需要手动创建一个新的对象,并将每个属性逐个复制。
class Player {
public name: string;
public level: number;
public abilities: string[];
constructor(name: string, level: number, abilities: string[]) {
this.name = name;
this.level = level;
this.abilities = abilities;
}
public describe(): string {
return `Player: ${this.name}, Level: ${this.level}, Abilities: ${this.abilities.join(", ")}`;
}
}
// 客户端代码
const originalPlayer = new Player("Warrior", 10, ["Sword Mastery", "Shield Block"]);
// 手动克隆角色
const clonedPlayer = new Player(
originalPlayer.name,
originalPlayer.level,
[...originalPlayer.abilities] // 需要显式拷贝数组,避免引用同一个数组
);
console.log(clonedPlayer.describe());
// 输出: Player: Warrior, Level: 10, Abilities: Sword Mastery, Shield Block
使用原型模式
// 原型接口
interface GameCharacter extends Prototype {
clone(): GameCharacter;
}
// 具体原型类:游戏角色
class Player implements GameCharacter {
public name: string;
public level: number;
public abilities: string[];
constructor(name: string, level: number, abilities: string[]) {
this.name = name;
this.level = level;
this.abilities = abilities;
}
public clone(): Player {
return new Player(this.name, this.level, [...this.abilities]);
}
public describe(): string {
return `Player: ${this.name}, Level: ${this.level}, Abilities: ${this.abilities.join(", ")}`;
}
}
// 客户端代码
const originalPlayer = new Player("Warrior", 10, ["Sword Mastery", "Shield Block"]);
const clonedPlayer = originalPlayer.clone();
console.log(clonedPlayer.describe());
// 输出: Player: Warrior, Level: 10, Abilities: Sword Mastery, Shield Block
优点:
-
简化克隆逻辑:克隆对象时不需要关注每个属性的具体实现。
-
代码维护更简单:如果
Player
类增加了新的属性,我们只需修改clone()
方法,而不必在每次手动克隆时都更新逻辑。 -
减少代码重复:克隆逻辑集中在
clone()
方法中,避免在客户端代码中反复编写属性复制的代码。
总结
不使用设计模式时,手动复制每个属性不仅容易出错,还可能导致代码冗长和难以维护。尤其当类的属性较多或者可能在未来发生变化时,手动复制每个属性的成本会增加。
3.建造者模式
场景:创造一个复杂的房屋对象
不使用设计模式
class House {
public type: string; // 房屋类型
public floors: number; // 楼层数量
public hasGarden: boolean; // 是否有花园
public hasPool: boolean; // 是否有游泳池
public hasGarage: boolean; // 是否有车库
public color: string; // 房屋颜色
public area: number; // 房屋面积(平方米)
public address: string; // 房屋地址
public numOfWindows: number; // 窗户数量
public numOfDoors: number; // 门的数量
constructor(
type: string,
floors: number,
hasGarden: boolean,
hasPool: boolean,
hasGarage: boolean,
color: string,
area: number,
address: string,
numOfWindows: number,
numOfDoors: number
) {
this.type = type;
this.floors = floors;
this.hasGarden = hasGarden;
this.hasPool = hasPool;
this.hasGarage = hasGarage;
this.color = color;
this.area = area;
this.address = address;
this.numOfWindows = numOfWindows;
this.numOfDoors = numOfDoors;
}
public describe(): string {
return `House Type: ${this.type}, Floors: ${this.floors}, Garden: ${this.hasGarden}, Pool: ${this.hasPool}, Garage: ${this.hasGarage}, Color: ${this.color}, Area: ${this.area}, Address: ${this.address}, Windows: ${this.numOfWindows}, Doors: ${this.numOfDoors}`;
}
}
// 客户端代码:手动构建复杂对象
const house = new House(
"Villa",
3,
true,
true,
true,
"White",
250,
"123 Main St",
10,
5
);
console.log(house.describe());
// 输出: House Type: Villa, Floors: 3, Garden: true, Pool: true, Garage: true, Color: White, Area: 250, Address: 123 Main St, Windows: 10, Doors: 5
不使用设计模式的缺点:
-
构造函数过于复杂:有10个参数,调用时容易出错。
-
参数顺序问题:必须严格按照参数顺序传递,增加了出错的风险,尤其是多个参数类型相同时。
-
难以扩展:如果要增加更多的属性,构造函数将变得更加臃肿,维护性差。
使用建造者模式
class House {
public type: string; // 房屋类型
public floors: number; // 楼层数量
public hasGarden: boolean; // 是否有花园
public hasPool: boolean; // 是否有游泳池
public hasGarage: boolean; // 是否有车库
public color: string; // 房屋颜色
public area: number; // 房屋面积(平方米)
public address: string; // 房屋地址
public numOfWindows: number; // 窗户数量
public numOfDoors: number; // 门的数量
constructor() {
this.type = "";
this.floors = 0;
this.hasGarden = false;
this.hasPool = false;
this.hasGarage = false;
this.color = "";
this.area = 0;
this.address = "";
this.numOfWindows = 0;
this.numOfDoors = 0;
}
public describe(): string {
return `House Type: ${this.type}, Floors: ${this.floors}, Garden: ${this.hasGarden}, Pool: ${this.hasPool}, Garage: ${this.hasGarage}, Color: ${this.color}, Area: ${this.area}, Address: ${this.address}, Windows: ${this.numOfWindows}, Doors: ${this.numOfDoors}`;
}
}
class HouseBuilder {
private house: House;
constructor() {
this.house = new House();
}
public setType(type: string): HouseBuilder {
this.house.type = type;
return this;
}
public setFloors(floors: number): HouseBuilder {
this.house.floors = floors;
return this;
}
public setGarden(hasGarden: boolean): HouseBuilder {
this.house.hasGarden = hasGarden;
return this;
}
public setPool(hasPool: boolean): HouseBuilder {
this.house.hasPool = hasPool;
return this;
}
public setGarage(hasGarage: boolean): HouseBuilder {
this.house.hasGarage = hasGarage;
return this;
}
public setColor(color: string): HouseBuilder {
this.house.color = color;
return this;
}
public setArea(area: number): HouseBuilder {
this.house.area = area;
return this;
}
public setAddress(address: string): HouseBuilder {
this.house.address = address;
return this;
}
public setNumOfWindows(numOfWindows: number): HouseBuilder {
this.house.numOfWindows = numOfWindows;
return this;
}
public setNumOfDoors(numOfDoors: number): HouseBuilder {
this.house.numOfDoors = numOfDoors;
return this;
}
public build(): House {
return this.house;
}
}
// 客户端代码:使用建造者模式构建对象
const builder = new HouseBuilder();
const house = builder
.setType("Villa")
.setFloors(3)
.setGarden(true)
.setPool(true)
.setGarage(true)
.setColor("White")
.setArea(250)
.setAddress("123 Main St")
.setNumOfWindows(10)
.setNumOfDoors(5)
.build();
console.log(house.describe());
// 输出: House Type: Villa, Floors: 3, Garden: true, Pool: true, Garage: true, Color: White, Area: 250, Address: 123 Main St, Windows: 10, Doors: 5
使用建造者模式的优点:
-
代码更简洁清晰:每个属性的设置是显式的,顺序无关,客户端代码更易读,容易理解正在设置哪些属性。
-
降低出错风险:不需要记住参数的顺序和类型,链式调用使得代码更具有自描述性。
-
高度灵活:可以按需设置某些属性,而无需一次性传入所有属性值,灵活性高,适应不同的对象构建需求。
-
扩展性好:如果将来需要为
House
类添加新属性,只需在HouseBuilder
中增加相应的设置方法,原有客户端代码不需要修改。 -
减少冗长构造函数:避免了长长的构造函数参数列表,尤其是当类的属性较多时,建造者模式能显著简化对象的创建过程。
总结
使用建造者模式构建拥有多个属性的复杂对象时,能够显著提高代码的可读性、可维护性和灵活性,减少出错的可能性。特别是在面对具有多个可选属性的大型对象时,建造者模式是一种非常合适的设计模式。
工厂方法模式
场景:订单处理系统中,不同类型的产品可能需要不同的处理方式。实物商品可能需要发货,而数字商品需要生成下载链接。
不使用工厂方法模式
// 订单处理系统中的普通订单
class RegularOrder {
process() {
console.log('处理普通订单');
// 普通订单的处理逻辑
}
}
// 订单处理系统中的会员订单
class MemberOrder {
process() {
console.log('处理会员订单');
// 会员订单的处理逻辑
}
}
// 订单处理类
class OrderProcessor {
processOrder(orderType: string) {
let order;
if (orderType === 'regular') {
order = new RegularOrder();
} else if (orderType === 'member') {
order = new MemberOrder();
} else {
throw new Error('Unknown order type');
}
order.process(); // 处理订单
}
}
// 测试
const orderProcessor = new OrderProcessor();
orderProcessor.processOrder('regular'); // 处理普通订单
orderProcessor.processOrder('member'); // 处理会员订单
缺点:
-
扩展性差:每次新增一个新的订单类型,都需要修改
OrderProcessor
类,添加新的if-else
分支,违反了 开闭原则(OCP)。 -
扩展困难:随着订单类型的增多,代码的维护和扩展将变得复杂且容易出错。
-
订单类型的实例化集中在
OrderProcessor
类:这个类不仅负责处理订单,还负责根据类型决定创建哪种订单对象,违反了单一职责原则。
使用工厂方法模式
// 订单接口
interface Order {
processOrder(): string;
}
// 普通订单类
class RegularOrder implements Order {
public processOrder(): string {
return "处理普通订单";
}
}
// 会员订单类
class MembershipOrder implements Order {
public processOrder(): string {
return "处理会员订单";
}
}
// 工厂接口
interface OrderFactory {
createOrder(): Order;
}
// 普通订单工厂类
class RegularOrderFactory implements OrderFactory {
public createOrder(): Order {
return new RegularOrder();
}
}
// 会员订单工厂类
class MembershipOrderFactory implements OrderFactory {
public createOrder(): Order {
return new MembershipOrder();
}
}
// 客户端代码
function orderProcessor(factory: OrderFactory): void {
const order = factory.createOrder();
console.log(order.processOrder());
}
// 使用工厂方法模式创建不同类型的订单
const regularOrderFactory = new RegularOrderFactory();
const membershipOrderFactory = new MembershipOrderFactory();
orderProcessor(regularOrderFactory); // 输出: 处理普通订单
orderProcessor(membershipOrderFactory); // 输出: 处理会员订单
工厂方法模式的优点
-
将对象创建推迟到子类:
-
OrderFactory
接口和具体的工厂类(RegularOrderFactory
和MembershipOrderFactory
)负责决定具体创建哪种类型的订单,而客户端代码(orderProcessor
函数)只关心如何处理订单,而不需要了解具体的创建细节。
-
-
扩展性:
-
如果需要添加新的订单类型,只需新增一个新的订单类和相应的工厂类。客户端代码无需修改,可以继续使用新的工厂类创建新的订单类型。
-
-
低耦合:
-
客户端代码不直接依赖于具体的订单类,而是依赖于工厂接口,这减少了代码的耦合度,提高了系统的灵活性和可维护性。
-
5.抽象工厂模式
场景:假设我们有一个支付处理系统,支持不同的支付方式,如信用卡和银行转账。每种支付方式都有自己的验证逻辑和交易处理逻辑。在不使用设计模式的情况下,通常会在客户端代码中通过 if-else
判断支付类型来处理不同支付方式。
不使用设计模式
// 信用卡支付类
class CreditCardPayment {
public validateCard(): void {
console.log("验证信用卡...");
}
public processPayment(): void {
console.log("处理信用卡支付...");
}
}
// 银行转账支付类
class BankTransferPayment {
public validateAccount(): void {
console.log("验证银行账户...");
}
public processPayment(): void {
console.log("处理银行转账支付...");
}
}
// 客户端代码
class PaymentProcessor {
public processPayment(paymentType: string): void {
if (paymentType === "CreditCard") {
const payment = new CreditCardPayment();
payment.validateCard();
payment.processPayment();
} else if (paymentType === "BankTransfer") {
const payment = new BankTransferPayment();
payment.validateAccount();
payment.processPayment();
} else {
throw new Error("未知的支付类型");
}
}
}
// 客户端代码示例
const processor = new PaymentProcessor();
processor.processPayment("CreditCard"); // 输出:验证信用卡... 处理信用卡支付...
processor.processPayment("BankTransfer"); // 输出:验证银行账户... 处理银行转账支付...
缺点:
- 扩展性差:如果未来添加新的支付方式,例如移动支付(如微信支付或支付宝),需要修改
PaymentProcessor
类的代码,违反了开闭原则(OCP)。 - 代码冗长且难以维护:随着支付方式的增加,
if-else
分支的数量也会增多,客户端代码将变得复杂且难以维护。 - 职责混乱:
PaymentProcessor
类不仅负责处理支付,还负责不同支付方式的实例化,这违反了单一职责原则。
使用抽象工厂模式
// 支付接口
interface Payment {
validate(): void;
processPayment(): void;
}
// 信用卡支付实现
class CreditCardPayment implements Payment {
public validate(): void {
console.log("验证信用卡...");
}
public processPayment(): void {
console.log("处理信用卡支付...");
}
}
// 银行转账支付实现
class BankTransferPayment implements Payment {
public validate(): void {
console.log("验证银行账户...");
}
public processPayment(): void {
console.log("处理银行转账支付...");
}
}
// 抽象支付工厂接口
interface PaymentFactory {
createPayment(): Payment;
}
// 信用卡支付工厂
class CreditCardPaymentFactory implements PaymentFactory {
public createPayment(): Payment {
return new CreditCardPayment();
}
}
// 银行转账支付工厂
class BankTransferPaymentFactory implements PaymentFactory {
public createPayment(): Payment {
return new BankTransferPayment();
}
}
// 客户端代码
class PaymentProcessor {
private factory: PaymentFactory;
constructor(factory: PaymentFactory) {
this.factory = factory;
}
public processPayment(): void {
const payment = this.factory.createPayment();
payment.validate();
payment.processPayment();
}
}
// 使用抽象工厂模式处理支付
const creditCardFactory = new CreditCardPaymentFactory();
const bankTransferFactory = new BankTransferPaymentFactory();
const creditCardProcessor = new PaymentProcessor(creditCardFactory);
creditCardProcessor.processPayment(); // 输出:验证信用卡... 处理信用卡支付...
const bankTransferProcessor = new PaymentProcessor(bankTransferFactory);
bankTransferProcessor.processPayment(); // 输出:验证银行账户... 处理银行转账支付...
优点总结:
- 扩展灵活性:如果要添加新的订单类型,比如国际订单,只需新增一个
InternationalOrder
类和InternationalOrderFactory
,而无需修改现有代码。 - 低耦合性:客户端代码不直接依赖具体的订单类,只依赖工厂接口,订单的创建逻辑被封装在具体的工厂中。
- 更好的维护性:当订单处理逻辑变更时,只需修改相关的订单类或工厂类,其他部分无需调整。
工厂方法模式和抽象工厂模式的差异
抽象工厂模式和工厂方法模式在某些场景下看起来非常相似,因为它们都是用于创建对象的设计模式,且都通过定义接口或抽象类来将具体类的创建延迟到子类中。不过,它们之间有一些重要的区别,主要体现在它们的用途、结构和复杂性上。
区别概述:
- 工厂方法模式:
- 目的:工厂方法模式的核心是定义一个创建对象的接口,由子类决定要实例化的具体类。它只解决一个具体对象的创建问题。
- 重点:专注于通过继承来控制单一对象的创建。一个类只负责创建某种对象。
- 使用场景:当一个类无法预知要实例化的具体类型时,使用工厂方法模式可以将对象创建推迟到子类中进行。
- 抽象工厂模式:
- 目的:抽象工厂模式用于创建多个相关对象的家族,而不是一个单独的对象。它提供一个创建一系列相关或相互依赖对象的接口,而不指定具体的类。
- 重点:它不仅仅是创建一个对象,而是创建多个相关联的对象,这些对象通常属于一个“产品族”。
- 使用场景:当系统需要创建多个不同类型的对象,且这些对象是相关联的时,抽象工厂模式可以确保它们的兼容性。
举例说明
工厂方法模式:
问题:你要创建一类产品,但具体是哪种产品在运行时才知道。例如,创建一种支付方式(信用卡、银行转账等)。
// 支付接口
interface Payment {
processPayment(): void;
}
// 工厂方法接口
abstract class PaymentFactory {
public abstract createPayment(): Payment;
}
// 具体支付类
class CreditCardPayment implements Payment {
public processPayment(): void {
console.log("处理信用卡支付");
}
}
class BankTransferPayment implements Payment {
public processPayment(): void {
console.log("处理银行转账支付");
}
}
// 具体工厂类
class CreditCardPaymentFactory extends PaymentFactory {
public createPayment(): Payment {
return new CreditCardPayment();
}
}
class BankTransferPaymentFactory extends PaymentFactory {
public createPayment(): Payment {
return new BankTransferPayment();
}
}
// 客户端使用
const factory: PaymentFactory = new CreditCardPaymentFactory();
const payment: Payment = factory.createPayment();
payment.processPayment(); // 输出: 处理信用卡支付
要点:工厂方法模式只处理创建单个产品对象,且通过继承来实现产品的创建控制。
抽象工厂模式:
问题:你要创建多个产品,这些产品必须属于同一个产品家族。例如,一个 UI 工具包可能同时创建按钮和文本框,这些控件必须保持一致的外观风格(例如,Windows 风格或 Mac 风格)。
// 抽象产品A
interface Button {
render(): void;
}
// 抽象产品B
interface TextBox {
render(): void;
}
// 具体产品A
class WindowsButton implements Button {
public render(): void {
console.log("渲染Windows风格的按钮");
}
}
// 具体产品B
class WindowsTextBox implements TextBox {
public render(): void {
console.log("渲染Windows风格的文本框");
}
}
// 具体产品A
class MacButton implements Button {
public render(): void {
console.log("渲染Mac风格的按钮");
}
}
// 具体产品B
class MacTextBox implements TextBox {
public render(): void {
console.log("渲染Mac风格的文本框");
}
}
// 抽象工厂
interface UIComponentFactory {
createButton(): Button;
createTextBox(): TextBox;
}
// 具体工厂(Windows风格组件工厂)
class WindowsUIFactory implements UIComponentFactory {
public createButton(): Button {
return new WindowsButton();
}
public createTextBox(): TextBox {
return new WindowsTextBox();
}
}
// 具体工厂(Mac风格组件工厂)
class MacUIFactory implements UIComponentFactory {
public createButton(): Button {
return new MacButton();
}
public createTextBox(): TextBox {
return new MacTextBox();
}
}
// 客户端代码
function renderUI(factory: UIComponentFactory): void {
const button = factory.createButton();
const textBox = factory.createTextBox();
button.render();
textBox.render();
}
// 使用Windows工厂
renderUI(new WindowsUIFactory());
// 输出: 渲染Windows风格的按钮
// 输出: 渲染Windows风格的文本框
// 使用Mac工厂
renderUI(new MacUIFactory());
// 输出: 渲染Mac风格的按钮
// 输出: 渲染Mac风格的文本框
要点:抽象工厂模式处理创建一系列相关的产品对象,比如按钮和文本框。它确保这些产品在风格或其他特性上相互兼容。
主要区别
-
目的:
- 工厂方法模式的重点是创建一个产品,是解决单一产品创建的问题。
- 抽象工厂模式则是创建一组相关的产品,强调产品之间的关联性。
-
复杂性:
- 工厂方法模式相对简单,只需要为每个产品创建一个具体的工厂。
- 抽象工厂模式相对复杂,需要同时创建多个产品的工厂。
-
抽象级别:
- 工厂方法模式通常只定义单一产品的创建。
- 抽象工厂模式定义的是产品族,即多个相关的产品(如按钮和文本框)共同组成一个完整的产品系列。
总结
尽管工厂方法模式和抽象工厂模式都用于创建对象,但它们的应用场景不同:
- 工厂方法模式:用于创建单个对象的场景,通常是当产品类型有多个变体但客户端只需要一个产品时。
- 抽象工厂模式:用于创建多个相关对象的场景,特别是当这些对象属于一个产品族,且必须兼容时。
因此,尽管两者看起来相似,但它们的适用性和复杂性不同。
设计模式-创建型总结
创建型设计模式的核心在于解决对象创建过程中的复杂性和灵活性。通过将对象创建与业务逻辑分离,这些模式提升了代码的扩展性和维护性,遵循了单一职责和开闭原则。它们提供了灵活的对象创建方式,比如工厂方法、抽象工厂、原型和建造者模式,帮助我们在面对不同需求时,选择最合适的对象生成方式。
这些模式使代码更易扩展、维护,并通过面向接口编程减少耦合度。最终,创建型模式不仅提高了代码复用性,还让系统在面对变化时更加灵活、稳健。
使用设计模式虽然在初期会增加代码的复杂性,但它带来了更好的可维护性和扩展性。通过解耦代码,设计模式让系统在面对变化时更加灵活,只需修改局部代码,而不影响整体架构。此外,它减少了类与类之间的依赖,使代码更加模块化和可读,尤其在大型项目中,设计模式有助于应对未来的扩展需求,提升代码质量,降低长期维护的成本。