C++ 23 种 设计模式
一、创建型设计模式
创建型模式关注对象的创建过程,旨在将对象的创建与使用解耦。
1.单例模式(Singleton)
确保一个类只有一个实例,并提供全局访问点。
单例模式(Singleton Pattern)是一种创建型设计模式,确保一个类只有一个实例,并提供一个全局访问点来获取该实例。这种模式通常用于需要全局共享资源或唯一实例的场景,比如配置管理类、日志记录类、连接池等。
1.1 单例模式的核心要点
- 唯一性:确保某个类只能有一个实例。
- 全局访问点:提供一个全局方法获取该实例。
- 延迟实例化:实例在首次使用时创建,避免不必要的资源开销。
1.2 单例模式的实现方式
在C++中,单例模式实现可以用多种方法,下面主要介绍最常见的几种实现方式。
1.2.1 饿汉式单例(Eager Initialization)
在类加载时直接创建实例,线程安全,但是可能会造成资源浪费。
class Singleton
{
private:
Singleton() {} // 私有构造函数,防止外部实例化
static Singleton instance; // 静态实例,程序启动时创建
public:
Singleton(const Singleton &) = delete; // 禁止拷贝构造
Singleton & operator=(const Singleton &) = delete; // 禁止赋值
static Singleton & getInstance()
{
return instance;
}
void someMethod()
{
// 执行单例类的一些操作
}
};
Singleton Singleton::instance; // 静态实例定义
优点:实现简单,线程安全。
缺点:类加载时即创建实例,即使不使用也会占用资源。
1.2.2 懒汉式单例(Lazy Initialization)
实例在首次使用时创建,节省资源,但需要处理多线程并发访问。
#include <mutex>
class Singleton
{
private:
Singleton() {}
static Singleton * instance;
static std::mutex mtx;
public:
Singleton(const Singleton &) = delete;
Singleton & operator=(const Singleton &) = delete;
static Singleton * getInstance()
{
if (!instance)
{ // 第一次检测
std::lock_guard<std::mutex> lock(mtx);
if (!instance)
{ // 第二次检测
instance = new Singleton();
}
}
return instance;
}
};
Singleton * Singleton::instance = nullptr; // 静态成员定义
std::mutex Singleton::mtx;
优点:实例按需创建,节省资源。
缺点:实现稍复杂,多线程下需要双重检查锁机制确保线程安全。
1.2.3 使用C++11标准的线程安全局部静态变量
C++11引入了线程安全的局部静态变量,可以简化懒汉式单例的实现,无需手动加锁。
class Singleton
{
private:
Singleton() {}
public:
Singleton(const Singleton &) = delete;
Singleton & operator=(const Singleton &) = delete;
static Singleton & getInstance()
{
static Singleton instance;
return instance;
}
};
优点:实现简单、线程安全、支持懒加载。
缺点:依赖C++11标准
1.2.4 单例模式的析构问题
单例类的实例通常设计为在程序结束时自动释放。常见方法是定义一个静态成员对象,确保在程序退出时释放单例实例
class Singleton
{
private:
Singleton() {}
~Singleton() {}
public:
Singleton(const Singleton &) = delete;
Singleton & operator=(const Singleton &) = delete;
static Singleton & getInstance()
{
static Singleton instance;
return instance;
}
};
1.3 使用场景
- 配置管理类:系统级配置信息通常需要全局访问和唯一性。
- 日志管理类:集中化的日志记录,通过单例模式确保所有模块的日志输出一致。
- 资源管理类:连接池、线程池等需要统一管理的资源。
- 计数器类:全局计数器或统计类确保在不同模块间共享数据。
1.4 单例模式的优缺点
优点:
- 提供全局访问点,减少全局变量的使用。
- 控制实例创建的数量,节省系统资源。
缺点:
- 不支持多态:单例模式限制了继承的灵活性,难以支持多态。
- 隐藏依赖关系:过多的单例会使模块间依赖变得复杂且难以检测,影响代码的维护和测试。
2.工厂方法(Factory Method)
定义一个用于创建对象的接口,但由子类决定实例化的类。
工厂方法模式(Factory Method Pattern)是一种创建型设计模式,用于定义一个创建对象的接口,但让子类决定实例化哪一个具体类。工厂方法将对象的创建逻辑延迟到子类,符合开闭原则(对扩展开放,对修改封闭),能够更灵活地管理和扩展代码。
2.1 工厂方法模式的组成
工厂方法模式通常由以下几部分组成:
- 产品接口(Product):定义了所有产品的共有接口,这样可以让工厂方法创建的对象具有一致的接口。
- 具体产品(Concrete Product):实现产品接口的类,代表具体的产品。
- 工厂接口(Creator):声明了一个工厂方法用于创建产品对象。工厂方法的返回类型通常是产品接口类型。
- 具体工厂(Concrete Creator):实现工厂接口,并定义具体产品的实例化方法。
2.2 工厂方法模式示例
假设我们有一个应用程序,需要根据不同的需求创建不同类型的日志记录器(如文件日志记录器和控制台日志记录器),可以使用工厂方法模式来实现。
2.2.1 定义产品接口
class Logger
{
public:
virtual void log(const std::string & message) = 0; // 纯虚函数
virtual ~Logger() = default;
};
2.2.2 实现具体产品
实现两种具体的日志记录器:FileLogger
和 ConsoleLogger
,它们都实现了 Logger
接口。
class FileLogger : public Logger {
public:
void log(const std::string& message) override {
// 模拟写入文件日志
std::cout << "Logging to a file: " << message << std::endl;
}
};
class ConsoleLogger : public Logger {
public:
void log(const std::string& message) override {
// 模拟输出到控制台
std::cout << "Logging to console: " << message << std::endl;
}
};
2.2.3 定义工厂接口
定义一个工厂类,声明创建日志记录器的接口。
class LoggerFactory {
public:
virtual Logger* createLogger() = 0; // 工厂方法
virtual ~LoggerFactory() = default;
};
2.2.4 实现具体工厂
具体工厂类分别创建不同类型的日志记录器实例。
class FileLoggerFactory : public LoggerFactory {
public:
Logger* createLogger() override {
return new FileLogger();
}
};
class ConsoleLoggerFactory : public LoggerFactory {
public:
Logger* createLogger() override {
return new ConsoleLogger();
}
};
2.2.5 客户端代码
客户端使用工厂类来创建特定的日志记录器,而不需要直接依赖具体的日志记录器类。
void clientCode(LoggerFactory& factory) {
Logger* logger = factory.createLogger(); // 获取特定的 Logger
logger->log("This is a test log message.");
delete logger;
}
int main() {
FileLoggerFactory fileLoggerFactory;
ConsoleLoggerFactory consoleLoggerFactory;
clientCode(fileLoggerFactory); // 使用文件日志记录器
clientCode(consoleLoggerFactory); // 使用控制台日志记录器
return 0;
}
2.3 工厂方法模式的优缺点
优点:
- 符合开闭原则:通过引入新产品子类和对应的具体工厂类,可以在不修改现有代码的情况下添加新功能。
- 减少耦合:客户端代码通过工厂接口创建产品对象,而不是依赖于具体产品类。
- 增强扩展性:可以通过创建新的工厂类和产品类,轻松添加新的产品类型。
缺点:
- 增加类的数量:每增加一种新的产品类型,就需要添加一个具体产品类和对应的工厂类。
- 系统复杂度增加:因为每个具体工厂仅负责一个具体产品的创建,类的数量增多,增加了系统维护成本。
2.4 工厂方法模式的适用场景
- 需要创建复杂对象:当对象的创建逻辑复杂且需要复用时。
- 增加灵活性:不想让客户端代码依赖于具体类,避免代码频繁修改。
- 希望根据条件创建不同对象:例如根据配置、参数等条件动态决定实例化哪种对象。
3.抽象工厂(Abstract Factory)
提供一个接口,用于创建一系列相关或依赖的对象,而无需指定具体类。
抽象工厂模式(Abstract Factory Pattern)是一种创建型设计模式,用于创建一组相关或相互依赖的对象,而不指定它们的具体类。抽象工厂模式提供了一个接口来创建系列相关的对象,让客户端代码能够使用这些对象而不必关心具体的实现细节。
3.1 抽象工厂模式的组成
抽象工厂模式包含以下几个主要部分:
- 抽象工厂(Abstract Factory):声明了一组用于创建不同系列产品的接口。
- 具体工厂(Concrete Factory):实现抽象工厂接口,定义创建特定系列产品的方法。
- 抽象产品(Abstract Product):为产品对象声明接口,使不同的产品实现具有一致性。
- 具体产品(Concrete Product):实现抽象产品接口,由具体工厂创建,形成同一系列的对象。
- 客户端(Client):使用抽象工厂和抽象产品接口来生成具体产品对象,无需关心具体工厂和具体产品的实现细节。
3.2 示例
假设我们要设计一个 UI 工具包,它可以在不同平台上(如 Windows 和 MacOS)创建不同的 UI 元素(如按钮和文本框)。使用抽象工厂模式来创建不同平台的 UI 控件,可以确保在某个平台的控件风格保持一致。
3.2.1 定义抽象产品接口
// 抽象产品 - 按钮接口
class Button {
public:
virtual void paint() const = 0;
virtual ~Button() = default;
};
// 抽象产品 - 文本框接口
class TextBox {
public:
virtual void render() const = 0;
virtual ~TextBox() = default;
};
3.2.2 定义具体产品
// Windows 平台的具体产品
class WindowsButton : public Button {
public:
void paint() const override {
std::cout << "Render a button in Windows style." << std::endl;
}
};
class WindowsTextBox : public TextBox {
public:
void render() const override {
std::cout << "Render a text box in Windows style." << std::endl;
}
};
// MacOS 平台的具体产品
class MacOSButton : public Button {
public:
void paint() const override {
std::cout << "Render a button in MacOS style." << std::endl;
}
};
class MacOSTextBox : public TextBox {
public:
void render() const override {
std::cout << "Render a text box in MacOS style." << std::endl;
}
};
3.2.3 定义抽象工厂
class GUIFactory {
public:
virtual Button* createButton() const = 0;
virtual TextBox* createTextBox() const = 0;
virtual ~GUIFactory() = default;
};
3.2.4 定义具体工厂
class WindowsFactory : public GUIFactory {
public:
Button* createButton() const override {
return new WindowsButton();
}
TextBox* createTextBox() const override {
return new WindowsTextBox();
}
};
class MacOSFactory : public GUIFactory {
public:
Button* createButton() const override {
return new MacOSButton();
}
TextBox* createTextBox() const override {
return new MacOSTextBox();
}
};
3.2.5 客户端代码
客户端代码通过抽象工厂接口创建具体产品,但并不直接依赖于某一平台的具体产品。
void clientCode(const GUIFactory& factory) {
Button* button = factory.createButton();
TextBox* textBox = factory.createTextBox();
button->paint();
textBox->render();
delete button;
delete textBox;
}
int main() {
// 假设当前运行在 Windows 环境
WindowsFactory windowsFactory;
clientCode(windowsFactory);
// 假设当前运行在 MacOS 环境
MacOSFactory macFactory;
clientCode(macFactory);
return 0;
}
3.3 抽象工厂模式的优缺点
优点:
- 符合开闭原则:可以在不修改客户端代码的情况下,添加新的产品族或平台。
- 保证产品族的一致性:适用于要求一组对象(产品族)在一起使用,确保相同风格或一致性的场景。
- 简化客户端代码:客户端代码不直接依赖具体产品类,仅依赖抽象工厂和抽象产品接口,减少耦合。
缺点:
- 复杂性增加:增加了类的数量,可能会让系统结构复杂。
- 难以支持新产品:如果需要向现有的产品族中添加新的产品(如新的控件类型),则需要修改所有的具体工厂类,违反开闭原则。
3.4 适用场景
- 需要创建一系列相互关联的对象:如不同平台的 UI 控件、不同数据库的访问对象等。
- 需要保证产品的一致性:确保客户端始终使用同一产品族,避免跨平台或产品间的兼容性问题。
4.建造者(Builder)
分步骤构造复杂对象,将构造与表示分离。
建造者模式(Builder Pattern)是一种创建型设计模式,用于分步骤构建一个复杂对象。建造者模式将对象的构建过程与表示分离,使得相同的构建过程可以创建不同的对象。它特别适合需要按照一系列步骤来创建复杂对象的场景,并且这些对象在创建过程中可能具有不同的表现或配置。
4.1 建造者模式的组成
建造者模式由以下几个部分组成:
- 产品(Product):表示要创建的复杂对象。通常包含多个部件,可以通过建造者逐步构建。
- 抽象建造者(Builder):定义构建产品各部件的接口,具体部件由其子类实现。
- 具体建造者(Concrete Builder):实现抽象建造者接口,负责创建和装配具体产品的各个部件。
- 指挥者(Director):负责按照特定顺序调用建造者的方法,以构建产品。它只关注如何构建产品,而不涉及产品的具体组成细节。
4.2 示例
假设我们要创建一个包含多个组件的电脑对象(Computer
),每个电脑可以由 CPU、GPU、存储设备等组成,不同配置的电脑可能由不同的建造者来构造。
4.2.1 定义产品类
#include <string>
#include <iostream>
class Computer {
public:
void setCPU(const std::string& cpu) { CPU = cpu; }
void setGPU(const std::string& gpu) { GPU = gpu; }
void setStorage(const std::string& storage) { Storage = storage; }
void showSpecs() const {
std::cout << "Computer specs: CPU = " << CPU
<< ", GPU = " << GPU
<< ", Storage = " << Storage << std::endl;
}
private:
std::string CPU;
std::string GPU;
std::string Storage;
};
4.2.2 定义抽象建造者
class ComputerBuilder {
public:
virtual ~ComputerBuilder() = default;
virtual void buildCPU() = 0;
virtual void buildGPU() = 0;
virtual void buildStorage() = 0;
virtual Computer* getComputer() = 0;
};
4.2.3 定义具体建造者
定义具体建造者类,负责创建特定配置的电脑(例如,游戏电脑和办公电脑)
class GamingComputerBuilder : public ComputerBuilder {
public:
GamingComputerBuilder() { computer = new Computer(); }
~GamingComputerBuilder() { delete computer; }
void buildCPU() override { computer->setCPU("High-end CPU"); }
void buildGPU() override { computer->setGPU("High-end GPU"); }
void buildStorage() override { computer->setStorage("1TB SSD"); }
Computer* getComputer() override { return computer; }
private:
Computer* computer;
};
class OfficeComputerBuilder : public ComputerBuilder {
public:
OfficeComputerBuilder() { computer = new Computer(); }
~OfficeComputerBuilder() { delete computer; }
void buildCPU() override { computer->setCPU("Mid-range CPU"); }
void buildGPU() override { computer->setGPU("Integrated GPU"); }
void buildStorage() override { computer->setStorage("512GB SSD"); }
Computer* getComputer() override { return computer; }
private:
Computer* computer;
};
4.2.4 定义指挥者
指挥者通过调用建造者的方法,按照特定顺序来构造产品。
class Director {
public:
void setBuilder(ComputerBuilder* builder) { this->builder = builder; }
Computer* buildComputer() {
builder->buildCPU();
builder->buildGPU();
builder->buildStorage();
return builder->getComputer();
}
private:
ComputerBuilder* builder;
};
4.2.5 客户端代码
在客户端代码中,创建不同的建造者,通过指挥者来构造不同配置的电脑
int main() {
Director director;
GamingComputerBuilder gamingBuilder;
director.setBuilder(&gamingBuilder);
Computer* gamingComputer = director.buildComputer();
gamingComputer->showSpecs();
delete gamingComputer;
OfficeComputerBuilder officeBuilder;
director.setBuilder(&officeBuilder);
Computer* officeComputer = director.buildComputer();
officeComputer->showSpecs();
delete officeComputer;
return 0;
}
4.3 建造者模式的优缺点
优点:
- 控制构造过程:可以分步骤创建复杂对象。
- 易于扩展:可以在不修改客户端代码的情况下,创建不同种类的复杂对象。
- 解耦构造和表示:客户端无需关心对象的具体构造细节。
缺点:
- 增加代码复杂度:需要创建多个具体建造者类,每个建造者负责一种特定的对象创建过程。
4.4 适用场景
- 需要创建复杂对象:对象的构建过程涉及多个步骤,且这些步骤可以灵活组合。
- 需要多种表现形式:相同的构建过程可以生成不同的表示(不同配置的对象)。
5.原型(Prototype)
使用一个现有实例作为原型,复制这个实例生成新对象。
原型模式(Prototype Pattern)是一种创建型设计模式,通过复制(克隆)现有的对象来生成新对象,而不是通过实例化类来创建新对象。原型模式适用于对象的创建成本较高或较复杂的情况,因为它通过复制已有对象的方式快速生成新的实例,避免了反复的初始化操作。
5.1 原型模式的组成
原型模式的核心在于复制自身的能力,因此主要包含以下角色:
- 原型接口(Prototype Interface):定义了克隆自身的方法(通常称为
clone
)。 - 具体原型(Concrete Prototype):实现原型接口,提供复制自身的方法(具体类的克隆逻辑)。
- 客户端(Client):通过原型接口复制对象,而不是直接调用构造函数来实例化新对象。
5.2 原型模式的实现
假设我们有一个“形状”类层次结构,每种形状(如圆形和矩形)都有自己的特性。我们希望能够快速复制这些形状,而不需要从头进行构造。
5.2.1 定义原型接口
首先,定义一个原型接口类,并定义一个纯虚函数 clone()
,返回类型是当前类的指针,代表生成的克隆对象。
#include <iostream>
#include <memory>
class Shape {
public:
virtual ~Shape() = default;
virtual std::unique_ptr<Shape> clone() const = 0; // 克隆方法
virtual void draw() const = 0; // 具体行为
};
5.2.2 定义具体原型
继承 Shape
接口,并实现 clone()
方法。在这个方法中创建当前对象的深拷贝
class Circle : public Shape {
public:
Circle(double radius) : radius(radius) {}
// 实现克隆方法
std::unique_ptr<Shape> clone() const override {
return std::make_unique<Circle>(*this);
}
void draw() const override {
std::cout << "Drawing a Circle with radius " << radius << std::endl;
}
private:
double radius;
};
class Rectangle : public Shape {
public:
Rectangle(double width, double height) : width(width), height(height) {}
// 实现克隆方法
std::unique_ptr<Shape> clone() const override {
return std::make_unique<Rectangle>(*this);
}
void draw() const override {
std::cout << "Drawing a Rectangle with width " << width << " and height " << height << std::endl;
}
private:
double width;
double height;
};
注:std::make_unique
是 C++14 引入的一个函数模板,用于创建一个 std::unique_ptr
对象。std::unique_ptr
是一种智能指针,它管理一个动态分配的对象的生命周期,确保对象在不再使用时自动释放内存,从而避免内存泄漏
5.2.3 客户端代码
在客户端代码中,可以利用 clone()
方法复制形状对象,而无需了解具体形状的内部实现细节。
void clientCode() {
std::unique_ptr<Shape> circle = std::make_unique<Circle>(5.0);
std::unique_ptr<Shape> rectangle = std::make_unique<Rectangle>(4.0, 6.0);
// 克隆对象
std::unique_ptr<Shape> anotherCircle = circle->clone();
std::unique_ptr<Shape> anotherRectangle = rectangle->clone();
// 打印输出
circle->draw();
anotherCircle->draw();
rectangle->draw();
anotherRectangle->draw();
}
int main() {
clientCode();
return 0;
}
5.3 原型模式的优缺点
优点:
- 克隆比实例化更高效:适合用于创建成本高的复杂对象。
- 减少子类数量:可以通过克隆现有对象并修改其属性来创建新对象,而不需要为每种配置创建新子类。
- 简化创建过程:可以在运行时动态决定创建哪些对象的副本。
缺点:
- 深拷贝和浅拷贝问题:实现
clone()
方法时需要考虑对象的深拷贝或浅拷贝问题,尤其是当对象中包含复杂的动态成员时。 - 实现难度较高:对某些对象,特别是涉及多层嵌套或指针的对象,克隆的实现较为复杂。
5.4 适用场景
- 需要大量相似对象:如在游戏中生成大量相似的 NPC。
- 创建成本较高的对象:比如数据库连接、复杂图形或计算成本较高的对象。
二.结构型设计模式
结构型模式关注如何将对象和类组装成更大的结构。
6.适配器(Adapter)
将不兼容的接口转换为兼容接口,使得原本不能在一起工作的类可以协同工作。
适配器模式(Adapter Pattern)是一种结构型设计模式,它通过将一个接口转换成客户端所期望的另一个接口,使得原本因接口不兼容而无法一起工作的类可以协同工作。适配器模式通常用于以下情况:
- 当你想要使用一些现有的类,但它们的接口不符合你的需求时。
- 当你需要创建一个可以与其他类一起使用的新类,但该新类不能直接使用这些类的接口时。
6.1 适配器模式的组成
适配器模式通常包含以下几个角色:
- 目标接口(Target Interface):定义客户端所需的接口。
- 适配者(Adaptee):需要被适配的类,其接口不符合客户端的需求。
- 适配器(Adapter):实现目标接口并持有适配者的引用,负责将客户端请求转发到适配者对象。
6.2 适配器模式的实现
下面通过一个简单的例子来演示适配器模式的工作原理。假设我们有一个游戏应用程序,需要让一个 Duck
类(鸭子)和一个 Turkey
类(火鸡)可以在相同的上下文中使用。
6.2.1 定义目标接口
我们首先定义一个 Duck
接口,作为目标接口。
class Duck {
public:
virtual void quack() = 0; // 鸭子的叫声
virtual void fly() = 0; // 鸭子的飞行
};
6.2.2 定义适配者
接下来,定义一个 Turkey
类,作为需要被适配的类。
class Turkey {
public:
void gobble() {
std::cout << "Gobble gobble!" << std::endl; // 火鸡的叫声
}
void fly() {
std::cout << "I'm flying a short distance!" << std::endl; // 火鸡的飞行
}
};
6.2.3 实现适配器
实现一个 TurkeyAdapter
类,将 Turkey
类适配到 Duck
接口。
class TurkeyAdapter : public Duck {
public:
TurkeyAdapter(Turkey* turkey) : turkey(turkey) {}
void quack() override {
turkey->gobble(); // 将火鸡的叫声映射到鸭子的叫声
}
void fly() override {
turkey->fly(); // 直接使用火鸡的飞行
}
private:
Turkey* turkey; // 持有一个火鸡的引用
};
6.3.4 客户端代码
客户端代码使用 Duck
接口,与 TurkeyAdapter
进行交互。
void testDuck(Duck* duck) {
duck->quack();
duck->fly();
}
int main() {
Turkey* turkey = new Turkey();
Duck* turkeyAdapter = new TurkeyAdapter(turkey);
// 测试火鸡适配器作为鸭子
testDuck(turkeyAdapter);
// 清理内存
delete turkeyAdapter;
delete turkey;
return 0;
}
6.3 适配器模式的优缺点
优点:
- 接口兼容:可以使不兼容的接口相互协作,增强了代码的可复用性。
- 松耦合:客户端与适配者之间是松耦合的,改变适配者的实现不会影响客户端。
- 可扩展性:可以很容易地添加新的适配者,而不需要修改现有的代码。
缺点:
- 增加了复杂性:适配器模式可能会导致系统的复杂性增加,因为引入了额外的类和层次结构。
- 可能影响性能:在某些情况下,使用适配器可能会对性能产生影响,特别是在调用频繁时,因为它增加了方法调用的开销。
6.4 适用场景
- 当你需要使用一些现有的类,但它们的接口不符合你的需求。
- 当你希望创建一个可以与多个不同类一起工作的新类,而不需要修改它们的代码。
- 当你想要使用一些第三方库或类时,而它们与系统中的接口不兼容。
适配器模式是一种灵活且广泛应用的设计模式,可以帮助开发者在不改变现有代码的情况下,轻松实现接口的转换。
7.桥接(Bridge)
将抽象部分与其实现部分分离,使它们可以独立变化。
桥接模式(Bridge Pattern)是一种结构型设计模式,旨在将抽象与实现分离,使它们可以独立变化。这个模式通过引入一个桥接接口,允许在运行时切换实现。桥接模式的主要目的是降低系统的复杂度,提高灵活性和可扩展性。
7.1 组成部分
桥接模式通常包含以下几个角色:
- 抽象类(Abstraction):定义抽象接口,并包含一个对实现部分的引用。
- 扩展抽象类(Refined Abstraction):继承自抽象类,提供更具体的实现。
- 实现接口(Implementor):定义实现类的接口,不一定与抽象类的接口完全一致。
- 具体实现类(Concrete Implementor):实现实现接口,提供具体的功能。
7.2 桥接模式的实现
通过一个简单的示例来演示桥接模式。假设我们要创建一个绘图工具,支持不同的形状(如圆形和正方形)以及不同的颜色(如红色和蓝色)。
7.2.1 定义实现接口
首先定义一个颜色实现接口。
class Color {
public:
virtual void applyColor() = 0; // 应用颜色
};
class Red : public Color {
public:
void applyColor() override {
std::cout << "Applying red color." << std::endl;
}
};
class Blue : public Color {
public:
void applyColor() override {
std::cout << "Applying blue color." << std::endl;
}
};
7.2.2 定义抽象类
接下来,定义一个形状抽象类。
class Shape {
protected:
Color* color; // 持有一个颜色实现的引用
public:
Shape(Color* c) : color(c) {} // 构造函数
virtual void draw() = 0; // 抽象方法
};
7.2.3 扩展抽象类
然后实现具体的形状类,如圆形和正方形。
class Circle : public Shape {
public:
Circle(Color* c) : Shape(c) {} // 通过构造函数传入颜色实现
void draw() override {
std::cout << "Drawing Circle." << std::endl;
color->applyColor(); // 应用颜色
}
};
class Square : public Shape {
public:
Square(Color* c) : Shape(c) {} // 通过构造函数传入颜色实现
void draw() override {
std::cout << "Drawing Square." << std::endl;
color->applyColor(); // 应用颜色
}
};
7.3.4 客户端代码
在客户端代码中,可以灵活地创建不同的形状和颜色的组合。
int main() {
Color* red = new Red();
Color* blue = new Blue();
Shape* circle = new Circle(red);
Shape* square = new Square(blue);
// 绘制形状
circle->draw(); // 输出: Drawing Circle. Applying red color.
square->draw(); // 输出: Drawing Square. Applying blue color.
// 清理内存
delete circle;
delete square;
delete red;
delete blue;
return 0;
}
7.3 桥接模式的优缺点
优点:
- 分离抽象与实现:可以独立扩展抽象和实现部分,从而提高了系统的灵活性和可扩展性。
- 避免了多重继承的复杂性:通过组合而不是继承来实现不同的实现部分,避免了因多重继承而导致的复杂性。
- 易于扩展:可以轻松添加新的实现或抽象,而不需要修改现有的代码。
缺点:
- 增加了系统复杂性:由于引入了更多的类,桥接模式可能会导致系统的复杂性增加。
- 需要对两个层次的设计:设计时需要考虑抽象和实现两部分,可能会增加设计的难度。
7.4 适用场景
- 当你需要在多个维度上进行扩展时,比如图形库中的形状和颜色。
- 当你想要避免使用多重继承来组合不同的类时。
- 当你希望通过组合而不是继承来实现灵活的系统设计时。
7.5 总结
桥接模式通过将抽象与实现分离,提供了一种灵活的解决方案,使得系统可以在不增加复杂性的情况下进行扩展。这种模式特别适合需要处理多个维度的系统,如图形、形状和颜色等,使得代码更加模块化和可维护。
注:桥接模式主要关注于将抽象与实现分离,而工厂模式则专注于对象的创建和管理。
8.组合(Composite)
将对象组合成树形结构,表示“部分-整体”层次结构,使得客户端可以统一对待单个对象和组合对象。
组合模式(Composite Pattern)是一种结构型设计模式,旨在将对象组合成树形结构以表示“部分-整体”的层次关系。组合模式允许客户以一致的方式处理单个对象和对象集合,使得客户端可以统一处理复杂的树形结构,简化代码的复杂性。
8.1 主要角色
组合模式通常包含以下几个角色:
- 组件接口(Component):
- 声明所有的具体组件(叶子节点和组合节点)所共有的接口,可能包含一些方法,如
add()
、remove()
和display()
等。
- 声明所有的具体组件(叶子节点和组合节点)所共有的接口,可能包含一些方法,如
- 叶子节点(Leaf):
- 实现组件接口,代表树的叶子节点。叶子节点不再有子节点,因此在这里实现具体的行为。
- 组合节点(Composite):
- 也实现组件接口,代表树的组合节点,可以包含叶子节点或其他组合节点。组合节点负责管理子节点并实现相关的行为。
8.2 示例
以下是一个简单的组合模式示例,模拟一个文件系统的结构,支持文件(叶子节点)和文件夹(组合节点)。
8.2.1 定义组件接口
#include <iostream>
#include <vector>
#include <string>
// 组件接口
class FileSystemComponent {
public:
virtual void showInfo() = 0; // 显示信息
virtual ~FileSystemComponent() = default; // 虚析构函数
};
8.2.2 实现叶子节点(文件)
class File : public FileSystemComponent {
private:
std::string name; // 文件名
public:
File(const std::string& name) : name(name) {}
void showInfo() override {
std::cout << "File: " << name << std::endl; // 显示文件信息
}
};
8.2.3 实现组合节点(文件夹)
class Folder : public FileSystemComponent {
private:
std::string name; // 文件夹名
std::vector<FileSystemComponent*> children; // 存储子节点
public:
Folder(const std::string& name) : name(name) {}
void add(FileSystemComponent* component) {
children.push_back(component); // 添加子节点
}
void remove(FileSystemComponent* component) {
// 从子节点中移除
children.erase(std::remove(children.begin(), children.end(), component), children.end());
}
void showInfo() override {
std::cout << "Folder: " << name << std::endl; // 显示文件夹信息
for (const auto& child : children) {
child->showInfo(); // 递归显示子节点信息
}
}
};
8.2.4 客户端代码
在客户端代码中,可以创建文件和文件夹,并显示它们的结构。
int main() {
FileSystemComponent* root = new Folder("Root Folder");
FileSystemComponent* folder1 = new Folder("Folder 1");
FileSystemComponent* folder2 = new Folder("Folder 2");
FileSystemComponent* file1 = new File("File 1.txt");
FileSystemComponent* file2 = new File("File 2.txt");
FileSystemComponent* file3 = new File("File 3.txt");
// 构建文件系统结构
folder1->add(file1);
folder1->add(file2);
root->add(folder1);
root->add(folder2);
folder2->add(file3);
// 显示文件系统信息
root->showInfo();
// 清理内存
delete file1;
delete file2;
delete file3;
delete folder1;
delete folder2;
delete root;
return 0;
}
8.3 组合模式的优缺点
优点:
- 简化客户端代码:客户端可以统一处理单个对象和组合对象,简化了代码结构。
- 树形结构的灵活性:可以自由组合和嵌套对象,支持复杂的层次结构。
- 易于扩展:可以添加新的叶子节点和组合节点,系统的扩展性强。
缺点:
- 过度使用组合模式:可能导致系统的设计变得过于复杂,尤其是在组件接口过于庞大的情况下。
- 性能开销:如果组合结构过于复杂,可能会对性能产生一定影响。
8.4 适用场景
- 需要表示“部分-整体”层次结构的场景,例如文件系统、图形界面组件、组织结构等。
- 希望客户端能够以统一的方式处理单个对象和对象集合的场景。
- 在需要对组合对象进行递归操作时,如遍历树形结构。
8.5 总结
组合模式通过将对象组合成树形结构,使得客户端可以以一致的方式处理单个对象和对象集合。它适用于需要表示“部分-整体”关系的场景,提高了代码的可维护性和扩展性。
9.装饰器(Decorator)
动态地给对象添加新的功能。与继承不同,装饰器可以在运行时选择性地添加行为。
装饰器模式(Decorator Pattern)是一种结构型设计模式,用于动态地给对象添加额外的功能。装饰器模式通过将对象包装在一个或多个装饰类中,以增加其功能,而无需修改原始对象的结构。这种模式非常灵活,允许用户在运行时选择要添加的装饰,从而实现功能的扩展。
9.1 主要角色
装饰器模式通常包含以下几个角色:
- 组件接口(Component):
- 定义一个接口,用于描述被装饰的对象的行为。
- 具体组件(Concrete Component):
- 实现组件接口,表示需要被装饰的对象。可以有多个具体组件。
- 装饰器基类(Decorator):
- 实现组件接口,持有一个组件对象的引用,并定义与具体组件相同的接口。装饰器可以扩展功能。
- 具体装饰器(Concrete Decorator):
- 继承自装饰器基类,添加具体的功能或行为。可以有多个具体装饰器,每个装饰器都可以添加不同的功能。
9.2 示例
以下是一个简单的装饰器模式示例,模拟一个文本消息的装饰器,可以给文本添加不同的样式。
9.2.1 定义组件接口
#include <iostream>
#include <string>
// 组件接口
class Message {
public:
virtual std::string getContent() const = 0; // 获取内容
virtual ~Message() = default; // 虚析构函数
};
9.2.2 实现具体组件
class SimpleMessage : public Message {
private:
std::string content; // 消息内容
public:
SimpleMessage(const std::string& msg) : content(msg) {}
std::string getContent() const override {
return content; // 返回简单消息内容
}
};
9.2.3 实现装饰器基类
class MessageDecorator : public Message {
protected:
Message* message; // 持有一个消息对象的引用
public:
MessageDecorator(Message* msg) : message(msg) {}
virtual std::string getContent() const override {
return message->getContent(); // 调用被装饰对象的内容
}
};
9.2.4 实现具体装饰器
class HtmlMessage : public MessageDecorator {
public:
HtmlMessage(Message* msg) : MessageDecorator(msg) {}
std::string getContent() const override {
return "<html>" + message->getContent() + "</html>"; // 添加HTML标签
}
};
class EncryptedMessage : public MessageDecorator {
public:
EncryptedMessage(Message* msg) : MessageDecorator(msg) {}
std::string getContent() const override {
return "Encrypted: " + message->getContent(); // 添加加密标识
}
};
9.2.5 客户端代码
在客户端代码中,可以创建消息对象,并使用装饰器添加不同的功能。
int main() {
Message* message = new SimpleMessage("Hello, World!"); // 创建简单消息
// 添加HTML装饰
Message* htmlMessage = new HtmlMessage(message);
std::cout << htmlMessage->getContent() << std::endl; // 输出: <html>Hello, World!</html>
// 添加加密装饰
Message* encryptedMessage = new EncryptedMessage(htmlMessage);
std::cout << encryptedMessage->getContent() << std::endl; // 输出: Encrypted: <html>Hello, World!</html>
// 清理内存
delete encryptedMessage; // 先删除最外层的装饰器
delete htmlMessage; // 然后删除HTML装饰
delete message; // 最后删除简单消息
return 0;
}
9.3 装饰器模式的优缺点
优点:
- 灵活性:可以动态添加和组合功能,不需要修改原始对象的代码。
- 扩展性:可以通过增加新的装饰类来扩展功能,遵循开闭原则(对扩展开放,对修改封闭)。
- 清晰的责任分离:每个装饰器可以负责一项功能,责任明确,便于管理。
缺点:
- 复杂性:增加了系统的复杂性,特别是当装饰类较多时,可能导致代码难以理解。
- 性能开销:每个装饰器会增加额外的调用开销,影响性能。
9.4 适用场景
- 需要在不修改对象的情况下,动态添加或修改对象的功能。
- 需要通过组合方式为对象增加功能,而不影响其他对象。
- 在需要扩展类的功能时,尤其是在类的层次结构中,增加了额外的类或功能。
9.5 总结
装饰器模式通过引入装饰器类,为对象提供动态的功能扩展。它使得功能的添加变得灵活且可组合,从而实现了更好的代码复用和维护性。适用于需要对对象进行多种功能扩展的场景,能够在运行时决定所需的功能组合。
10.外观(Facade)
为一组接口提供一个简单的统一接口,使子系统更加易用。
外观模式(Facade Pattern)是一种结构型设计模式,旨在为复杂的子系统提供一个简化的接口。通过外观模式,用户可以通过一个统一的接口与复杂的系统交互,而无需了解系统的内部细节。这种模式有助于减少系统的复杂性,提高代码的可维护性和可理解性。
10.1 主要角色
外观模式通常包含以下几个角色:
- 外观类(Facade):
- 提供简化的接口,封装复杂子系统的功能。
- 子系统类(Subsystem):
- 实现系统的核心功能,外观类通过它们提供服务。
- 客户端(Client):
- 通过外观类与复杂子系统进行交互。
10.2 示例
以下是一个使用外观模式的简单示例,模拟一个音响系统的使用场景。
10.2.1 定义子系统类
#include <iostream>
// 子系统类A
class Amplifier {
public:
void on() {
std::cout << "Amplifier is on." << std::endl;
}
void off() {
std::cout << "Amplifier is off." << std::endl;
}
};
// 子系统类B
class DVDPlayer {
public:
void on() {
std::cout << "DVD Player is on." << std::endl;
}
void play() {
std::cout << "DVD is playing." << std::endl;
}
void stop() {
std::cout << "DVD is stopped." << std::endl;
}
void off() {
std::cout << "DVD Player is off." << std::endl;
}
};
// 子系统类C
class Projector {
public:
void on() {
std::cout << "Projector is on." << std::endl;
}
void off() {
std::cout << "Projector is off." << std::endl;
}
};
10.2.2 实现外观类
// 外观类
class HomeTheaterFacade {
private:
Amplifier amplifier;
DVDPlayer dvdPlayer;
Projector projector;
public:
HomeTheaterFacade() {}
void watchMovie() {
projector.on();
amplifier.on();
dvdPlayer.on();
dvdPlayer.play();
}
void endMovie() {
dvdPlayer.stop();
amplifier.off();
projector.off();
}
};
10.2.3 客户端代码
在客户端代码中,通过外观类与复杂的子系统交互。
int main() {
HomeTheaterFacade homeTheater; // 创建外观对象
homeTheater.watchMovie(); // 启动观影模式
std::cout << std::endl;
homeTheater.endMovie(); // 结束观影模式
return 0;
}
10.3 外观模式的优缺点
优点:
- 简化接口:外观模式提供了一个简单的接口,减少了系统的复杂性,用户无需了解系统的内部实现。
- 解耦:客户端与子系统之间的依赖关系减少,降低了模块之间的耦合度。
- 提升可维护性:由于外观类封装了复杂的子系统,修改子系统的实现时只需关注外观类,不会影响客户端。
缺点:
- 不适合所有场景:外观模式可能会限制系统的扩展性,某些特定的操作可能需要直接与子系统交互。
- 可能引入冗余:外观类可能会成为一个“胖类”,承担过多的责任,导致维护困难。
10.4 适用场景
- 当需要为复杂子系统提供一个统一的接口时。
- 当要简化客户端与子系统之间的交互时。
- 在开发大型系统时,有助于隐藏系统的复杂性并降低耦合度。
10.5 总结
外观模式通过引入一个统一的接口,使得复杂子系统的使用变得简单和直观。它提高了代码的可维护性和可理解性,同时降低了模块间的耦合。外观模式在构建大型系统、库或框架时非常有用,能够有效管理和简化系统的复杂性。
11.享元(Flyweight)
通过共享对象减少内存使用。适用于大量相似对象的情况。
享元模式(Flyweight Pattern)是一种结构型设计模式,旨在通过共享对象来减少内存使用和提高性能。享元模式特别适用于需要大量相似对象的场景。通过重用现有对象,享元模式可以显著减少系统所需的内存空间和对象创建的开销。
11.1 主要角色
享元模式通常包含以下几个角色:
- 享元接口(Flyweight):
- 定义享元对象的接口,通常包含一些可共享的方法。
- 具体享元(Concrete Flyweight):
- 实现享元接口,表示可共享的对象。具体享元对象应该是不可变的(或只读的),以便在共享时不会发生状态变化。
- 享元工厂(Flyweight Factory):
- 负责创建和管理享元对象。它确保在需要时返回相同的享元实例,而不是每次都创建新的实例。
- 外部状态(External State):
- 由于享元模式的对象通常是不可变的,所以需要将对象的可变状态提取到外部,以确保多个对象共享同一状态。
11.2 示例
以下是一个使用享元模式的简单示例,模拟一个文本编辑器中的字符对象的共享。
11.2.1 定义享元接口
#include <iostream>
#include <unordered_map>
#include <memory>
// 享元接口
class Character {
public:
virtual void display(int fontSize) const = 0; // 显示字符的方法
virtual ~Character() = default; // 虚析构函数
};
11.2.2 实现具体享元
// 具体享元
class ConcreteCharacter : public Character {
private:
char symbol; // 字符
public:
ConcreteCharacter(char s) : symbol(s) {}
void display(int fontSize) const override {
std::cout << "Character: " << symbol << ", Font size: " << fontSize << std::endl;
}
};
11.2.3 实现享元工厂
// 享元工厂
class CharacterFactory {
private:
std::unordered_map<char, std::shared_ptr<Character>> characters; // 字符缓存
public:
std::shared_ptr<Character> getCharacter(char c) {
// 检查缓存中是否存在字符
if (characters.find(c) == characters.end()) {
// 如果不存在,创建新的具体享元并缓存
characters[c] = std::make_shared<ConcreteCharacter>(c);
}
return characters[c]; // 返回共享的字符
}
};
11.2.4 客户端代码
在客户端代码中,可以使用享元工厂创建字符对象,并在需要时进行共享。
int main() {
CharacterFactory factory; // 创建享元工厂
// 通过工厂获取字符对象
auto characterA = factory.getCharacter('A');
auto characterB = factory.getCharacter('B');
auto characterC = factory.getCharacter('C');
auto characterA2 = factory.getCharacter('A'); // 重用字符A
// 使用字符对象
characterA->display(12);
characterB->display(14);
characterC->display(16);
characterA2->display(18); // 使用重用的字符A
return 0;
}
11.3 享元模式的优缺点
优点:
- 节省内存:通过共享对象,显著减少内存占用,尤其是在创建大量相似对象的情况下。
- 提高性能:减少了对象的创建和销毁次数,提高了程序的性能。
- 符合开闭原则:可以通过增加新的享元类而不影响已有的代码。
缺点:
- 复杂性:引入了额外的复杂性,特别是在管理共享对象时。
- 维护外部状态:外部状态的管理可能会使代码更加复杂,可能会影响可读性。
11.4 适用场景
- 当需要大量相似对象时,尤其是对象状态大部分是相同的。
- 当对象的创建和销毁成本较高时,享元模式可以显著提高性能。
- 在需要节省内存的场景,尤其是内存资源有限的应用程序。
11.5 总结
享元模式通过共享对象来减少内存占用和提高性能,特别适合于需要大量相似对象的场景。它通过分离对象的内部状态和外部状态,优化了对象的管理和使用。尽管享元模式增加了一定的复杂性,但在资源受限或性能敏感的应用中,它仍然是一种非常有效的设计模式。
12.代理(Proxy)
为其他对象提供代理,控制对该对象的访问。代理模式常用于懒加载、访问控制等场景。
代理模式(Proxy Pattern)是一种结构型设计模式,提供了一个代理对象,以控制对另一个对象的访问。通过代理模式,可以在不改变原始对象的情况下,增加对该对象的控制和功能。代理模式通常用于以下几种场景:
- 延迟加载(Lazy Loading):在实际需要时才创建对象,节省资源。
- 访问控制(Access Control):控制对原始对象的访问权限。
- 日志记录(Logging):在对原始对象的方法调用前后添加日志记录。
- 远程代理(Remote Proxy):为远程对象提供本地代理。
12.1 主要角色
代理模式通常包含以下几个角色:
- 抽象主题(Subject):
- 定义了代理和真实对象的共同接口。
- 真实主题(Real Subject):
- 实现了抽象主题接口,表示被代理的对象。
- 代理(Proxy):
- 维护对真实主题对象的引用,并实现抽象主题接口。代理可以在调用真实对象的方法时添加额外的功能。
12.2 示例
以下是一个使用代理模式的简单示例,模拟图像加载的场景。
12.2.1 定义抽象主题
#include <iostream>
// 抽象主题
class Image {
public:
virtual void display() = 0; // 显示图像的方法
virtual ~Image() = default; // 虚析构函数
};
12.2.2 实现真实主题
// 真实主题
class RealImage : public Image {
private:
std::string filename; // 图像文件名
public:
RealImage(const std::string& file) : filename(file) {
loadImageFromDisk(); // 加载图像
}
void display() override {
std::cout << "Displaying " << filename << std::endl;
}
private:
void loadImageFromDisk() {
std::cout << "Loading " << filename << std::endl; // 模拟加载
}
};
12.2.3 实现代理
// 代理
class ProxyImage : public Image {
private:
RealImage* realImage; // 指向真实主题的指针
std::string filename; // 图像文件名
public:
ProxyImage(const std::string& file) : realImage(nullptr), filename(file) {}
void display() override {
// 延迟加载
if (!realImage) {
realImage = new RealImage(filename); // 只在需要时加载图像
}
realImage->display(); // 调用真实主题的显示方法
}
~ProxyImage() {
delete realImage; // 释放资源
}
};
12.2.4 客户端代码
在客户端代码中,可以使用代理对象来代替真实对象。
int main() {
Image* image = new ProxyImage("test.jpg"); // 创建代理对象
image->display(); // 第一次调用,加载图像
std::cout << std::endl;
image->display(); // 第二次调用,不再加载
delete image; // 释放代理对象
return 0;
}
12.3 代理模式的优缺点
优点:
- 控制访问:可以通过代理控制对真实对象的访问,增加安全性。
- 延迟加载:在需要时才创建真实对象,节省资源。
- 附加功能:可以在代理中增加附加的功能,如日志、缓存等。
缺点:
- 性能开销:代理可能会增加调用的开销,尤其是在代理逻辑复杂的情况下。
- 增加复杂性:引入代理对象会增加代码的复杂性。
12.4 适用场景
- 当需要控制对某个对象的访问时。
- 当想要为某个对象添加额外的功能而不改变其实现时。
- 当需要实现延迟加载的场景时。
12.5 总结
代理模式通过引入代理对象来控制对真实对象的访问,提供了灵活性和额外功能,尤其适用于访问控制、延迟加载和附加功能等场景。尽管代理模式增加了一定的复杂性,但在需要优化资源使用和保护真实对象的情况下,它仍然是一种非常有效的设计模式。
三.行为型设计模式
行为型模式关注对象如何协作完成任务,封装和管理对象之间的通信。
13.责任链(Chain of Responsibility)
将请求沿着一条链传递,链上的每个处理者可以选择处理请求或传递给下一个处理者。
责任链模式(Chain of Responsibility Pattern)是一种行为型设计模式,用于避免请求发送者与多个请求处理者之间的耦合关系。该模式通过将请求沿着处理链传递,让多个处理者都有机会处理请求,从而将请求的处理责任分散到多个对象中。每个处理者决定是否处理请求,或者将请求转发给下一个处理者。
13.1 主要角色
责任链模式通常包含以下几个角色:
- 处理者(Handler):
- 定义处理请求的接口,并包含一个指向下一个处理者的引用。每个处理者负责处理特定类型的请求。
- 具体处理者(Concrete Handler):
- 实现处理请求的逻辑,如果该处理者可以处理请求,则执行相应操作;否则,将请求转发给下一个处理者。
- 客户端(Client):
- 发送请求的对象,通常会创建处理链并设置请求的起始处理者。
13.2 示例
以下是一个使用责任链模式的简单示例,模拟一个支持不同级别请求的审批系统。
13.2.1 定义处理者接口
#include <iostream>
#include <string>
// 抽象处理者
class Approver {
public:
virtual void setNext(Approver* next) = 0; // 设置下一个处理者
virtual void processRequest(const std::string& request, int amount) = 0; // 处理请求
virtual ~Approver() = default; // 虚析构函数
};
13.2.2 实现具体处理者
// 具体处理者 - 经理
class Manager : public Approver {
private:
Approver* next; // 指向下一个处理者
public:
void setNext(Approver* next) override {
this->next = next;
}
void processRequest(const std::string& request, int amount) override {
if (amount < 1000) {
std::cout << "Manager approved " << request << " of amount " << amount << std::endl;
} else if (next) {
next->processRequest(request, amount); // 转发请求
}
}
};
// 具体处理者 - 总监
class Director : public Approver {
private:
Approver* next;
public:
void setNext(Approver* next) override {
this->next = next;
}
void processRequest(const std::string& request, int amount) override {
if (amount < 5000) {
std::cout << "Director approved " << request << " of amount " << amount << std::endl;
} else if (next) {
next->processRequest(request, amount);
}
}
};
// 具体处理者 - CEO
class CEO : public Approver {
private:
Approver* next;
public:
void setNext(Approver* next) override {
this->next = next;
}
void processRequest(const std::string& request, int amount) override {
std::cout << "CEO approved " << request << " of amount " << amount << std::endl;
}
};
13.2.3 客户端代码
在客户端代码中,可以构建处理链并发送请求。
int main() {
// 创建处理者
Manager* manager = new Manager();
Director* director = new Director();
CEO* ceo = new CEO();
// 建立责任链
manager->setNext(director);
director->setNext(ceo);
// 发送请求
manager->processRequest("Purchase Request", 500); // 经理处理
manager->processRequest("Purchase Request", 3000); // 总监处理
manager->processRequest("Purchase Request", 10000); // CEO处理
// 清理资源
delete manager;
delete director;
delete ceo;
return 0;
}
13.3 责任链模式的优缺点
优点:
- 解耦合:请求发送者和处理者之间不需要直接耦合,增加了系统的灵活性。
- 动态改变处理链:可以在运行时动态地改变处理链,增加或删除处理者。
- 职责分配:可以将请求处理的责任分配给多个对象,简化了请求处理逻辑。
缺点:
- 无法保证请求被处理:由于请求可能在链中被丢弃,客户端可能无法得知请求的处理状态。
- 调试困难:由于请求在多个处理者之间传递,调试时可能会比较困难。
- 性能开销:链的长度过长可能会导致性能问题,特别是处理复杂请求时。
13.4 适用场景
- 需要处理的请求在多个对象之间有多个处理者,且每个处理者可以选择是否处理该请求。
- 希望将请求处理的责任解耦,并提供动态的处理链。
- 需要实现多个对象之间的分配责任。
13.5 总结
责任链模式通过将请求在多个处理者之间传递,使得请求的处理责任可以灵活地分配,增加了系统的灵活性和可扩展性。它适用于请求处理逻辑复杂且需要多个处理者的场景,能够有效地解耦请求发送者和处理者之间的关系。
14.命令(Command)
将请求封装成对象,解耦请求发送者与执行者。
命令模式(Command Pattern)是一种行为型设计模式,它将请求封装为对象,从而使您可以用不同的请求对客户端进行参数化、排队或记录请求日志。它还支持可撤销的操作。命令模式涉及几个主要角色:
14.1 主要角色
- 命令接口(Command):
- 声明了执行操作的接口。
- 具体命令(Concrete Command):
- 实现命令接口,并定义与接收者之间的绑定关系。它调用接收者的方法以实现请求。
- 接收者(Receiver):
- 负责实际执行与请求相关的操作。
- 调用者(Invoker):
- 请求的发起者,持有命令对象并调用命令对象的
execute
方法。
- 请求的发起者,持有命令对象并调用命令对象的
- 客户端(Client):
- 创建具体命令对象并设置其接收者。
14.2 示例
以下是一个使用命令模式的简单示例,模拟一个遥控器控制家用电器的场景。
14.2.1 定义命令接口
// 命令接口
class Command {
public:
virtual void execute() = 0; // 执行命令
virtual ~Command() = default; // 虚析构函数
};
14.2.2 定义接收者
#include <iostream>
// 接收者 - 灯
class Light {
public:
void turnOn() {
std::cout << "The light is on." << std::endl;
}
void turnOff() {
std::cout << "The light is off." << std::endl;
}
};
14.2.3 实现具体命令
// 具体命令 - 打开灯
class LightOnCommand : public Command {
private:
Light* light; // 持有接收者的引用
public:
LightOnCommand(Light* light) : light(light) {}
void execute() override {
light->turnOn(); // 调用接收者的方法
}
};
// 具体命令 - 关闭灯
class LightOffCommand : public Command {
private:
Light* light;
public:
LightOffCommand(Light* light) : light(light) {}
void execute() override {
light->turnOff();
}
};
14.2.4 定义调用者
// 调用者 - 遥控器
class RemoteControl {
private:
Command* command; // 持有命令的引用
public:
void setCommand(Command* command) {
this->command = command; // 设置命令
}
void pressButton() {
if (command) {
command->execute(); // 执行命令
}
}
};
14.2.5 客户端代码
在客户端代码中,您可以创建命令、接收者和调用者,并通过调用者执行命令。
int main() {
Light* light = new Light(); // 创建接收者
Command* lightOn = new LightOnCommand(light); // 创建打开灯的命令
Command* lightOff = new LightOffCommand(light); // 创建关闭灯的命令
RemoteControl* remote = new RemoteControl(); // 创建遥控器
// 打开灯
remote->setCommand(lightOn);
remote->pressButton();
// 关闭灯
remote->setCommand(lightOff);
remote->pressButton();
// 清理资源
delete lightOn;
delete lightOff;
delete remote;
delete light;
return 0;
}
14.3 命令模式的优缺点
优点:
- 解耦请求发起者和请求接收者:命令模式将请求的发送者与接收者解耦,使它们可以独立变化。
- 支持撤销和重做操作:命令对象可以存储状态,允许实现撤销和重做的功能。
- 易于扩展:添加新命令只需实现新的命令类,而无需更改现有代码。
- 可以支持日志记录:可以记录请求以支持操作的日志功能。
缺点:
- 增加系统复杂性:命令模式引入了许多类,可能导致系统结构变得复杂。
- 可能会导致类膨胀:每个命令都需要一个类,如果命令种类繁多,可能会导致大量类的生成。
14.4 适用场景
- 需要将请求调用的发起者和接收者解耦。
- 希望支持撤销和重做操作。
- 需要记录请求的日志或实现宏命令(将多个命令组合成一个命令)。
- 希望使用不同的请求对客户端进行参数化。
14.5 总结
命令模式通过将请求封装为对象,使得请求的发送者与接收者解耦,增加了系统的灵活性和可扩展性。它适用于需要记录操作、支持撤销/重做和希望将请求处理逻辑与请求发起者分离的场景。
15.解释器(Interpreter)
为语言创建解释器,定义语言的语法表示和解释器。适合用于构建简单的脚本或规则引擎。
解释器模式(Interpreter Pattern)是一种行为型设计模式,用于定义一种语言的文法表示,并提供一个解释器来处理该文法。这个模式的主要目的是将某种特定的语言表达式转化为可执行的操作,常用于需要处理特定语法或格式的应用场景,比如编程语言、数据库查询、表达式求值等。
15.1 主要角色
- 抽象表达式(Abstract Expression):
- 声明一个用于解释的接口。
- 终结符表达式(Terminal Expression):
- 实现了抽象表达式接口,表示文法的基本元素。终结符表达式通常对应于文法中的叶子节点。
- 非终结符表达式(Non-terminal Expression):
- 也实现了抽象表达式接口,表示文法中的组合规则。它通常由其他表达式组成。
- 上下文(Context):
- 包含解释器所需的信息,通常用于存储输入数据。
15.2 示例
以下是一个使用解释器模式的简单示例,模拟对基本数学表达式的解析和计算。
15.2.1 定义抽象表达式
class Expression {
public:
virtual int interpret() const = 0; // 解释接口
virtual ~Expression() = default; // 虚析构函数
};
15.2.2 实现终结符表达式
#include <string>
#include <map>
// 终结符表达式 - 变量
class VariableExpression : public Expression {
private:
std::string name; // 变量名
std::map<std::string, int>& context; // 上下文引用
public:
VariableExpression(const std::string& name, std::map<std::string, int>& context)
: name(name), context(context) {}
int interpret() const override {
// 根据上下文返回变量的值
return context[name];
}
};
15.2.3 实现非终结符表达式
// 非终结符表达式 - 加法
class AddExpression : public Expression {
private:
Expression* left; // 左操作数
Expression* right; // 右操作数
public:
AddExpression(Expression* left, Expression* right)
: left(left), right(right) {}
int interpret() const override {
return left->interpret() + right->interpret(); // 执行加法
}
~AddExpression() {
delete left; // 清理内存
delete right;
}
};
// 非终结符表达式 - 减法
class SubtractExpression : public Expression {
private:
Expression* left;
Expression* right;
public:
SubtractExpression(Expression* left, Expression* right)
: left(left), right(right) {}
int interpret() const override {
return left->interpret() - right->interpret(); // 执行减法
}
~SubtractExpression() {
delete left;
delete right;
}
};
15.2.4 客户端代码
在客户端代码中,您可以创建表达式并执行解析。
#include <iostream>
int main() {
std::map<std::string, int> context; // 上下文存储变量
context["x"] = 5;
context["y"] = 10;
// 创建表达式: x + y - 2
Expression* expression = new SubtractExpression(
new AddExpression(new VariableExpression("x", context), new VariableExpression("y", context)),
new VariableExpression("2", context)
);
std::cout << "Result: " << expression->interpret() << std::endl; // 输出结果
delete expression; // 清理内存
return 0;
}
15.3 解释器模式的优缺点
优点:
- 简化代码:通过将文法规则封装为对象,代码变得更加清晰和易于理解。
- 易于扩展:可以通过添加新的终结符和非终结符类来扩展新的文法规则,而无需修改现有代码。
- 实现简洁:对于简单的文法,解释器模式实现起来相对简单。
缺点:
- 性能问题:对于复杂的文法,可能会导致性能问题,因为每个表达式都需要创建一个对象。
- 难以维护:如果文法规则复杂,可能导致系统的维护变得困难,因为类的数量可能会显著增加。
- 适用性限制:只适合用来处理简单的语法;复杂的语言解析通常更适合使用编译器设计技术。
15.4 适用场景
- 需要对某种语言的语法进行解释和执行。
- 需要解析简单的文法规则,如计算表达式。
- 需要支持新的文法规则扩展。
15.5 总结
解释器模式为处理和解释特定语言或格式提供了一种灵活的解决方案。它将文法规则与执行逻辑分离,使得扩展和维护变得更加容易。然而,解释器模式通常只适用于简单的文法,对于复杂的语法解析,更适合使用其他编译原理的方法。
tips:文法(区别语法:特指文法中的结构规则,描述如何正确地组合符号和单词来形成有效的程序或表达式)
在计算机科学中,文法通常指的是编程语言或数据格式的语法规则。它定义了合法的语句和表达式的结构。这种文法通常用形式语言的规则来表示,包括:
- 终结符(Terminal Symbols):文法中最基本的符号,无法被进一步分解。
- 非终结符(Non-terminal Symbols):可以被替换为一个或多个终结符或其他非终结符的符号。
- 产生式(Production Rules):描述了非终结符如何被替换为其他符号的规则。
常见的文法类型包括:
- 上下文无关文法(Context-Free Grammar, CFG):用于描述编程语言的语法,常用于编译器。
- 正则文法(Regular Grammar):用于描述正则表达式。
16.迭代器(Iterator)
提供一种方法来顺序访问集合中的元素,而不暴露底层结构。
迭代器(Iterator)是一种设计模式和编程概念,用于提供对集合(如数组、列表、树等)中元素的顺序访问,而无需暴露集合的内部结构。迭代器模式允许开发者在不直接操作集合的情况下,遍历集合中的元素,从而提供了一种统一的访问接口。
16.1 主要组成部分
- 迭代器接口:
- 定义用于访问集合元素的方法,如
next()
、hasNext()
等。
- 定义用于访问集合元素的方法,如
- 具体迭代器:
- 实现了迭代器接口,维护当前的遍历状态(如当前位置)并实现具体的遍历逻辑。
- 聚合类(集合):
- 提供创建迭代器的接口,通常会有一个方法返回一个新的迭代器实例。
16.2 迭代器的基本功能
迭代器通常提供以下基本功能:
- 初始化:将迭代器设置到集合的开始位置。
- 访问元素:提供方法获取当前元素。
- 移动到下一个元素:提供方法移动到下一个元素。
- 检查是否有下一个元素:提供方法检查是否还有更多元素可以访问。
16.3 C++ 中的迭代器
在 C++ 中,标准模板库(STL)提供了强大的迭代器支持,迭代器可以用于遍历容器(如 std::vector
、std::list
、std::map
等)。
以下是一个简单的例子,展示了如何使用 C++ 的迭代器遍历一个 std::vector
:
#include <iostream>
#include <vector>
int main() {
// 创建一个整数向量
std::vector<int> numbers = {1, 2, 3, 4, 5};
// 使用迭代器遍历向量
std::vector<int>::iterator it;
for (it = numbers.begin(); it != numbers.end(); ++it) {
std::cout << *it << " "; // 输出当前元素
}
std::cout << std::endl;
return 0;
}
16.4 C++ 迭代器的类型
在 C++ 中,迭代器有不同的类型,具体包括:
- 输入迭代器(Input Iterator):
- 只允许读取数据,通常用于单次遍历。
- 输出迭代器(Output Iterator):
- 只允许写入数据,通常用于向集合中添加元素。
- 前向迭代器(Forward Iterator):
- 支持多次遍历,但只能向前移动。
- 双向迭代器(Bidirectional Iterator):
- 支持向前和向后移动,适用于如
std::list
等容器。
- 支持向前和向后移动,适用于如
- 随机访问迭代器(Random Access Iterator):
- 支持快速随机访问,允许在容器中任意位置移动,适用于如
std::vector
等容器。
- 支持快速随机访问,允许在容器中任意位置移动,适用于如
16.5 迭代器的优点
- 封装性:迭代器隐藏了集合的具体实现,使得集合的内部结构对外部代码透明。
- 统一接口:通过相同的接口访问不同类型的集合,增强了代码的灵活性和可重用性。
- 简化代码:提供了简洁的方式遍历集合,减少了使用索引的复杂性。
16.6 迭代器的缺点
- 性能开销:某些情况下,使用迭代器可能会带来额外的性能开销,尤其是在频繁创建和销毁迭代器的场景中。
- 复杂性:对于某些复杂数据结构,迭代器的实现可能会增加系统的复杂性。
16.7总结
迭代器是一种重要的设计模式,用于简化集合的遍历操作。通过提供统一的接口,迭代器不仅提高了代码的可读性和可维护性,还使得对不同类型集合的操作变得更加一致。迭代器在现代 C++ 编程中被广泛使用,是 STL 的核心部分之一。
17.中介者(Mediator)
使用一个中介对象来管理对象之间的交互,从而减少类之间的耦合。
中介者(Mediator)是一种设计模式,旨在减少对象之间的直接通信,从而降低它们之间的耦合。通过引入一个中介者对象来协调各个对象的交互,中介者模式可以使得对象之间的通信变得更加清晰、灵活,并且易于维护。
17.1 主要组成部分
- 中介者接口:
- 定义一个接口,声明所有参与者(同事对象)之间的交互方法。
- 具体中介者:
- 实现中介者接口,并维护对所有参与者的引用。负责协调参与者之间的交互。
- 同事类:
- 参与者(对象),它们通过中介者进行交互,而不是直接相互通信。
17.2 中介者的工作原理
- 同事对象通过中介者对象进行通信,而不是直接彼此交互。
- 中介者负责协调同事对象之间的行为和交互,处理它们之间的复杂关系。
- 通过中介者,可以在不修改同事对象的情况下,添加新的同事或改变交互规则。
17.3 示例
下面是一个使用中介者模式的简单示例,模拟一个聊天系统中的用户和聊天室的交互:
#include <iostream>
#include <string>
#include <vector>
// 前向声明
class Mediator;
// 同事类
class User {
public:
User(Mediator* mediator, const std::string& name) : mediator(mediator), name(name) {}
void sendMessage(const std::string& message);
void receiveMessage(const std::string& message) {
std::cout << name << " received: " << message << std::endl;
}
private:
Mediator* mediator;
std::string name;
};
// 中介者接口
class Mediator {
public:
virtual void sendMessage(const std::string& message, User* user) = 0;
};
// 具体中介者
class ChatRoom : public Mediator {
public:
void addUser(User* user) {
users.push_back(user);
}
void sendMessage(const std::string& message, User* user) override {
for (auto u : users) {
if (u != user) { // 不发给发送者
u->receiveMessage(message);
}
}
}
private:
std::vector<User*> users; // 参与者列表
};
// 同事类方法实现
void User::sendMessage(const std::string& message) {
std::cout << name << " sends: " << message << std::endl;
mediator->sendMessage(message, this);
}
// 主程序
int main() {
ChatRoom chatRoom;
User alice(&chatRoom, "Alice");
User bob(&chatRoom, "Bob");
chatRoom.addUser(&alice);
chatRoom.addUser(&bob);
alice.sendMessage("Hello, Bob!");
bob.sendMessage("Hi, Alice!");
return 0;
}
17.4 中介者模式的优点
- 降低耦合:
- 中介者模式通过将对象之间的交互封装到中介者中,减少了对象之间的直接依赖,从而降低了耦合度。
- 集中管理:
- 所有的交互逻辑集中在中介者中,易于维护和扩展。
- 灵活性:
- 可以方便地添加新的同事类或修改现有的交互规则,而不需要修改其他同事类的代码。
- 清晰的交互:
- 通过中介者的引入,系统中的对象交互变得更加清晰,易于理解。
17.5 中介者模式的缺点
- 单点故障:
- 中介者作为中央协调者,如果出现问题,可能会影响到所有同事对象的交互。
- 复杂性增加:
- 如果中介者变得过于复杂,可能会导致代码的可维护性下降。
- 性能开销:
- 引入中介者可能会增加一定的性能开销,尤其是在需要频繁交互的场景中。
17.6 总结
中介者模式是一种有效的设计模式,适用于复杂对象之间的交互场景。通过将对象之间的交互逻辑集中到中介者中,可以减少对象之间的耦合,提高系统的灵活性和可维护性。中介者模式在GUI框架、消息传递系统和事件处理系统等场景中广泛应用。
18.备忘录(Memento)
在不暴露内部状态的情况下保存对象的状态,以便稍后恢复。常用于实现撤销功能。
备忘录(Memento)是一种设计模式,主要用于在不违反封装性的情况下,捕获和保存一个对象的内部状态,以便在将来能够恢复到这个状态。备忘录模式的核心思想是提供一个机制,允许对象在未来的某个时间点恢复其之前的状态,而无需公开其内部实现细节。
18.1 主要组成部分
- 备忘录(Memento):
- 用于存储对象的内部状态。备忘录对象通常是不可变的,以确保其状态在被外部对象访问时不会被修改。
- 发起人(Originator):
- 需要保存状态的对象。发起人可以创建一个备忘录对象来保存其当前状态,也可以从备忘录中恢复状态。
- 管理者(Caretaker):
- 负责管理备忘录,保存备忘录的对象。管理者不允许直接访问备忘录的内容,而只是存储和恢复备忘录。
18.2 备忘录的工作原理
- 发起人创建一个备忘录,保存其当前状态。
- 管理者存储备忘录。
- 当需要恢复状态时,发起人可以使用备忘录恢复到之前的状态。
18.3 示例
以下是一个使用备忘录模式的简单示例,模拟文本编辑器的撤销操作:
#include <iostream>
#include <string>
#include <memory>
// 备忘录类
class Memento {
public:
Memento(const std::string& state) : state(state) {}
std::string getState() const {
return state;
}
private:
std::string state; // 存储状态
};
// 发起人类
class Originator {
public:
void setState(const std::string& state) {
this->state = state;
std::cout << "Setting state to: " << state << std::endl;
}
std::unique_ptr<Memento> saveStateToMemento() {
return std::make_unique<Memento>(state); // 保存当前状态
}
void getStateFromMemento(const Memento& memento) {
state = memento.getState(); // 从备忘录恢复状态
std::cout << "Restored state to: " << state << std::endl;
}
private:
std::string state; // 发起人的状态
};
// 管理者类
class Caretaker {
public:
void saveState(Originator& originator) {
memento = originator.saveStateToMemento(); // 保存发起人的状态
}
void restoreState(Originator& originator) {
if (memento) {
originator.getStateFromMemento(*memento); // 从备忘录恢复状态
}
}
private:
std::unique_ptr<Memento> memento; // 存储备忘录
};
// 主程序
int main() {
Originator originator;
Caretaker caretaker;
originator.setState("State 1");
caretaker.saveState(originator);
originator.setState("State 2");
caretaker.saveState(originator);
originator.setState("State 3");
// 恢复到之前的状态
caretaker.restoreState(originator); // 恢复到 State 2
caretaker.restoreState(originator); // 恢复到 State 1
return 0;
}
18.4 备忘录模式的优点
- 封装性:
- 备忘录模式可以在不暴露对象内部状态的情况下保存和恢复状态,保护了对象的封装性。
- 简化撤销操作:
- 备忘录模式特别适合实现撤销(Undo)功能,可以方便地保存和恢复状态。
- 历史记录:
- 可以保存多个状态的备忘录,使得对象可以在多个历史状态之间切换。
18.5 备忘录模式的缺点
- 内存开销:
- 如果状态数据很大,可能会消耗较多内存来存储多个备忘录。
- 性能问题:
- 频繁创建和销毁备忘录可能会影响性能。
- 实现复杂性:
- 在某些情况下,管理多个备忘录可能会使得实现变得复杂。
18.6 总结
备忘录模式是一种重要的设计模式,适用于需要保存和恢复对象状态的场景,尤其是在实现撤销操作时。通过将状态保存和恢复的逻辑封装在备忘录中,备忘录模式不仅可以保护对象的内部状态,还能使得对象之间的交互变得更加灵活。
19.观察者(Observer)
定义对象之间的依赖关系,便于在一个对象发生变化时通知依赖的对象。
观察者模式(Observer Pattern)是一种行为型设计模式,定义了一种一对多的依赖关系,让多个观察者对象能够同时监听和响应一个主题对象的状态变化。这个模式通常用于实现事件处理系统或消息发布-订阅机制。
19.1 主要组成部分
- 主题(Subject):
- 主题是被观察的对象,负责维护一组观察者并通知它们状态的变化。主题通常提供添加、移除观察者的接口。
- 观察者(Observer):
- 观察者是需要对主题的状态变化做出反应的对象。每个观察者都实现一个更新接口,以接收主题的通知。
- 具体主题(Concrete Subject):
- 具体主题实现了主题接口,维护观察者的集合,并在其状态变化时通知所有观察者。
- 具体观察者(Concrete Observer):
- 具体观察者实现了观察者接口,定义了在接收到主题通知时应执行的具体操作。
19.2 工作原理
- 观察者注册到主题中。
- 当主题的状态发生变化时,它会通知所有注册的观察者。
- 观察者接收到通知后,根据主题的状态执行相应的操作。
19.3 示例
以下是一个使用观察者模式的简单示例,模拟天气监测系统:
#include <iostream>
#include <vector>
#include <string>
// 观察者接口
class Observer {
public:
virtual void update(float temperature, float humidity) = 0;
};
// 主题接口
class Subject {
public:
virtual void registerObserver(Observer* observer) = 0;
virtual void removeObserver(Observer* observer) = 0;
virtual void notifyObservers() = 0;
};
// 具体主题
class WeatherData : public Subject {
private:
std::vector<Observer*> observers;
float temperature;
float humidity;
public:
void registerObserver(Observer* observer) override {
observers.push_back(observer);
}
void removeObserver(Observer* observer) override {
observers.erase(std::remove(observers.begin(), observers.end(), observer), observers.end());
}
void notifyObservers() override {
for (Observer* observer : observers) {
observer->update(temperature, humidity);
}
}
void setMeasurements(float temp, float hum) {
temperature = temp;
humidity = hum;
notifyObservers(); // 通知所有观察者
}
};
// 具体观察者
class CurrentConditionsDisplay : public Observer {
private:
float temperature;
float humidity;
public:
void update(float temp, float hum) override {
temperature = temp;
humidity = hum;
display();
}
void display() {
std::cout << "Current conditions: " << temperature << "°C and " << humidity << "% humidity" << std::endl;
}
};
// 主程序
int main() {
WeatherData weatherData;
CurrentConditionsDisplay currentDisplay;
weatherData.registerObserver(¤tDisplay);
weatherData.setMeasurements(30.0f, 65.0f);
weatherData.setMeasurements(28.0f, 70.0f);
return 0;
}
19.4 观察者模式的优点
- 解耦:
- 观察者模式将主题和观察者解耦,主题不需要知道观察者的具体实现,从而提高了模块之间的独立性。
- 动态关系:
- 观察者可以在运行时动态注册和注销,提供了灵活的对象间关系。
- 多重观察者:
- 主题可以通知多个观察者,适合于一对多的场景。
19.5 观察者模式的缺点
- 性能问题:
- 当观察者数量较多或通知频繁时,可能会导致性能下降。
- 循环依赖:
- 如果观察者中也注册了主题,可能会导致循环依赖和无限循环通知的问题。
- 复杂性:
- 在实现时可能需要处理观察者的注册、注销和通知逻辑,增加了系统的复杂性。
19.6 应用场景
观察者模式广泛应用于需要广播通信的场景,如:
- GUI 事件处理系统。
- 数据监控系统(如股票、天气等)。
- MVC(模型-视图-控制器)架构中,模型可以作为主题,视图作为观察者。
- 发布-订阅系统。
19.7 总结
观察者模式是一种强大的设计模式,用于处理对象之间的依赖关系。通过使用观察者模式,可以有效地管理对象之间的交互,使得代码更加灵活和可维护。
20.状态(State)
允许对象在状态变化时更改其行为。通常是通过改变其内部状态类来实现的。
状态模式(State Pattern)是一种行为型设计模式,它允许对象在其内部状态发生变化时改变其行为。换句话说,状态模式使得一个对象的行为可以根据其状态的不同而变化,适用于那些在不同状态下表现出不同行为的对象。
20.1 主要组成部分
- 上下文(Context):
- 上下文维护一个对状态对象的引用,通常是一个接口类型。上下文会委托状态对象来处理具体的行为。
- 状态接口(State):
- 定义了与上下文相关的状态所需的接口,通常包含一些用于处理状态转换的方法。
- 具体状态(Concrete State):
- 实现状态接口的类,每个具体状态类都实现了在该状态下应执行的行为。
20.2 工作原理
- 当上下文的状态发生变化时,它会切换到另一个具体状态对象。
- 状态对象可以在自己的内部逻辑中决定何时切换到另一个状态。
- 上下文将请求委托给当前的状态对象。
20.3 示例
以下是一个简单的示例,演示了一个文本编辑器的状态:草稿、提交和审核。
#include <iostream>
#include <memory>
// 状态接口
class DocumentState {
public:
virtual void draft() = 0;
virtual void submit() = 0;
virtual void review() = 0;
virtual ~DocumentState() = default;
};
// 上下文
class Document {
private:
std::unique_ptr<DocumentState> state;
public:
Document(std::unique_ptr<DocumentState> initialState) : state(std::move(initialState)) {}
void setState(std::unique_ptr<DocumentState> newState) {
state = std::move(newState);
}
void draft() {
state->draft();
}
void submit() {
state->submit();
}
void review() {
state->review();
}
};
// 具体状态:草稿状态
class DraftState : public DocumentState {
public:
void draft() override {
std::cout << "Document is already in draft state." << std::endl;
}
void submit() override {
std::cout << "Submitting the document." << std::endl;
}
void review() override {
std::cout << "Can't review, first submit the document." << std::endl;
}
};
// 具体状态:提交状态
class SubmittedState : public DocumentState {
public:
void draft() override {
std::cout << "Can't edit, document is submitted." << std::endl;
}
void submit() override {
std::cout << "Document is already submitted." << std::endl;
}
void review() override {
std::cout << "Reviewing the submitted document." << std::endl;
}
};
// 具体状态:审核状态
class ReviewState : public DocumentState {
public:
void draft() override {
std::cout << "Can't edit, document is under review." << std::endl;
}
void submit() override {
std::cout << "Can't submit, document is under review." << std::endl;
}
void review() override {
std::cout << "Document is already under review." << std::endl;
}
};
// 主程序
int main() {
Document doc(std::make_unique<DraftState>());
doc.draft(); // Document is already in draft state.
doc.submit(); // Submitting the document.
// 状态转换
doc.setState(std::make_unique<SubmittedState>());
doc.submit(); // Document is already submitted.
doc.review(); // Reviewing the submitted document.
// 状态转换
doc.setState(std::make_unique<ReviewState>());
doc.review(); // Document is already under review.
return 0;
}
20.4 状态模式的优点
- 简化代码:
- 状态模式将与状态相关的行为集中在状态类中,避免了使用大量的条件语句(如if-else或switch)。
- 易于扩展:
- 可以轻松地添加新状态,只需创建新的状态类并实现状态接口,而不需要修改现有代码。
- 状态封装:
- 每个状态的行为都被封装在相应的状态类中,符合单一职责原则。
20.5 状态模式的缺点
- 类数量增加:
- 每个状态通常需要一个类,可能导致类的数量增加,增加系统的复杂性。
- 状态转移管理:
- 状态转移的逻辑可能会变得复杂,特别是在多个状态之间存在相互依赖时。
20.6 应用场景
状态模式适用于以下场景:
- 需要根据对象的内部状态改变其行为的场合。
- 复杂的状态转移逻辑,适合通过状态模式进行封装。
- 需要动态改变对象的状态并调整行为的应用。
20.7 总结
状态模式提供了一种灵活的方式来处理对象状态的变化。通过使用状态模式,可以使代码更加清晰、易于维护和扩展。它在许多应用中都得到了广泛的应用,特别是在需要处理复杂状态的情况下。
21.策略(Strategy)
定义一系列算法,将它们封装在可互换的类中,使得算法可以独立于使用它的客户端变化。
策略模式(Strategy Pattern)是一种行为型设计模式,它允许在运行时选择算法的行为。通过将算法的实现放在独立的策略类中,策略模式使得客户端可以根据需要在运行时选择和切换不同的策略。这样可以避免使用大量的条件语句(如if-else或switch)来选择算法的实现,提高代码的可维护性和可扩展性。
21.1 主要组成部分
- 策略接口(Strategy):
- 定义了一系列算法或操作的方法接口,通常会有一个执行的方法。
- 具体策略(Concrete Strategy):
- 实现策略接口的具体类,每个具体策略类定义了不同的算法或操作。
- 上下文(Context):
- 持有对策略接口的引用,用于在运行时调用策略的行为。上下文可以动态地切换所使用的策略。
21.2 工作原理
- 客户端创建一个上下文,并传入所需的具体策略。
- 上下文调用策略的执行方法,具体策略类的实现决定了实际的行为。
- 客户端可以随时更改上下文中的策略,以便在运行时选择不同的行为。
21.3 示例
以下是一个简单的示例,展示了不同的排序策略(如冒泡排序和快速排序):
#include <iostream>
#include <vector>
#include <algorithm>
// 策略接口
class SortStrategy {
public:
virtual void sort(std::vector<int>& data) = 0;
virtual ~SortStrategy() = default;
};
// 具体策略:冒泡排序
class BubbleSort : public SortStrategy {
public:
void sort(std::vector<int>& data) override {
std::cout << "Using Bubble Sort." << std::endl;
for (size_t i = 0; i < data.size(); ++i) {
for (size_t j = 0; j < data.size() - 1 - i; ++j) {
if (data[j] > data[j + 1]) {
std::swap(data[j], data[j + 1]);
}
}
}
}
};
// 具体策略:快速排序
class QuickSort : public SortStrategy {
public:
void sort(std::vector<int>& data) override {
std::cout << "Using Quick Sort." << std::endl;
std::sort(data.begin(), data.end());
}
};
// 上下文
class Sorter {
private:
SortStrategy* strategy;
public:
Sorter(SortStrategy* strat) : strategy(strat) {}
void setStrategy(SortStrategy* strat) {
strategy = strat;
}
void sort(std::vector<int>& data) {
strategy->sort(data);
}
};
// 主程序
int main() {
std::vector<int> data = {5, 3, 8, 1, 2};
Sorter sorter(new BubbleSort());
sorter.sort(data); // 使用冒泡排序
sorter.setStrategy(new QuickSort());
sorter.sort(data); // 使用快速排序
return 0;
}
21.4 策略模式的优点
- 消除了条件语句:
- 策略模式将条件语句移到策略类中,使得代码更加清晰。
- 易于扩展:
- 可以方便地添加新策略,只需实现新的策略类,而不需要修改现有代码。
- 算法的封装:
- 每个算法被封装在独立的策略类中,符合单一职责原则。
- 动态选择算法:
- 客户端可以在运行时选择不同的算法,增加了灵活性。
21.5 策略模式的缺点
- 类的数量增加:
- 每个策略通常需要一个类,可能导致类的数量增加。
- 客户端需要了解所有策略:
- 客户端需要知道可用的策略类,可能导致客户端变得复杂。
21.6 应用场景
策略模式适用于以下场景:
- 需要在运行时选择算法的场合。
- 有多个算法可以相互替换,且客户端希望能够自由切换的场合。
- 需要消除条件语句,增加代码的可维护性和可读性。
21.7 总结
策略模式提供了一种灵活的方式来定义和选择算法。通过使用策略模式,代码可以更加清晰和易于扩展,使得在不同的上下文中选择合适的算法成为可能。策略模式在许多应用中都得到了广泛的应用,特别是在需要多种可替换算法的情况下。
22.模板方法(Template Method)
定义算法框架,允许子类重写特定步骤,而不改变算法的结构。
模板方法模式(Template Method Pattern)是一种行为型设计模式,它定义了一个算法的骨架,将一些步骤的实现延迟到子类中。通过这种方式,模板方法允许子类在不改变算法结构的情况下重新定义算法的某些特定步骤。这个模式的关键在于,父类提供了算法的基本框架,而具体的实现则由子类完成。
22.1 主要组成部分
- 抽象类(Abstract Class):
- 定义了一个模板方法(template method),包含了算法的基本结构。模板方法通常是一个公共的方法,包含了一系列的调用顺序。
- 包含一些抽象方法,这些方法需要在子类中实现,以便完成具体的操作。
- 具体类(Concrete Class):
- 实现抽象类中定义的抽象方法,提供具体的操作步骤。
22.2 工作原理
- 客户端调用模板方法,该方法按照固定的步骤调用抽象方法和具体实现的方法。
- 具体类提供了算法的具体实现,用户可以通过继承来扩展或修改算法的某些部分。
22.3 示例
以下是一个简单的示例,展示了制作饮料的过程,其中不同的饮料有不同的制作方式(如茶和咖啡):
#include <iostream>
// 抽象类
class CaffeineBeverage {
public:
// 模板方法
void prepareRecipe() {
boilWater();
brew();
pourInCup();
addCondiments();
}
protected:
// 抽象方法
virtual void brew() = 0;
virtual void addCondiments() = 0;
private:
void boilWater() {
std::cout << "Boiling water" << std::endl;
}
void pourInCup() {
std::cout << "Pouring into cup" << std::endl;
}
};
// 具体类:茶
class Tea : public CaffeineBeverage {
protected:
void brew() override {
std::cout << "Steeping the tea" << std::endl;
}
void addCondiments() override {
std::cout << "Adding lemon" << std::endl;
}
};
// 具体类:咖啡
class Coffee : public CaffeineBeverage {
protected:
void brew() override {
std::cout << "Dripping coffee through filter" << std::endl;
}
void addCondiments() override {
std::cout << "Adding sugar and milk" << std::endl;
}
};
// 主程序
int main() {
CaffeineBeverage* tea = new Tea();
tea->prepareRecipe(); // 制作茶
std::cout << std::endl;
CaffeineBeverage* coffee = new Coffee();
coffee->prepareRecipe(); // 制作咖啡
delete tea;
delete coffee;
return 0;
}
22.4 模板方法模式的优点
- 代码复用:
- 通过将共同的算法步骤放在基类中,可以避免代码重复,提高代码复用率。
- 控制算法的结构:
- 基类定义了算法的结构,子类只能改变算法的特定部分,确保了算法的一致性。
- 方便扩展:
- 通过创建新的子类,可以方便地添加新的算法,而无需修改现有代码。
22.5 模板方法模式的缺点
- 基类与子类之间的紧耦合:
- 由于模板方法在基类中定义,子类必须遵循该结构,可能会限制子类的灵活性。
- 类的数量增加:
- 每个算法都需要一个子类,可能导致类的数量增加。
22.6 应用场景
模板方法模式适用于以下场景:
- 在多个子类中有相同的算法步骤,但某些步骤的实现可能不同的情况。
- 当需要控制算法的整体结构并允许部分步骤的变化时。
22.7 总结
模板方法模式通过定义算法的骨架,允许子类在不改变算法结构的情况下实现具体的步骤。这种模式通过将公共代码放在基类中,避免了代码的重复,提高了代码的复用性和可维护性。模板方法模式在许多框架和库中得到了广泛应用,例如许多设计模式中的实现都体现了这一模式。
23.访问者(Visitor)
定义作用于对象结构的操作,将操作与对象的结构分离。可以在不更改类的前提下增加新操作。
访问者模式(Visitor Pattern)是一种行为型设计模式,它允许你在不修改对象结构的前提下,定义新的操作。通过将这些操作封装到访问者对象中,访问者模式使得可以在一组对象上执行不同的操作。
23.1 主要组成部分
- 抽象访问者(Visitor):
- 定义对每个具体元素类的访问方法(
visit
方法)。
- 定义对每个具体元素类的访问方法(
- 具体访问者(Concrete Visitor):
- 实现抽象访问者中的方法,定义对具体元素的具体操作。
- 抽象元素(Element):
- 定义一个接受访问者的方法(
accept
方法),该方法通常接收一个访问者对象。
- 定义一个接受访问者的方法(
- 具体元素(Concrete Element):
- 实现抽象元素,并在接受访问者时调用访问者的方法。
- 对象结构(Object Structure):
- 维护一组元素,可以遍历这些元素,并通过访问者访问它们。
23.2 工作原理
- 客户端代码创建一个访问者实例,并将其传递给对象结构中的元素。
- 每个元素调用访问者的相关方法,允许访问者对其进行操作。
23.3 示例
以下是一个简单的示例,展示了如何使用访问者模式来计算不同形状的面积和周长。
#include <iostream>
#include <vector>
// 前向声明
class Circle;
class Square;
// 抽象访问者
class ShapeVisitor {
public:
virtual void visit(Circle* circle) = 0;
virtual void visit(Square* square) = 0;
};
// 抽象元素
class Shape {
public:
virtual void accept(ShapeVisitor* visitor) = 0;
};
// 具体元素:圆形
class Circle : public Shape {
public:
Circle(double radius) : radius(radius) {}
void accept(ShapeVisitor* visitor) override {
visitor->visit(this);
}
double getRadius() const {
return radius;
}
private:
double radius;
};
// 具体元素:正方形
class Square : public Shape {
public:
Square(double side) : side(side) {}
void accept(ShapeVisitor* visitor) override {
visitor->visit(this);
}
double getSide() const {
return side;
}
private:
double side;
};
// 具体访问者:计算面积
class AreaVisitor : public ShapeVisitor {
public:
void visit(Circle* circle) override {
double area = 3.14159 * circle->getRadius() * circle->getRadius();
std::cout << "Circle Area: " << area << std::endl;
}
void visit(Square* square) override {
double area = square->getSide() * square->getSide();
std::cout << "Square Area: " << area << std::endl;
}
};
// 具体访问者:计算周长
class PerimeterVisitor : public ShapeVisitor {
public:
void visit(Circle* circle) override {
double perimeter = 2 * 3.14159 * circle->getRadius();
std::cout << "Circle Perimeter: " << perimeter << std::endl;
}
void visit(Square* square) override {
double perimeter = 4 * square->getSide();
std::cout << "Square Perimeter: " << perimeter << std::endl;
}
};
// 主程序
int main() {
std::vector<Shape*> shapes;
shapes.push_back(new Circle(5));
shapes.push_back(new Square(4));
AreaVisitor areaVisitor;
PerimeterVisitor perimeterVisitor;
for (Shape* shape : shapes) {
shape->accept(&areaVisitor);
shape->accept(&perimeterVisitor);
}
// 清理内存
for (Shape* shape : shapes) {
delete shape;
}
return 0;
}
23.4 访问者模式的优点
- 分离操作和对象结构:
- 访问者模式将操作的实现与对象结构的定义分离,使得可以在不修改对象结构的情况下增加新的操作。
- 增加新的操作:
- 只需添加新的访问者类就可以增加新的操作,而不需要修改已有的类。
- 强类型操作:
- 由于每个访问者都为每种具体元素提供了类型安全的操作,避免了类型转换带来的错误。
23.5 访问者模式的缺点
- 对象结构的变化:
- 如果对象结构发生变化(添加新元素类),需要修改所有访问者的实现,增加了维护成本。
- 复杂性:
- 增加了系统的复杂性,尤其是在元素类和访问者类较多的情况下。
23.6 应用场景
访问者模式适用于以下场景:
- 对于一个对象结构中的元素需要执行很多不同操作,但这些操作的实现又不会影响到元素的结构。
- 需要在不改变元素类的情况下增加新的操作。
23.7 总结
访问者模式通过将操作封装在访问者对象中,使得可以在不修改对象结构的情况下为对象添加新的操作。通过将行为与对象的状态分离,访问者模式提供了良好的扩展性和灵活性,但也带来了额外的复杂性和维护成本。