设计模式(五)
状态模式(State Pattern)详解
一、核心概念
状态模式允许对象在其内部状态改变时改变其行为,使得对象看起来如同修改了其类。该模式将状态相关的行为封装在独立的状态类中,并通过统一接口进行切换。
通过切换状态来实现切换行为。
当一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为时,就可以考虑使用状态模式了
核心组件:
- 上下文(Context):持有当前状态的引用,并将状态相关行为委托给当前状态。
- 抽象状态(State):定义状态接口,封装与上下文特定状态相关的行为。
- 具体状态(Concrete State):实现抽象状态接口,处理特定状态下的行为,并可切换上下文的状态。
二、代码示例:电梯控制系统
场景:电梯有多种状态(开门、关门、运行、停止),不同状态下响应不同命令。
#include <iostream>
#include <string>// 前置声明
class Elevator;// 抽象状态类
class ElevatorState {
public:virtual ~ElevatorState() = default;virtual void openDoor(Elevator* elevator) = 0;virtual void closeDoor(Elevator* elevator) = 0;virtual void run(Elevator* elevator) = 0;virtual void stop(Elevator* elevator) = 0;virtual std::string getStateName() const = 0;
};// 上下文类:电梯
class Elevator {
private:ElevatorState* currentState;public:explicit Elevator(ElevatorState* state) : currentState(state) {}~Elevator() { delete currentState; }void setState(ElevatorState* state) {std::cout << "电梯状态从 " << currentState->getStateName() << " 变为 " << state->getStateName() << std::endl;delete currentState;currentState = state;}// 委托给当前状态处理void openDoor() { currentState->openDoor(this); }void closeDoor() { currentState->closeDoor(this); }void run() { currentState->run(this); }void stop() { currentState->stop(this); }
};// 具体状态:开门状态
class DoorOpenState : public ElevatorState {
public:void openDoor(Elevator* elevator) override {std::cout << "电梯门已开,无需重复操作" << std::endl;}void closeDoor(Elevator* elevator) override {std::cout << "电梯门正在关闭..." << std::endl;elevator->setState(new DoorClosedState());}void run(Elevator* elevator) override {std::cout << "电梯门未关,无法运行" << std::endl;}void stop(Elevator* elevator) override {std::cout << "电梯未运行,无需停止" << std::endl;}std::string getStateName() const override { return "开门状态"; }
};// 具体状态:关门状态
class DoorClosedState : public ElevatorState {
public:void openDoor(Elevator* elevator) override {std::cout << "电梯门正在打开..." << std::endl;elevator->setState(new DoorOpenState());}void closeDoor(Elevator* elevator) override {std::cout << "电梯门已关闭,无需重复操作" << std::endl;}void run(Elevator* elevator) override {std::cout << "电梯开始运行..." << std::endl;elevator->setState(new RunningState());}void stop(Elevator* elevator) override {std::cout << "电梯未运行,无需停止" << std::endl;}std::string getStateName() const override { return "关门状态"; }
};// 具体状态:运行状态
class RunningState : public ElevatorState {
public:void openDoor(Elevator* elevator) override {std::cout << "电梯运行中,不能开门" << std::endl;}void closeDoor(Elevator* elevator) override {std::cout << "电梯运行中,门已关闭" << std::endl;}void run(Elevator* elevator) override {std::cout << "电梯已在运行中" << std::endl;}void stop(Elevator* elevator) override {std::cout << "电梯正在停止..." << std::endl;elevator->setState(new StoppedState());}std::string getStateName() const override { return "运行状态"; }
};// 具体状态:停止状态
class StoppedState : public ElevatorState {
public:void openDoor(Elevator* elevator) override {std::cout << "电梯门正在打开..." << std::endl;elevator->setState(new DoorOpenState());}void closeDoor(Elevator* elevator) override {std::cout << "电梯门已关闭,无需重复操作" << std::endl;}void run(Elevator* elevator) override {std::cout << "电梯开始运行..." << std::endl;elevator->setState(new RunningState());}void stop(Elevator* elevator) override {std::cout << "电梯已停止,无需重复操作" << std::endl;}std::string getStateName() const override { return "停止状态"; }
};// 客户端代码
int main() {// 创建电梯,初始状态为开门Elevator elevator(new DoorOpenState());// 执行一系列操作elevator.closeDoor(); // 开门 -> 关门elevator.run(); // 关门 -> 运行elevator.openDoor(); // 运行中,无法开门elevator.stop(); // 运行 -> 停止elevator.openDoor(); // 停止 -> 开门return 0;
}
三、状态模式的优势
-
解耦状态与行为:
- 将特定状态的行为封装在独立类中,避免使用大量
if-else
或switch-case
。
- 将特定状态的行为封装在独立类中,避免使用大量
-
符合开闭原则:
- 新增状态只需添加新的具体状态类,无需修改现有代码。
-
状态转换显式化:
- 状态转换逻辑集中在状态类内部,便于维护和理解。
-
行为局部化:
- 特定状态的行为由该状态类完全负责,降低了上下文类的复杂度。
四、状态模式与策略模式的对比
维度 | 状态模式(State) | 策略模式(Strategy) |
---|---|---|
核心意图 | 封装基于状态的行为,状态间可自动转换 | 封装可互换的算法,由客户端主动选择 |
状态管理 | 状态由上下文或状态类自身管理,自动转换 | 策略由客户端管理,需显式切换 |
依赖关系 | 上下文依赖抽象状态,具体状态相互知晓 | 上下文依赖抽象策略,具体策略相互独立 |
使用场景 | 行为随状态变化而变化,状态转换逻辑复杂 | 算法可互相替换,客户端需灵活选择 |
五、状态模式的适用场景
-
对象行为依赖状态:
- 如电商订单状态(待支付、已支付、已发货、已完成)。
-
状态转换频繁:
- 如游戏角色状态(站立、行走、跳跃、攻击)。
-
条件语句复杂:
- 当大量
if-else
或switch-case
使代码难以维护时。
- 当大量
六、状态模式的实现变种
-
共享状态实例:
- 对于无状态的具体状态类,可使用单例模式避免重复创建。
-
状态工厂:
- 使用工厂模式创建状态实例,集中管理状态创建逻辑。
-
状态机框架:
- 复杂场景下,可使用第三方状态机库(如Boost.Statechart)简化开发。
七、注意事项
-
内存管理:
- 状态类通常由上下文持有,需注意析构顺序避免内存泄漏。
-
状态爆炸:
- 若状态数量过多,可能导致类膨胀,需考虑状态分层或组合。
-
状态转换逻辑:
- 状态转换规则应集中管理,避免分散在多个状态类中。
状态模式是处理复杂状态逻辑的有效工具,通过将状态相关行为封装到独立类中,使代码更清晰、可维护和可扩展。
策略模式(Strategy Pattern)详解
一、核心概念
策略模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换。策略模式让算法的变化独立于使用算法的客户端。
核心组件:
- 策略接口(Strategy):定义所有支持的算法的公共接口。
- 具体策略(Concrete Strategy):实现策略接口的具体算法。
- 上下文(Context):持有一个策略对象的引用,负责使用策略。
二、代码示例:支付系统
场景:电商系统支持多种支付方式(信用卡、支付宝、微信支付)。
#include <iostream>
#include <string>// 策略接口:支付方式
class PaymentStrategy {
public:virtual ~PaymentStrategy() = default;virtual void pay(double amount) const = 0;
};// 具体策略:信用卡支付
class CreditCardPayment : public PaymentStrategy {
private:std::string cardNumber;std::string cvv;std::string expiryDate;public:CreditCardPayment(const std::string& cardNum, const std::string& cvv, const std::string& expiry): cardNumber(cardNum), cvv(cvv), expiryDate(expiry) {}void pay(double amount) const override {std::cout << "使用信用卡支付: " << amount << " 元" << std::endl;std::cout << "卡号: " << cardNumber << ", CVV: " << cvv << ", 有效期: " << expiryDate << std::endl;}
};// 具体策略:支付宝支付
class AlipayPayment : public PaymentStrategy {
private:std::string userId;public:explicit AlipayPayment(const std::string& id) : userId(id) {}void pay(double amount) const override {std::cout << "使用支付宝支付: " << amount << " 元" << std::endl;std::cout << "支付宝账户: " << userId << std::endl;}
};// 具体策略:微信支付
class WechatPayment : public PaymentStrategy {
private:std::string openId;public:explicit WechatPayment(const std::string& id) : openId(id) {}void pay(double amount) const override {std::cout << "使用微信支付: " << amount << " 元" << std::endl;std::cout << "微信ID: " << openId << std::endl;}
};// 上下文:订单
class Order {
private:double amount;PaymentStrategy* paymentStrategy;public:explicit Order(double orderAmount) : amount(orderAmount), paymentStrategy(nullptr) {}~Order() { delete paymentStrategy; }void setPaymentStrategy(PaymentStrategy* strategy) {delete paymentStrategy;paymentStrategy = strategy;}void processPayment() const {if (!paymentStrategy) {std::cout << "请先选择支付方式" << std::endl;return;}paymentStrategy->pay(amount);}
};// 客户端代码
int main() {// 创建订单Order order(99.99);// 选择支付方式(策略)order.setPaymentStrategy(new CreditCardPayment("1234-5678-9012-3456", "123", "12/25"));order.processPayment();// 切换支付方式order.setPaymentStrategy(new AlipayPayment("user@example.com"));order.processPayment();return 0;
}
三、策略模式的优势
-
算法可互换:
- 客户端可以在运行时动态切换算法,无需修改上下文。
-
解耦算法与客户端:
- 算法的实现和使用分离,符合单一职责原则。
-
扩展性强:
- 新增策略无需修改现有代码,符合开闭原则。
-
消除条件语句:
- 避免使用大量
if-else
或switch-case
来选择算法。
- 避免使用大量
四、策略模式与状态模式的对比
维度 | 策略模式(Strategy) | 状态模式(State) |
---|---|---|
核心意图 | 封装可互换的算法,由客户端主动选择 | 封装基于状态的行为,状态间可自动转换 |
状态管理 | 策略由客户端管理,需显式切换 | 状态由上下文或状态类自身管理,自动转换 |
依赖关系 | 上下文依赖抽象策略,具体策略相互独立 | 上下文依赖抽象状态,具体状态相互知晓 |
使用场景 | 算法可互相替换,客户端需灵活选择 | 行为随状态变化而变化,状态转换逻辑复杂 |
五、策略模式的适用场景
-
多种算法选择:
- 如排序算法(冒泡排序、快速排序、归并排序)。
-
动态切换行为:
- 如游戏中的角色移动方式(步行、跑步、飞行)。
-
避免条件语句:
- 当算法选择逻辑复杂时,使用策略模式替代条件语句。
六、策略模式的实现变种
-
策略工厂:
- 使用工厂模式创建策略实例,隐藏具体策略的创建逻辑。
-
参数化策略:
- 通过构造函数或设置方法向策略传递参数,增强灵活性。
-
策略注册表:
- 使用注册表存储策略,支持动态注册和发现策略。
七、注意事项
-
客户端需了解策略:
- 客户端必须知道所有策略的区别,才能选择合适的策略。
-
策略数量控制:
- 过多的策略类可能导致类爆炸,可考虑使用策略组合或分层。
-
策略共享:
- 无状态的策略可设计为单例,减少对象创建开销。
策略模式是处理算法变化的有效工具,通过将算法封装在独立的策略类中,使代码更具灵活性和可维护性。
适配器模式(Adapter Pattern)详解
一、核心概念
适配器模式是一种结构型设计模式,它允许将一个类的接口转换成客户期望的另一个接口。适配器让原本接口不兼容的类可以合作无间,其核心思想是通过一个中间组件(适配器)来兼容不同的接口。
需要的东西就在面前,但却不能使用,而短时间又无法改造它,于是我们就想办法适配它。比如笔记本电脑是不能什么电压都能用的,但国家不同,电压可能不相同也是事实,于是就用一个电源适配器,只要是电,不管多少伏,都能把电源变成需要的电压,这就是电源适配器的作用。适配器的意思就是使得一个东西适合另一个东西的东西。
适配器也可以看作“ 翻译 ”,使不兼容的两个系统间能够彼此沟通。
什么时候需要适配器?
在软件开发中,也就是系统的数据和行为都正确,但接口不符时,我们应该考虑用适配器,目的是使控制范围之外的一个原有对象与某个接口匹配。适配器模式主要应用于希望复用一些现存的类,但是接口又与复用环境要求不一致的情况,比如在需要对早期代码复用一些功能等应用上很有实际价值。
核心角色:
- 目标接口(Target):客户期望的接口,定义了特定领域的方法。
- 适配者(Adaptee):现有接口,需要被适配的类。
- 适配器(Adapter):实现目标接口并包装适配者,将请求转换为适配者的接口调用。
二、实现方式
适配器模式有两种主要实现方式:
- 类适配器:通过多重继承同时实现目标接口和适配者类。
- 对象适配器:通过组合持有适配者实例,更灵活且常用。
三、代码示例
场景:将第三方矩形绘制库适配为统一的形状接口。
1. 对象适配器实现
#include <iostream>
#include <string>// 目标接口:形状
class Shape {
public:virtual void draw() = 0;virtual ~Shape() = default;
};// 适配者:第三方矩形类(接口不兼容)
class ThirdPartyRectangle {
public:void drawRectangle(double x1, double y1, double x2, double y2) {std::cout << "第三方库绘制矩形:从 (" << x1 << "," << y1 << ") 到 (" << x2 << "," << y2 << ")" << std::endl;}
};// 对象适配器:将矩形适配为形状接口
class RectangleAdapter : public Shape {
private:ThirdPartyRectangle* adaptee; // 组合适配者double x1, y1, x2, y2; // 矩形坐标public:RectangleAdapter(double x1, double y1, double x2, double y2) : adaptee(new ThirdPartyRectangle()), x1(x1), y1(y1), x2(x2), y2(y2) {}~RectangleAdapter() override { delete adaptee; }// 实现目标接口方法void draw() override {adaptee->drawRectangle(x1, y1, x2, y2); // 转换调用}
};// 客户端代码
int main() {// 使用适配器创建矩形(通过形状接口)Shape* rectangle = new RectangleAdapter(10, 10, 50, 50);rectangle->draw(); // 输出:第三方库绘制矩形:从 (10,10) 到 (50,50)delete rectangle;return 0;
}
2. 类适配器实现(C++多重继承)
// 目标接口(同上)
class Shape {
public:virtual void draw() = 0;virtual ~Shape() = default;
};// 适配者(同上)
class ThirdPartyRectangle {
public:void drawRectangle(double x1, double y1, double x2, double y2) {std::cout << "第三方库绘制矩形:从 (" << x1 << "," << y1 << ") 到 (" << x2 << "," << y2 << ")" << std::endl;}
};// 类适配器:通过多重继承同时实现目标接口和适配者
class RectangleAdapter : public Shape, private ThirdPartyRectangle {
private:double x1, y1, x2, y2;public:RectangleAdapter(double x1, double y1, double x2, double y2) : x1(x1), y1(y1), x2(x2), y2(y2) {}// 实现目标接口方法void draw() override {drawRectangle(x1, y1, x2, y2); // 直接调用适配者方法}
};
四、双向适配器
允许两个不兼容的接口互相适配:
class Shape {
public:virtual void draw() = 0;
};class Rectangle {
public:virtual void drawRect(double x1, double y1, double x2, double y2) = 0;
};class DoubleAdapter : public Shape, public Rectangle {
private:Shape* shape;Rectangle* rect;public:DoubleAdapter(Shape* s) : shape(s), rect(nullptr) {}DoubleAdapter(Rectangle* r) : shape(nullptr), rect(r) {}// 实现Shape接口void draw() override {if (rect) rect->drawRect(0, 0, 100, 100);}// 实现Rectangle接口void drawRect(double x1, double y1, double x2, double y2) override {if (shape) shape->draw();}
};
五、适配器模式的优势
-
接口兼容性:
- 使不兼容的接口可以协同工作,提高代码复用性。
-
松耦合设计:
- 客户端与适配者解耦,只依赖目标接口。
-
开闭原则:
- 新增适配器无需修改现有代码,符合扩展性要求。
-
双向适配:
- 支持双向适配,使两个方向的接口调用都能兼容。
六、适用场景
-
整合第三方库:
- 当现有接口与第三方库不兼容时,使用适配器转换接口。
-
系统升级:
- 旧系统接口与新系统不兼容时,通过适配器过渡。
-
复用遗留代码:
- 将不兼容的遗留代码封装在适配器中,与新系统集成。
-
统一多个相似接口:
- 将多个不同但功能相似的类,通过适配器统一到一个接口下。
七、与其他模式的对比
-
与桥接模式的区别:
- 适配器是事后补救(解决已存在的不兼容),桥接是事前设计(分离抽象与实现)。
-
与装饰器模式的区别:
- 适配器改变接口,装饰器增强功能但不改变接口。
-
与外观模式的区别:
- 适配器转换单个接口,外观为子系统提供统一接口。
八、注意事项
-
避免过度使用:
- 适配器模式可能掩盖设计缺陷,应优先考虑重构接口而非适配。
-
性能开销:
- 对象适配器通过组合实现,可能增加一层调用开销。
-
双向适配器复杂性:
- 双向适配器增加了代码复杂度,需谨慎使用。
适配器模式是解决接口不兼容问题的有效工具,通过引入中间层(适配器),使原本无法协作的类能够无缝集成。在实际开发中,它常用于整合第三方库、系统升级过渡或复用遗留代码。