Bootstrap

C++学习33、设计模式

设计模式(Design Patterns)是软件工程中一种重要的思想,它提供了针对特定问题的通用解决方案。通过学习和应用设计模式,开发者可以编写更加灵活、可维护和可扩展的代码。C++作为一种强大的编程语言,在设计模式的实现上提供了丰富的工具和特性。本文将详细介绍C++中常见的设计模式,包括创建型模式、结构型模式和行为型模式,并通过代码示例进行说明。

一、创建型模式

1. 单例模式(Singleton Pattern)

a. 介绍

单例模式(Singleton Pattern)确保一个类只有一个实例,并提供一个全局访问点来获取该实例。单例模式的核心思想是控制对象的实例化过程,使得系统中只有一个对象实例被创建和使用,以避免重复创建对象带来的资源浪费和状态不一致的问题。

b. 特点

  • 单一实例:整个系统中,类只有一个实例存在。
  • 全局访问:提供一个全局访问点来获取该实例,通常是通过一个静态方法。
  • 延迟加载:实例的创建可以推迟到实际需要使用时,而不是在程序启动时立即创建。
  • 线程安全:在多线程环境下,确保单例实例的唯一性和正确性。

c. 优缺点

优点

  • 节省资源:避免重复创建实例,节省系统资源。
  • 控制访问:通过全局访问点,可以更好地控制对象的访问权限。
  • 状态管理:确保系统中某个对象的状态全局唯一,有利于状态管理。

缺点

  • 单一职责问题:单例模式可能会违背单一职责原则,因为它可能承担过多的职责。
  • 测试困难:单例模式的状态在测试时难以重置,因为整个程序运行期间只有一个实例。
  • 多线程问题:在多线程环境下,如果不采取适当的同步措施,可能会导致多个实例被创建。

d. 示例

以下是一个简单的C++单例模式实现示例:

#include <iostream>
#include <mutex>

class Singleton {
public:
    // 禁用拷贝构造函数和赋值运算符
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    // 提供一个全局访问点来获取实例
    static Singleton& getInstance() {
        // 使用双重检查锁(Double-Checked Locking)实现线程安全的延迟加载
        std::lock_guard<std::mutex> guard(mutex_);
        if (instance_ == nullptr) {
            instance_ = new Singleton();
        }
        return *instance_;
    }

    // 示例方法
    void doSomething() {
        std::cout << "Singleton instance is doing something." << std::endl;
    }

private:
    // 私有构造函数,防止外部实例化
    Singleton() {}

    // 静态实例指针
    static Singleton* instance_;

    // 静态互斥锁,用于线程同步
    static std::mutex mutex_;
};

// 初始化静态成员变量
Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;

int main() {
    Singleton& singleton = Singleton::getInstance();
    singleton.doSomething();
    return 0;
}

e. 应用场景

  • 日志系统:确保整个系统中只有一个日志对象,便于集中管理日志输出。
  • 配置管理器:管理应用程序的配置信息,确保配置的全局唯一性。
  • 连接池:数据库连接池,确保数据库连接的管理和复用。
  • 工厂类:在某些情况下,工厂类也可能设计为单例,以确保系统中只有一个工厂实例。

单例模式在实际应用中非常常见,但也需要谨慎使用,以避免引入不必要的复杂性和潜在的问题。

2. 工厂方法模式(Factory Method Pattern)

a. 介绍

C++中的工厂方法模式(Factory Method Pattern)提供了一种创建对象的方法,而无需指定具体的类,也不需要直接指定对象的类型。工厂方法模式抽象了对象的创建过程,使得客户端只需要通过指定具体的参数,而无需关心对象的创建细节。

b. 特点

  1. 封装性:工厂方法模式将对象的创建逻辑封装在工厂类中,使得客户端代码与具体产品类解耦,提高了代码的灵活性和可维护性。
  2. 多态性:工厂方法模式通常利用C++的多态性特性,通过抽象工厂和具体工厂来实现对象的创建。
  3. 扩展性:当需要添加新产品时,只需添加新的具体产品类和相应的具体工厂类,无需修改原有代码,增强了系统的可扩展性。

c. 优缺点

优点

  1. 降低耦合度:工厂方法模式将对象的创建与使用分离,降低了客户端和具体产品类之间的耦合度。
  2. 提高灵活性:客户端只需要知道所需产品的工厂,而不需要知道具体的产品类,从而提高了系统的灵活性。
  3. 易于维护:由于对象的创建逻辑被封装在工厂类中,因此当需要修改对象的创建方式时,只需修改工厂类即可,而无需修改客户端代码。

缺点

  1. 增加复杂度:引入工厂类之后,系统中类的数量会增加,从而增加了代码的复杂度和理解难度。
  2. 性能开销:需要额外的工厂类来创建对象,会带来一定的性能开销。
  3. 适用范围受限:只适用于创建具有相同接口或基类的对象,如果需要创建具有不同接口的对象,该模式不适用。

d. 示例

以下是一个简单的C++工厂方法模式示例:

#include <iostream>
using namespace std;

// 抽象产品类
class Product {
public:
    virtual ~Product() {}
    virtual void operation() = 0;
};

// 具体产品类A
class ProductA : public Product {
public:
    void operation() override {
        cout << "Performing Operation A..." << endl;
    }
};

// 具体产品类B
class ProductB : public Product {
public:
    void operation() override {
        cout << "Performing Operation B..." << endl;
    }
};

// 抽象工厂类
class Factory {
public:
    virtual ~Factory() {}
    virtual Product* createProduct() = 0;
};

// 具体工厂类A
class FactoryA : public Factory {
public:
    Product* createProduct() override {
        return new ProductA();
    }
};

// 具体工厂类B
class FactoryB : public Factory {
public:
    Product* createProduct() override {
        return new ProductB();
    }
};

int main() {
    Factory* factoryA = new FactoryA();
    Product* productA = factoryA->createProduct();
    productA->operation();

    Factory* factoryB = new FactoryB();
    Product* productB = factoryB->createProduct();
    productB->operation();

    // 释放内存
    delete productA;
    delete factoryA;
    delete productB;
    delete factoryB;

    return 0;
}

e. 应用场景

  1. 对象创建复杂:当对象的创建过程比较复杂,需要涉及多个步骤或条件判断时,可以使用工厂方法模式来封装这些复杂的创建逻辑。
  2. 需要解耦:当客户端代码与具体产品类之间存在紧密的耦合关系,需要降低这种耦合度以提高系统的灵活性和可维护性时,可以使用工厂方法模式。
  3. 多态性需求:当系统中存在多个具有相同接口或基类的产品类,并且需要根据不同条件创建不同的具体产品时,可以使用工厂方法模式来实现多态性创建。
  4. 扩展性需求:当系统中需要添加新产品时,如果希望在不修改客户端代码的情况下实现这种扩展,可以使用工厂方法模式来支持这种动态的产品创建需求。

3. 抽象工厂模式(Abstract Factory Pattern)

a. 介绍

C++中的抽象工厂模式(Abstract Factory Pattern)提供了一种方式来封装一组具有共同主题的单个工厂,而不需要指定它们的具体类。该模式允许客户端使用抽象的接口来创建一系列相关或依赖的对象,而无需关心具体的实现细节。抽象工厂模式通过定义一个创建对象的接口,使得子类可以决定实例化哪一个类,并且使得一个类的实例化延迟到其子类。

b. 特点

  1. 封装性:抽象工厂模式将对象的创建逻辑封装在工厂类中,客户端代码通过工厂接口来创建对象,而无需知道具体的实现细节。
  2. 抽象性:抽象工厂模式定义了一组抽象产品类,这些类声明了产品的接口,而具体产品类则实现了这些接口。
  3. 家族性:抽象工厂模式中的具体工厂类负责创建一系列相关的或相互依赖的对象,这些对象构成了一个产品家族。
  4. 易扩展性:当需要添加新的产品家族时,只需添加新的具体产品类和相应的具体工厂类,而无需修改原有代码。

c. 优缺点

优点

  1. 分离具体实现:客户端代码通过接口操作实例,不依赖于具体的类实现,提高了代码的灵活性和可维护性。
  2. 易于交换产品系列:由于具体工厂类在一个应用中只需要在初始化的时候出现一次,这使得改变一个应用的具体工厂变得十分容易,从而可以轻松地切换产品系列。
  3. 符合开闭原则:抽象工厂模式允许在不修改客户端代码的情况下,通过添加新的具体工厂类和具体产品类来扩展系统,符合开闭原则(对扩展开放,对修改关闭)。

缺点

  1. 难以支持新种类的产品:一旦工厂类中定义好了创建产品的方法后,如果需要添加新产品,就需要修改抽象工厂接口,这可能会带来较大的改动。
  2. 复杂度增加:随着产品族的增加,相关的具体工厂和产品类的数量也会增加,这会使系统更加复杂,增加了理解和维护的难度。

d. 示例

以下是一个使用C++实现抽象工厂模式的简单示例:

#include <iostream>
#include <string>
using namespace std;

// 抽象产品类A
class AbstractProductA {
public:
    virtual ~AbstractProductA() {}
    virtual void use() = 0;
};

// 具体产品类A1
class ProductA1 : public AbstractProductA {
public:
    void use() override {
        cout << "Using Product A1" << endl;
    }
};

// 具体产品类A2
class ProductA2 : public AbstractProductA {
public:
    void use() override {
        cout << "Using Product A2" << endl;
    }
};

// 抽象产品类B
class AbstractProductB {
public:
    virtual ~AbstractProductB() {}
    virtual void eat() = 0;
};

// 具体产品类B1
class ProductB1 : public AbstractProductB {
public:
    void eat() override {
        cout << "Eating Product B1" << endl;
    }
};

// 具体产品类B2
class ProductB2 : public AbstractProductB {
public:
    void eat() override {
        cout << "Eating Product B2" << endl;
    }
};

// 抽象工厂类
class AbstractFactory {
public:
    virtual ~AbstractFactory() {}
    virtual AbstractProductA* createProductA() = 0;
    virtual AbstractProductB* createProductB() = 0;
};

// 具体工厂类1
class Factory1 : public AbstractFactory {
public:
    AbstractProductA* createProductA() override {
        return new ProductA1();
    }
    AbstractProductB* createProductB() override {
        return new ProductB1();
    }
};

// 具体工厂类2
class Factory2 : public AbstractFactory {
public:
    AbstractProductA* createProductA() override {
        return new ProductA2();
    }
    AbstractProductB* createProductB() override {
        return new ProductB2();
    }
};

// 客户端代码
void clientCode(AbstractFactory* factory) {
    AbstractProductA* productA = factory->createProductA();
    AbstractProductB* productB = factory->createProductB();
    productA->use();
    productB->eat();
    delete productA;
    delete productB;
}

int main() {
    AbstractFactory* factory1 = new Factory1();
    AbstractFactory* factory2 = new Factory2();

    cout << "Using the products created by Factory 1:" << endl;
    clientCode(factory1);

    cout << "Using the products created by Factory 2:" << endl;
    clientCode(factory2);

    delete factory1;
    delete factory2;

    return 0;
}

在这个示例中,我们定义了两个抽象产品类AbstractProductAAbstractProductB,以及两个具体产品类ProductA1ProductA2ProductB1ProductB2。我们还定义了一个抽象工厂类AbstractFactory,以及两个具体工厂类Factory1Factory2。客户端代码使用抽象工厂接口来创建具体产品,并使用这些产品。

e. 应用场景

抽象工厂模式适用于以下场景:

  1. 系统需要独立于具体产品的创建、组成和表示:系统应该对产品类的实现细节和创建过程无需了解,只需通过抽象接口来操作产品。
  2. 系统需要多个产品系列中的一个来配置:可以动态地更改产品系列,而无需修改客户端代码。
  3. 系统需要提供产品库的类,且只想显示它们的接口而不是实现:通过抽象工厂模式,可以将产品的创建逻辑封装在工厂类中,客户端代码只需通过接口来操作产品。

例如,在一个图形用户界面(GUI)系统中,可能需要不同的主题或皮肤,每个主题或皮肤包含一组相关的控件(如按钮、文本框等)。使用抽象工厂模式,可以定义一个抽象工厂接口,用于创建这些控件,并为每个主题或皮肤提供一个具体工厂类来实现该接口。这样,客户端代码就可以通过抽象工厂接口来创建控件,而无需关心具体的主题或皮肤实现。

4. 建造者模式(Builder Pattern)

a. 介绍

建造者模式(Builder Pattern)主要用于处理在软件构建过程中复杂对象的创建问题。它将一个复杂对象的构建过程分解为一系列简单的步骤,每个步骤由独立的建造者对象负责。建造者模式常用于创建复杂的对象,它避免了直接传递大量参数来构造函数,使得构建过程变得可控,让代码变得灵活和可维护。建造者模式允许开发者按照指定的步骤创建复杂对象,构建过程的细节被封装在具体建造者中,将创建对象的过程和表示对象的过程分离,且同一个构建过程可以使用不同的具体建造者以及不同的顺序来创建不同的表示。

b. 特点

建造者模式的特点主要包括:

  1. 产品(Product):这是最终构建完成的对象,包含一些基本部件和组装方法。
  2. 建造者(Builder):这是一个抽象接口,定义了创建产品各个部件的抽象方法。
  3. 具体建造者(ConcreteBuilder):实现了Builder接口,完成具体产品的构建。
  4. 指挥者(Director):负责安排已有模块的顺序,然后指导它们完成一个复杂对象(即产品)的构建工作。

建造者模式将对象的构建与它的表示分离,使得客户端不必知道产品内部组成的细节,隐藏了产品的内部实现细节,提供了更好的封装性。同时,由于具体的建造者是相互独立的,因此易于扩展。

c. 优缺点

优点

  1. 封装性好:建造者模式将对象的构建与它的表示分离,使得客户端不必知道产品内部组成的细节。
  2. 扩展性好:由于具体的建造者是相互独立的,因此易于扩展。如果需要增加新的部件或修改现有部件的创建方式,只需要增加或修改相应的建造者类,而不会影响其他已构建的部件和客户端代码。
  3. 控制细节风险:建造者模式允许对创建过程逐步细化,而不对其他模块产生任何影响,便于控制细节风险。客户端只需要指定需要构建的对象类型和内容,而不需要了解具体的构建细节,从而降低了客户端代码的复杂性。

缺点

  1. 使用范围受限:建造者模式所创建的产品一般具有较多的共同点,其组成部分相似。如果产品之间的差异性很大,例如很多组成部分都不相同,不适合使用建造者模式。
  2. 可能导致系统庞大:如果产品的内部变化复杂,可能需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大。这会增加系统的理解难度和运行成本,因为需要维护大量的建造者类。

d. 示例

以下是一个简单的建造者模式示例,以组装电脑为例:

#include <iostream>
#include <memory>
#include <vector>
#include <string>

// 抽象产品类
class AbstractProduct {
public:
    virtual ~AbstractProduct() {}
    virtual void setDisplay(std::string display) = 0;
    virtual void setHost(std::string host) = 0;
    virtual void setKeyBoard(std::string keyBoard) = 0;
    virtual void setMouse(std::string mouse) = 0;
    virtual void show() = 0;
};

// 具体产品类
class Computer : public AbstractProduct {
public:
    ~Computer() {}
    void setDisplay(std::string display) { m_vec.emplace_back(display); }
    void setHost(std::string host) { m_vec.emplace_back(host); }
    void setKeyBoard(std::string keyBoard) { m_vec.emplace_back(keyBoard); }
    void setMouse(std::string mouse) { m_vec.emplace_back(mouse); }
    void show() {
        std::cout << "----------组装电脑---------" << std::endl;
        for (auto& x : m_vec) {
            std::cout << x << std::endl;
        }
    }
private:
    std::vector<std::string> m_vec;
};

// 抽象建造者
class AbstractBuilder {
public:
    AbstractBuilder() : product(std::make_shared<Computer>()) {}
    virtual ~AbstractBuilder() {}
    virtual void BuildDisplay(std::string display) = 0;
    virtual void BuildHost(std::string host) = 0;
    virtual void BuildKeyBoard(std::string keyBoard) = 0;
    virtual void BuildMouse(std::string mouse) = 0;
    std::shared_ptr<AbstractProduct> getProduct() { return product; }
protected:
    std::shared_ptr<AbstractProduct> product;
};

// 具体建造者
class ComputerBuilder : public AbstractBuilder {
public:
    ~ComputerBuilder() {}
    void BuildDisplay(std::string display) { product->setDisplay(display); }
    void BuildHost(std::string host) { product->setHost(host); }
    void BuildKeyBoard(std::string keyBoard) { product->setKeyBoard(keyBoard); }
    void BuildMouse(std::string mouse) { product->setMouse(mouse); }
};

// 指挥者
class Director {
public:
    Director(std::shared_ptr<AbstractBuilder> builder) : m_builder(builder) {}
    AbstractProduct* createComputer(std::string display, std::string host, std::string keyBoard, std::string mouse) {
        m_builder->BuildDisplay(display);
        m_builder->BuildHost(host);
        m_builder->BuildKeyBoard(keyBoard);
        m_builder->BuildMouse(mouse);
        return m_builder->getProduct().get();
    }
private:
    std::shared_ptr<AbstractBuilder> m_builder;
};

int main() {
    // 创建电脑建造者
    std::shared_ptr<AbstractBuilder> computerBuilder = std::make_shared<ComputerBuilder>();
    // 创建电脑建造者的管理者
    Director* pDcomp = new Director(computerBuilder);
    // 管理者指挥建造者制造电脑产品
    AbstractProduct* computerPro = pDcomp->createComputer("联想显示器", "外星人主机", "雷蛇键盘", "罗技鼠标");
    // 电脑产品制造完成
    computerPro->show();

    // 释放内存
    delete pDcomp;
    delete computerPro;

    return 0;
}

e. 应用场景

建造者模式通常适用于以下应用场景:

  1. 对象结构复杂:当需要创建的对象具有复杂的内部结构,包含多个组件或属性时,可以使用建造者模式来构建这些对象。通过将构建过程分解为多个步骤,可以更加清晰地管理和控制对象的创建过程。
  2. 创建流程固定:当对象的创建流程是固定的,即无论创建多少个对象,其构建步骤都是相同的时候,可以使用建造者模式。通过将这些步骤封装在建造者类中,可以确保每次创建对象时都遵循相同的流程。
  3. 需要控制创建过程:当需要更加灵活地控制对象的创建过程,例如根据用户输入或运行时条件来决定对象的某些属性时,可以使用建造者模式。通过在指挥者类中引入逻辑来控制对象的创建,可以实现更加灵活的构建过程。
  4. 代码易于阅读和维护:当需要创建的对象具有大量的参数或配置选项时,使用建造者模式可以将这些参数和选项分组并封装在不同的建造者类中,从而减少构造函数的复杂度,使代码更加易读和易于维护。

5. 原型模式(Prototype)

a. 介绍

原型模式(Prototype Pattern)允许通过复制(或克隆)已有对象来创建新对象,而不是通过实例化一个类来创建。这种模式的核心思想是利用原型对象来指定要创建对象的类型,并通过复制这些原型对象来创建新的对象。原型模式提供了一种灵活的创建方式,可以在运行时动态地创建对象,而无需关心对象的创建细节。

b. 特点

原型模式的主要特点包括:

  1. 基于原型创建对象:通过复制已有的原型对象来创建新对象,而不是通过实例化类。
  2. 简化对象创建:减少了使用构造函数创建对象的开销,特别是在对象创建过程复杂或耗时的情况下。
  3. 支持深拷贝和浅拷贝:可以根据需要选择深拷贝或浅拷贝来复制对象,深拷贝会复制对象及其引用的所有对象,而浅拷贝仅复制对象本身。
  4. 隐藏创建细节:封装了对象的创建过程,客户端无需了解具体的创建细节。

c. 优缺点

优点

  1. 提高性能:当创建对象的过程比较复杂时,使用原型模式可以显著提高性能,因为它避免了重复的构造过程。
  2. 简化代码:通过复制原型对象来创建新对象,减少了代码量,使代码更加简洁。
  3. 支持动态创建:可以在运行时根据需要动态地创建对象,增加了系统的灵活性。
  4. 易于扩展:通过增加新的原型对象来扩展系统,无需修改现有代码。

缺点

  1. 实现复杂:需要为每个需要克隆的类实现一个克隆方法,这可能会增加代码的复杂性。
  2. 深拷贝实现困难:当对象之间存在复杂的引用关系时,实现深拷贝可能会比较困难。
  3. 潜在的线程安全问题:在多线程环境下,如果没有适当的同步措施,可能会导致竞态条件等问题。

d. 示例

以下是一个简单的C++原型模式示例:

#include <iostream>
#include <memory>

// 抽象原型类
class Prototype {
public:
    virtual ~Prototype() {}
    virtual std::unique_ptr<Prototype> clone() const = 0;
    virtual void display() const = 0;
};

// 具体原型类
class ConcretePrototype : public Prototype {
private:
    std::string name;
    int value;

public:
    ConcretePrototype(const std::string& name, int value)
        : name(name), value(value) {}

    std::unique_ptr<Prototype> clone() const override {
        return std::make_unique<ConcretePrototype>(name, value);
    }

    void display() const override {
        std::cout << "Name: " << name << ", Value: " << value << std::endl;
    }
};

int main() {
    // 创建原型对象
    auto prototype = std::make_unique<ConcretePrototype>("Prototype1", 100);

    // 克隆原型对象
    auto clonedPrototype = prototype->clone();

    // 显示原型对象和克隆对象
    prototype->display();
    clonedPrototype->display();

    return 0;
}

在这个示例中,Prototype是一个抽象原型类,定义了克隆方法和显示方法。ConcretePrototype是一个具体原型类,实现了克隆方法和显示方法,并包含了具体的属性。在main函数中,我们创建了一个原型对象,并通过调用其克隆方法来创建了一个新的克隆对象。最后,我们显示了原型对象和克隆对象的信息。

e. 应用场景

原型模式适用于以下场景:

  1. 对象创建过程复杂:当对象的创建过程比较复杂或耗时,且需要频繁创建相似对象时,可以使用原型模式来简化对象的创建过程。
  2. 需要动态创建对象:当需要在运行时动态地创建对象时,可以使用原型模式来根据需要创建对象。
  3. 避免使用构造函数:当需要避免使用构造函数来创建对象时,可以使用原型模式来复制现有对象来创建新对象。
  4. 对象状态保存和恢复:当需要保存和恢复对象的状态时,可以使用原型模式来复制对象并保存其状态,以便在需要时恢复到历史状态。

例如,在游戏开发中,可以使用原型模式来创建复杂的游戏角色和道具对象,并通过复制现有对象来快速生成相似的对象。在文档编辑器中,可以使用原型模式来实现复制粘贴功能,通过复制现有文档来创建新的文档对象。

二、结构型模式

结构型设计模式主要关注类和对象的组合,它们帮助设计类或对象的组合,使结构更加灵活,易于扩展和维护。常见的结构型模式有适配器模式、桥接模式、组合模式、装饰器模式、外观模式和享元模式。

1. 适配器模式(Adapter)

a. 介绍

适配器模式允许接口不兼容的类一起工作。通过将一个类的接口转换成客户希望的另一个接口,适配器模式实现了接口的转换。这种设计模式也被称为包装器(Wrapper)。在C++中,适配器模式分为类适配器和对象适配器两种,前者通过多继承实现,后者通过对象组合实现。

b. 特点

  1. 接口转换:适配器模式的核心特点是能够将一个类的接口转换成客户希望的另一个接口。
  2. 封装:适配器将适配过程进行封装,从而隐藏适配的细节,只对外提供被适配后的接口。
  3. 灵活扩展:和装饰器模式类似,适配器模式可以动态扩展一些遗留或不易改动的代码。

c. 优缺点

优点

  1. 提高复用性:适配器模式可以让原本不兼容的类一起工作,从而提高类的复用性。
  2. 增加透明度:通过适配器,系统的接口更加透明,使得开发者可以更容易理解和使用。
  3. 灵活性好:适配器模式使得系统更容易适应变化,特别是在需要集成第三方库或API时。

缺点

  1. 系统复杂性增加:过多地使用适配器会使系统变得复杂和难以维护,特别是当适配器的关系链很长时。
  2. 性能影响:在某些情况下,适配器的使用可能会引入额外的性能开销。

d. 示例

以下是一个简单的C++适配器模式示例,展示了如何将一个不支持printDate方法的类适配为支持该方法的接口。

#include <iostream>

// 目标接口
class Date {
public:
    virtual void printDate() = 0;
};

// 被适配类 - DateA
class DateA {
public:
    void display() {
        std::cout << "DateA: 2023-06-05" << std::endl;
    }
};

// 适配器类
class DateAdapter : public Date {
private:
    DateA dateA; // 被适配对象
public:
    void printDate() override {
        dateA.display(); // 调用DateA的方法
    }
};

// 客户端代码
void clientCode(Date* date) {
    date->printDate();
}

int main() {
    DateAdapter adapter; // 使用适配器将DateA适配为Date接口
    clientCode(&adapter);
    return 0;
}

e. 应用场景

  1. 兼容不同接口实现:当需要兼容相同业务下的不同接口实现时,可以使用适配器模式。
  2. 通信方式转换:在需要将一种通信方式转换为另一种通信方式时,例如将UDP通信转换为内部共享内存通信。
  3. 简化调用:在嵌入式开发中,经常需要将硬件或操作系统的底层API封装成高级接口或类,从而简化上层应用的调用。这时,可以使用适配器模式(即Wrapper)来隐藏底层实现细节。
  4. 集成第三方库:当需要集成第三方库或API,而这些库或API的接口与系统现有接口不兼容时,适配器模式提供了一种优雅的解决方案。

总的来说,适配器模式在C++编程中是一种非常有用的设计模式,它能够提高代码的复用性、灵活性和可维护性。然而,也需要谨慎使用,以避免引入不必要的复杂性和性能开销。

2. 桥接模式(Bridge)

a. 介绍

桥接模式(Bridge Pattern)旨在将抽象部分与其具体实现部分分离,从而使它们可以独立变化而互不影响。桥接模式通过将一个类的抽象部分与它的实现部分分开,允许它们独立扩展和变化,同时也提供了一种组合的方式来连接抽象部分和实现部分。这种分离使得系统更加灵活,可以在不影响抽象部分的情况下修改实现部分,反之亦然。

在桥接模式中,通常包含以下几个角色:

  • 抽象角色类:定义了统一的对外接口,并定义了接口的组成结构,但不包含接口对应的具体实现。
  • 具体实现类:包含了对抽象角色类的接口的具体代码实现。这些类可以根据需求变化而独立变化,且不会影响到其他类的功能。
  • 桥接类:充当了抽象角色类和具体实现类之间的桥梁,负责维护抽象角色类和具体实现类之间的关系,允许客户端在运行时选择使用哪个具体实现类。

b. 特点

桥接模式的主要特点包括:

  • 抽象与实现分离:桥接模式将抽象部分与实现部分分离,使它们可以独立地变化,从而降低了系统的复杂度和耦合程度。
  • 扩展性好:由于抽象与实现分离,扩展起来更便捷,可以获得更多样式的目标。
  • 解耦:不同抽象间的耦合程度低,满足了设计模式要求的合成复用原则和开闭原则。
  • 封装性好:具体实现细节对客户而言是透明不可见的,提高了系统的安全性和可维护性。

c. 优缺点

优点

  • 提高了系统的可扩展性和灵活性。
  • 降低了系统的耦合度和复杂度。
  • 遵循了开闭原则和单一职责原则。

缺点

  • 使用场景有限制,只有系统有两个以上独立变化维度时才适用。
  • 增加了系统的复杂性,因为需要引入多个类来实现抽象部分和实现部分的分离。

d. 示例

以下是一个使用桥接模式的简单示例,展示了如何通过桥接模式将抽象部分与实现部分分离:

#include <iostream>
#include <string>

// 实现类接口 - 文件系统
class FileSystem {
public:
    virtual void readFile() = 0;
    virtual void writeFile() = 0;
};

// 具体实现类 - Windows 文件系统
class WindowsFileSystem : public FileSystem {
public:
    void readFile() override {
        std::cout << "Reading file on Windows." << std::endl;
    }
    void writeFile() override {
        std::cout << "Writing file on Windows." << std::endl;
    }
};

// 具体实现类 - Linux 文件系统
class LinuxFileSystem : public FileSystem {
public:
    void readFile() override {
        std::cout << "Reading file on Linux." << std::endl;
    }
    void writeFile() override {
        std::cout << "Writing file on Linux." << std::endl;
    }
};

// 抽象类 - 操作系统
class OperatingSystem {
protected:
    FileSystem* fileSystem_;
public:
    OperatingSystem(FileSystem* fs) : fileSystem_(fs) {}
    virtual void run() = 0;
};

// 具体抽象类 - Windows 操作系统
class WindowsOS : public OperatingSystem {
public:
    WindowsOS(FileSystem* fs) : OperatingSystem(fs) {}
    void run() override {
        std::cout << "Running Windows OS." << std::endl;
        fileSystem_->readFile();
        fileSystem_->writeFile();
    }
};

// 具体抽象类 - Linux 操作系统
class LinuxOS : public OperatingSystem {
public:
    LinuxOS(FileSystem* fs) : OperatingSystem(fs) {}
    void run() override {
        std::cout << "Running Linux OS." << std::endl;
        fileSystem_->readFile();
        fileSystem_->writeFile();
    }
};

int main() {
    FileSystem* windowsFS = new WindowsFileSystem();
    OperatingSystem* windowsOS = new WindowsOS(windowsFS);
    windowsOS->run();

    FileSystem* linuxFS = new LinuxFileSystem();
    OperatingSystem* linuxOS = new LinuxOS(linuxFS);
    linuxOS->run();

    delete windowsFS;
    delete windowsOS;
    delete linuxFS;
    delete linuxOS;

    return 0;
}

在这个示例中,我们将操作系统(OperatingSystem)和文件系统(FileSystem)分离为抽象部分和实现部分。通过桥接模式,我们可以在不修改操作系统类的情况下更换文件系统实现,从而实现了两者的独立变化。

e. 应用场景

桥接模式的应用场景非常广泛,包括但不限于以下几个方面:

  • 系统有两个以上独立变化维度:当系统中存在多个独立但相互关联的类时,可以使用桥接模式来解耦它们的依赖关系。
  • 动态创建对象:当需要动态地创建一系列具有相似特性的对象时,可以使用桥接模式来实现对象的创建和初始化过程。
  • 算法或策略切换:当需要在一个系统中灵活地切换不同的算法或策略时,可以使用桥接模式来实现算法或策略的切换和组合。
  • 跨平台应用开发:使用桥接模式来处理不同操作系统或硬件平台的差异,例如在移动端APP应用中,UI组件同时兼容iOS和Android平台。
  • 第三方插件开发:使用桥接模式开发出可支持多种第三方服务的组件,例如移动支付API。

3. 组合模式(Composite)

a. 介绍

组合模式(Composite Pattern),又称部分-整体模式。它将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。组合模式通过递归组合对象来创建复杂的树形结构,并且客户端可以通过相同的接口来访问叶子节点和组合节点,而无需关心它们的具体实现。

在组合模式中,通常包含以下几个角色:

  • Component(组件):定义了所有具体组件(叶子和组合)的公共接口。它可以是抽象类或接口,通常包含一些基本的操作方法,如添加、删除子组件,以及获取子组件等。
  • Leaf(叶子):实现了Component接口,表示树形结构中的叶子节点。叶子节点不包含子节点,通常执行一些具体的业务操作。
  • Composite(组合):实现了Component接口,表示树形结构中的非叶子节点(即包含子节点的节点)。它通常有一个集合来存储子组件,并且实现了对这些子组件的操作方法,如递归调用子组件的业务操作等。

b. 特点

组合模式的主要特点包括:

  • 树形结构:组合模式将对象组合成树形结构,使得客户端能够以相同的方式对待单个对象和对象组合。
  • 递归调用:组合对象通常通过递归调用其子组件的方法来执行操作,这使得客户端可以方便地处理复杂的树形结构。
  • 接口一致:无论是叶子节点还是组合节点,它们都实现了相同的接口,这使得客户端可以统一处理它们,而无需关心它们的具体实现。

c. 优缺点

优点

  • 简化客户端代码:客户端可以通过相同的接口来访问叶子节点和组合节点,而无需关心它们的具体实现,从而简化了客户端代码。
  • 提高系统的可扩展性:通过添加新的叶子节点或组合节点,可以方便地扩展系统的功能,而无需修改现有代码。
  • 灵活性好:组合模式允许在运行时动态地添加或删除子组件,从而提高了系统的灵活性。

缺点

  • 设计复杂度增加:组合模式的设计相对复杂,需要定义多个类和接口,并且需要考虑递归调用的实现方式。
  • 性能开销:在处理复杂的树形结构时,递归调用可能会带来一定的性能开销。

d. 示例

以下是一个使用组合模式的简单示例,展示了如何创建一个表示文件系统的树形结构:

#include <iostream>
#include <vector>
#include <memory>
#include <string>

// 抽象基类 Component
class Component {
public:
    virtual ~Component() = default;
    virtual void operation() const = 0; // 业务操作
    virtual void add(std::shared_ptr<Component> component) = 0; // 添加子组件
    virtual void remove(std::shared_ptr<Component> component) = 0; // 移除子组件
    virtual std::shared_ptr<Component> getChild(int index) const = 0; // 获取子组件
};

// 叶子节点类 Leaf
class Leaf : public Component {
private:
    std::string name; // 叶子节点名称
public:
    Leaf(const std::string& name) : name(name) {}
    void operation() const override {
        std::cout << "Leaf " << name << " operation." << std::endl;
    }
    // 对于叶子节点,添加、移除和获取子组件的操作是无意义的,因此可以抛出异常或不做任何操作
    void add(std::shared_ptr<Component> component) override {
        throw std::runtime_error("Cannot add child to leaf.");
    }
    void remove(std::shared_ptr<Component> component) override {
        throw std::runtime_error("Cannot remove child from leaf.");
    }
    std::shared_ptr<Component> getChild(int index) const override {
        return nullptr;
    }
};

// 组合节点类 Composite
class Composite : public Component {
private:
    std::vector<std::shared_ptr<Component>> children; // 存储子组件
public:
    void operation() const override {
        std::cout << "Composite operation." << std::endl;
        for (const auto& child : children) {
            child->operation(); // 递归调用子组件的操作
        }
    }
    void add(std::shared_ptr<Component> component) override {
        children.push_back(component); // 添加子组件
    }
    void remove(std::shared_ptr<Component> component) override {
        children.erase(std::remove(children.begin(), children.end(), component), children.end()); // 移除子组件
    }
    std::shared_ptr<Component> getChild(int index) const override {
        if (index >= 0 && index < children.size()) {
            return children[index]; // 获取子组件
        }
        return nullptr;
    }
};

// 客户端代码示例
int main() {
    // 创建叶子节点
    auto leaf1 = std::make_shared<Leaf>("Leaf 1");
    auto leaf2 = std::make_shared<Leaf>("Leaf 2");

    // 创建组合节点并添加叶子节点
    auto composite = std::make_shared<Composite>();
    composite->add(leaf1);
    composite->add(leaf2);

    // 执行组合节点的操作
    composite->operation();

    return 0;
}

e. 应用场景

组合模式适用于以下几种场景:

  • 需要表示对象的部分-整体层次结构:例如文件系统、组织结构等,其中文件夹(或部门)可以包含其他文件夹(或子部门)和文件(或员工)。
  • 希望客户端忽略组合对象与单个对象的差异:客户端可以通过相同的接口处理叶子节点和组合节点,而无需关心它们的具体实现。这有助于简化客户端代码并提高系统的可扩展性。

总之,组合模式是一种强大的设计模式,它通过将对象组合成树形结构来表示“部分-整体”的层次结构,并使得客户端可以统一处理单个对象和组合对象。然而,在使用组合模式时也需要考虑其设计复杂度和性能开销等因素。

4. 装饰器模式(Decorator)

a. 介绍

C++中的装饰器模式(Decorator Pattern)允许在不改变现有对象结构的情况下,动态地向对象添加新的行为和责任。装饰器模式通过创建一个或多个装饰器类,这些类包装了原始对象并提供了与原始对象相同的接口,从而可以在不修改原始对象代码的情况下,向其添加额外的功能或行为。

装饰器模式的核心思想是将对象的行为和职责进行分离,使得可以在运行时根据需要动态地添加或删除对象的行为。这种灵活性使得装饰器模式在需要扩展对象功能而不希望修改其源代码的场景中非常有用。

b. 特点

装饰器模式的主要特点包括:

  1. 动态扩展:可以在不修改现有对象代码的情况下,动态地向对象添加新的行为和责任。
  2. 透明性:客户端可以通过相同的接口访问原始对象和装饰后的对象,而无需关心它们之间的区别。
  3. 灵活性:可以通过组合不同的装饰器来实现复杂的功能扩展,同时保持对象的结构不变。
  4. 开闭原则:装饰器模式遵循开闭原则,即对扩展开放,对修改关闭。这意味着可以通过添加新的装饰器类来扩展系统的功能,而无需修改现有的代码。

c. 优缺点

优点

  1. 灵活性:可以动态地添加或删除对象的行为,而无需修改其源代码。
  2. 透明性:客户端可以通过相同的接口访问原始对象和装饰后的对象,简化了客户端代码。
  3. 可扩展性:可以通过添加新的装饰器类来扩展系统的功能,而无需修改现有的代码结构。

缺点

  1. 复杂性:随着装饰器数量的增加,系统的复杂性可能会增加,因为需要管理多个装饰器之间的关系。
  2. 性能开销:由于装饰器模式涉及到多个对象的包装和委托调用,可能会带来一定的性能开销。

d. 示例

以下是一个使用C++实现的装饰器模式的简单示例:

#include <iostream>
#include <string>

// 抽象组件接口
class Component {
public:
    virtual ~Component() = default;
    virtual std::string getDescription() const = 0;
    virtual double cost() const = 0;
};

// 具体组件类:普通咖啡
class PlainCoffee : public Component {
public:
    std::string getDescription() const override {
        return "普通咖啡";
    }
    double cost() const override {
        return 2.0;
    }
};

// 抽象装饰器类
class Decorator : public Component {
protected:
    Component* component;
public:
    Decorator(Component* comp) : component(comp) {}
    std::string getDescription() const override {
        return component->getDescription();
    }
    double cost() const override {
        return component->cost();
    }
};

// 具体装饰器类:牛奶装饰器
class MilkDecorator : public Decorator {
public:
    MilkDecorator(Component* comp) : Decorator(comp) {}
    std::string getDescription() const override {
        return Decorator::getDescription() + " + 牛奶";
    }
    double cost() const override {
        return Decorator::cost() + 1.0;
    }
};

// 具体装饰器类:糖浆装饰器
class SyrupDecorator : public Decorator {
public:
    SyrupDecorator(Component* comp) : Decorator(comp) {}
    std::string getDescription() const override {
        return Decorator::getDescription() + " + 糖浆";
    }
    double cost() const override {
        return Decorator::cost() + 0.5;
    }
};

int main() {
    // 创建一个普通咖啡对象
    Component* coffee = new PlainCoffee();
    std::cout << "咖啡描述: " << coffee->getDescription() << std::endl;
    std::cout << "价格: $" << coffee->cost() << std::endl;

    // 使用牛奶装饰器装饰咖啡
    Component* coffeeWithMilk = new MilkDecorator(coffee);
    std::cout << "\n咖啡描述: " << coffeeWithMilk->getDescription() << std::endl;
    std::cout << "价格: $" << coffeeWithMilk->cost() << std::endl;

    // 使用糖浆装饰器进一步装饰咖啡
    Component* coffeeWithMilkAndSyrup = new SyrupDecorator(coffeeWithMilk);
    std::cout << "\n咖啡描述: " << coffeeWithMilkAndSyrup->getDescription() << std::endl;
    std::cout << "价格: $" << coffeeWithMilkAndSyrup->cost() << std::endl;

    // 释放内存
    delete coffeeWithMilkAndSyrup;
    delete coffeeWithMilk;
    delete coffee;

    return 0;
}

e. 应用场景

装饰器模式在C++中适用于以下场景:

  1. 组件扩展:在大型项目中,随着业务的增加,需要添加新的功能。使用装饰器模式可以避免修改原有的基础组件,同时保持系统的可扩展性。
  2. API增强:当为第三方提供API时,可以使用装饰器模式添加额外的功能,如日志记录、安全校验等,而无需修改原有的API接口。
  3. 权限管理:可以使用装饰器模式来控制对原有特定接口的访问权限,从而实现细粒度的权限控制。
  4. 缓存机制:在网络请求或数据库查询等操作中,可以使用装饰器模式添加额外的缓存、重试、超时处理等功能,以提高系统的性能和可靠性。

5. 外观模式(Facade)

a. 介绍

C++中的外观模式(Facade Pattern)为复杂的子系统提供了一个统一的接口,使得客户端能够通过这个接口轻松访问子系统的功能,而无需了解子系统内部的复杂结构和实现细节。外观模式就像是一个“门面”,它隐藏了子系统的复杂性,并向客户端提供了一个简化且易于使用的接口。

b. 特点

外观模式的主要特点包括:

  1. 简化接口:外观模式通过提供一个简化的接口,使得客户端能够更容易地访问子系统的功能,而无需了解子系统内部的复杂实现。
  2. 降低耦合度:外观模式将客户端与子系统之间的直接交互替换为与外观类的交互,从而降低了客户端与子系统之间的耦合度,使得系统更加灵活和易于维护。
  3. 封装性:外观类封装了多个子系统的功能,使得客户端无需直接与这些子系统交互,从而增强了系统的封装性。
  4. 协调功能:外观类通常还负责协调子系统之间的交互,确保它们能够协同工作,以提供所需的功能。

c. 优缺点

优点

  1. 提高系统的易用性:通过提供一个简化的接口,使得客户端能够更容易地使用复杂的子系统。
  2. 降低系统的复杂度:外观模式将多个子系统的功能封装在一起,提供了一个统一的接口,从而降低了系统的复杂度。
  3. 增强系统的灵活性:由于客户端与子系统之间的耦合度降低,系统变得更加灵活,易于维护和扩展。

缺点

  1. 不符合开闭原则:虽然外观模式在一定程度上降低了系统的复杂度,但增加新的子系统或修改现有子系统的功能时,可能需要修改外观类,这违反了开闭原则(即对扩展开放,对修改关闭)。然而,这种修改通常相对较小,且主要集中在外观类上,因此其影响是有限的。
  2. 可能隐藏子系统的细节:外观模式可能会隐藏子系统的某些细节,导致客户端无法直接访问这些功能。然而,这通常是为了简化接口和提高系统的易用性而做出的权衡。

d. 示例

以下是一个使用外观模式的简单示例,展示了如何为复杂的子系统提供一个统一的接口:

#include <iostream>

// 子系统接口
class Subsystem {
public:
    virtual ~Subsystem() {}
    virtual void operation() = 0;
};

// 具体子系统A
class ConcreteSubsystemA : public Subsystem {
public:
    void operation() override {
        std::cout << "Subsystem A operation." << std::endl;
    }
};

// 具体子系统B
class ConcreteSubsystemB : public Subsystem {
public:
    void operation() override {
        std::cout << "Subsystem B operation." << std::endl;
    }
};

// 外观类
class Facade {
private:
    ConcreteSubsystemA* subsystemA;
    ConcreteSubsystemB* subsystemB;

public:
    Facade() : subsystemA(new ConcreteSubsystemA()), subsystemB(new ConcreteSubsystemB()) {}
    ~Facade() {
        delete subsystemA;
        delete subsystemB;
    }

    void operation() {
        std::cout << "Facade initializes subsystems." << std::endl;
        subsystemA->operation();
        subsystemB->operation();
    }
};

// 客户端代码
int main() {
    Facade facade;
    facade.operation();
    return 0;
}

在这个示例中,Facade类封装了两个具体的子系统ConcreteSubsystemAConcreteSubsystemB,并提供了一个统一的接口operation()。客户端通过调用Facade类的operation()方法,可以轻松地访问两个子系统的功能,而无需了解它们内部的实现细节。

e. 应用场景

外观模式适用于以下场景:

  1. 复杂的子系统:当系统由多个复杂的子系统组成,且客户端需要访问这些子系统的功能时,可以使用外观模式来简化接口,降低系统的复杂度。
  2. 需要统一接口:当需要为多个子系统提供一个统一的接口时,可以使用外观模式来封装这些子系统的功能,并提供一个易于使用的接口给客户端。
  3. 层次化结构:在层次化结构中,可以使用外观模式来定义系统中每一层的入口,从而简化客户端与系统的交互。

6. 享元模式(Flyweight)

a. 介绍

C++中的享元模式(Flyweight Pattern)旨在通过共享尽可能多的对象来减少内存使用和提高性能。享元模式的核心思想是使用一个共享的对象池来存储那些可以共享的对象实例,而不是每次需要时都创建新的实例。在享元模式中,享元对象(Flyweight)本身是不可变的,并且通常只包含内部状态(即不依赖于外部环境的属性),而外部状态(即依赖于外部环境的属性)则通过客户端来维护。

b. 特点

享元模式的主要特点包括:

  1. 对象共享:享元模式通过共享对象实例来减少内存占用,从而提高性能。
  2. 细粒度对象:享元模式通常用于创建大量细粒度的对象,这些对象可以通过共享来优化性能。
  3. 不可变性:享元对象通常是不可变的,以确保它们可以安全地共享。
  4. 外部状态与内部状态:享元对象包含内部状态(存储在对象内部,可以共享)和外部状态(由客户端维护,不共享)。

c. 优缺点

优点

  1. 减少内存占用:通过共享对象实例,享元模式可以显著减少内存占用。
  2. 提高性能:由于减少了对象的创建和销毁次数,享元模式可以提高系统的性能。
  3. 优化资源使用:享元模式适用于创建大量相似对象的场景,可以优化资源使用。

缺点

  1. 增加复杂性:享元模式需要仔细设计和管理对象池,以及区分内部状态和外部状态,这增加了系统的复杂性。
  2. 外部状态管理:由于外部状态由客户端维护,这可能导致客户端代码变得复杂和难以维护。
  3. 不可变性限制:享元对象的不可变性限制了它们在某些场景下的使用。

d. 示例

以下是一个使用享元模式的简单示例,展示了如何共享字符对象来减少内存使用:

#include <iostream>
#include <unordered_map>
#include <memory>
#include <string>

// 享元接口
class Flyweight {
public:
    virtual ~Flyweight() {}
    virtual void operation(const std::string& extrinsicState) = 0;
};

// 具体享元类
class ConcreteFlyweight : public Flyweight {
private:
    std::string intrinsicState;

public:
    ConcreteFlyweight(const std::string& state) : intrinsicState(state) {}

    void operation(const std::string& extrinsicState) override {
        std::cout << "Intrinsic State: " << intrinsicState << ", Extrinsic State: " << extrinsicState << std::endl;
    }
};

// 享元工厂类,用于管理享元对象池
class FlyweightFactory {
private:
    std::unordered_map<std::string, std::shared_ptr<Flyweight>> flyweights;

public:
    std::shared_ptr<Flyweight> getFlyweight(const std::string& key) {
        if (flyweights.find(key) == flyweights.end()) {
            flyweights[key] = std::make_shared<ConcreteFlyweight>(key);
        }
        return flyweights[key];
    }
};

// 客户端代码
int main() {
    FlyweightFactory factory;

    // 获取共享的享元对象
    std::shared_ptr<Flyweight> flyweight1 = factory.getFlyweight("A");
    std::shared_ptr<Flyweight> flyweight2 = factory.getFlyweight("A");
    std::shared_ptr<Flyweight> flyweight3 = factory.getFlyweight("B");

    // 执行操作,传递外部状态
    flyweight1->operation("First call to A");
    flyweight2->operation("Second call to A");
    flyweight3->operation("Call to B");

    // 验证对象是否共享
    if (flyweight1 == flyweight2) {
        std::cout << "Flyweight1 and Flyweight2 are shared." << std::endl;
    } else {
        std::cout << "Flyweight1 and Flyweight2 are not shared." << std::endl;
    }

    return 0;
}

在这个示例中,Flyweight接口定义了一个操作,该操作接受一个外部状态作为参数。ConcreteFlyweight类实现了Flyweight接口,并存储了一个内部状态。FlyweightFactory类管理一个享元对象池,并在需要时返回共享的享元对象。客户端代码通过工厂获取享元对象,并传递外部状态来执行操作。

e. 应用场景

享元模式适用于以下场景:

  1. 大量细粒度对象:当系统中存在大量细粒度对象,且这些对象可以通过共享来优化性能时,可以使用享元模式。
  2. 内存受限:在内存受限的环境中,使用享元模式可以减少内存占用,提高系统性能。
  3. 不可变对象:当对象是不可变的,或者可以安全地共享时,可以使用享元模式。

7. 代理模式(Proxy)

a. 介绍

代理模式(Proxy Pattern)提供了一种代理对象来控制对其他对象的访问。代理对象通常充当原始对象的接口,在不直接访问原始对象的情况下,通过代理对象来控制对原始对象的访问。该模式的主要目的是在不改变原始对象的情况下,附加新功能,提供一种间接的访问方式,以实现对原始对象的控制、管理或延迟加载。

b. 特点

  1. 接口一致性:代理对象和原始对象通常具有相同的接口,这使得客户端可以透明地通过代理对象访问原始对象。
  2. 中介作用:代理对象在客户端和目标对象之间起到中介的作用,可以添加一些额外的操作,如权限控制、性能优化(如延迟加载)、结果缓存、日志记录等。
  3. 控制访问:通过代理对象可以控制对目标对象的访问,例如进行权限检查或延迟加载。
  4. 扩展功能:可以在不修改目标对象代码的前提下,通过代理对象扩展功能。

c. 优缺点

优点

  1. 职责清晰:通过代理模式,可以将原始对象的业务逻辑与其他非本职责事务(如安全性检查、日志记录等)分离,使得编程更加简洁清晰。
  2. 高扩展性:代理模式允许在不修改原始对象代码的情况下,通过代理对象添加额外的功能,提高了系统的扩展性。

缺点

  1. 性能下降:由于引入了额外的代理对象,可能会导致请求的处理速度变慢。
  2. 系统复杂性增加:引入代理模式可能会使系统设计更复杂,需要维护额外的代理类。

d. 示例

以下是一个C++代理模式的示例,展示了如何使用虚拟代理模式来延迟加载图像:

#include <iostream>
#include <memory>
#include <string>

// 抽象接口
class Image {
public:
    virtual void display() = 0;
    virtual ~Image() {}
};

// 真实图像类
class RealImage : public Image {
    std::string filename;
public:
    RealImage(const std::string& filename) : filename(filename) {
        loadFromDisk();
    }
    void display() override {
        std::cout << "Displaying " << filename << std::endl;
    }
private:
    void loadFromDisk() {
        std::cout << "Loading " << filename << std::endl;
        // 模拟耗时加载过程
        std::this_thread::sleep_for(std::chrono::seconds(2));
    }
};

// 代理类
class ProxyImage : public Image {
    std::unique_ptr<RealImage> realImage;
    std::string filename;
public:
    ProxyImage(const std::string& filename) : filename(filename), realImage(nullptr) {}
    void display() override {
        if (!realImage) {
            realImage = std::make_unique<RealImage>(filename); // 延迟初始化
        }
        realImage->display();
    }
};

int main() {
    std::unique_ptr<Image> image = std::make_unique<ProxyImage>("test_10mb.jpg");
    std::cout << "Image will be displayed now:\n";
    image->display(); // 图像在需要时才被加载和显示
    return 0;
}

在这个示例中,ProxyImage 类充当 RealImage 类的代理,并在实际需要显示图像时才加载图像,从而减少了程序启动时的加载时间。

e. 应用场景

  1. 远程代理:用于在不同地址空间中的两个对象之间通信,将请求发送到远程对象。这在网络编程中非常常见,如RPC(远程过程调用)或分布式系统中。
  2. 虚拟代理:用于延迟加载资源,即在需要时才加载资源。这可以优化资源的加载过程,减少程序启动时的加载时间。
  3. 保护代理:用于控制对对象的访问权限,例如只有特定用户才能访问某个对象。这在权限管理系统中非常有用。
  4. 缓存代理:用于缓存对象的访问结果,以避免重复执行计算密集型操作。这可以提高系统的性能和响应速度。

代理模式在业务系统中也有广泛的应用,如开发非功能性需求(如监控、统计、鉴权、限流等)时,可以将这些需求与主业务功能解耦,放到代理类中统一处理。

三、行为型模式

1. 策略模式(Strategy)

a. 介绍

策略模式(Strategy Pattern)定义了一系列算法,并将每个算法封装起来,使它们可以互换使用。策略模式让算法的变化独立于使用算法的客户,即算法的变化不会影响到使用算法的客户。在策略模式中,算法被封装在独立的策略类中,客户端通过上下文类(Context)来调用具体的策略算法,而不需要知道算法的具体实现。

b. 特点

  1. 算法封装:策略模式将每个算法封装在独立的类中,使得算法可以独立于使用它的客户端而变化。
  2. 可替换性:策略模式使得算法可以自由地切换,客户端可以在运行时动态地选择使用哪种算法。
  3. 扩展性良好:新增策略或修改策略时,不需要修改现有代码,符合开闭原则(对扩展开放,对修改关闭)。

c. 优缺点

优点

  1. 算法自由切换:客户端可以在运行时动态地选择使用哪种算法,提高了系统的灵活性。
  2. 避免多重条件判断:策略模式可以避免在客户端代码中使用大量的条件判断语句来选择相应的算法,使得代码更加简洁和易于维护。
  3. 扩展性良好:新增策略或修改策略时,不需要修改现有代码,降低了系统的维护成本。

缺点

  1. 策略类增多:每种策略都需要一个新类,可能导致类数量增多,增加了系统的复杂性。
  2. 客户端需了解策略:客户端在选择合适的策略时需要了解每种策略的差异,这可能会增加客户端的复杂性。

d. 示例

以下是一个C++策略模式的示例,展示了如何使用策略模式来计算不同的折扣:

#include <iostream>
#include <memory>

// 抽象策略类
class DiscountStrategy {
public:
    virtual double calculateDiscount(double price) const = 0;
    virtual ~DiscountStrategy() {}
};

// 具体策略类A:无折扣
class NoDiscountStrategy : public DiscountStrategy {
public:
    double calculateDiscount(double price) const override {
        return price;
    }
};

// 具体策略类B:百分比折扣
class PercentageDiscountStrategy : public DiscountStrategy {
    double percentage;
public:
    PercentageDiscountStrategy(double percentage) : percentage(percentage) {}
    double calculateDiscount(double price) const override {
        return price * (1 - percentage / 100.0);
    }
};

// 具体策略类C:固定金额折扣
class FixedAmountDiscountStrategy : public DiscountStrategy {
    double amount;
public:
    FixedAmountDiscountStrategy(double amount) : amount(amount) {}
    double calculateDiscount(double price) const override {
        return price - amount;
    }
};

// 上下文类
class ShoppingCart {
    std::unique_ptr<DiscountStrategy> strategy;
public:
    void setDiscountStrategy(std::unique_ptr<DiscountStrategy> strategy) {
        this->strategy = std::move(strategy);
    }
    double checkout(double price) const {
        return strategy->calculateDiscount(price);
    }
};

int main() {
    ShoppingCart cart;

    double price = 100.0;

    // 使用无折扣策略
    cart.setDiscountStrategy(std::make_unique<NoDiscountStrategy>());
    std::cout << "Price with no discount: " << cart.checkout(price) << std::endl;

    // 使用百分比折扣策略
    cart.setDiscountStrategy(std::make_unique<PercentageDiscountStrategy>(10));
    std::cout << "Price with 10% discount: " << cart.checkout(price) << std::endl;

    // 使用固定金额折扣策略
    cart.setDiscountStrategy(std::make_unique<FixedAmountDiscountStrategy>(20));
    std::cout << "Price with $20 discount: " << cart.checkout(price) << std::endl;

    return 0;
}

在这个示例中,DiscountStrategy 是一个抽象策略类,定义了计算折扣的接口。NoDiscountStrategyPercentageDiscountStrategyFixedAmountDiscountStrategy 是具体策略类,分别实现了不同的折扣算法。ShoppingCart 是上下文类,它持有一个指向 DiscountStrategy 的指针,并通过该指针调用具体的折扣算法。

e. 应用场景

  1. 算法选择:当系统需要在不同的算法之间进行切换以适应不同的性能需求或业务逻辑时,可以使用策略模式。例如,在排序系统中,可能需要根据不同的数据分布或性能要求选择不同的排序算法(如快速排序、归并排序、堆排序等)。
  2. 业务规则变化:当系统的业务规则经常发生变化,且这些规则的实现可以抽象为一系列算法时,可以使用策略模式。这样,可以通过添加新的策略类来轻松地扩展系统,而不需要修改现有代码。
  3. 多种支付方式:在支付系统中,可以根据不同的支付方式(如银行卡支付、微信支付、支付宝支付等)创建不同的支付策略类,并在需要时切换支付方式。
  4. 动态行为选择:在一些情况下,一个对象可能需要根据不同的条件执行不同的行为。如果使用传统的多重条件语句来实现这种行为的选择,会导致代码的可读性和可维护性降低。通过策略模式,可以将这些行为分离到各自的策略类中,并通过上下文类来根据条件选择不同的策略类执行。

2. 模板方法模式(Template Method)

a. 介绍

模板方法模式(Template Method Pattern)在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中实现。模板方法使得子类可以不改变算法的结构即可重定义算法的某些特定步骤。这是一种代码复用的基本技术,它在类层次上以一种不改变调用端代码的方式来实现新的功能。

b. 特点

  1. 算法骨架:模板方法定义了一个算法的骨架,将一些具体步骤延迟到子类中实现。
  2. 代码复用:通过把算法中不变的部分移到父类,而可变的行为则允许子类去实现,从而实现了代码的复用。
  3. 钩子方法:模板方法中包含了一些“钩子”(hook)方法,这些方法在模板方法中默认不做任何事情,但可以在子类中实现,以改变算法的行为。
  4. 高内聚:通过将算法的不同部分分离到不同的方法中,增强了类的内聚性。

c. 优缺点

优点

  1. 代码复用:通过将算法的共同部分提取到父类中,减少了重复代码。
  2. 扩展性好:通过子类来实现算法的不同部分,可以很容易地扩展新的算法。
  3. 封装性好:父类中的模板方法对外隐藏了算法的具体实现,只暴露了一个接口。

缺点

  1. 增加了类的复杂性:由于引入了父类和子类,以及钩子方法,可能会增加代码的复杂性。
  2. 灵活性受限:由于算法的骨架已经在父类中定义,子类只能在这个框架内实现算法,不能改变算法的整体结构。

d. 示例

以下是一个C++模板方法模式的示例,展示了如何在一个咖啡冲泡过程中使用模板方法模式:

#include <iostream>
#include <string>

// 抽象类,定义模板方法和钩子方法
class Coffee {
public:
    void prepareRecipe() {
        boilWater();
        brewCoffeeGrinds();
        pourInCup();
        if (wantsMilk()) {
            addMilk();
        }
    }

    // 模板方法,定义了冲泡咖啡的步骤
    virtual void boilWater() const = 0;
    virtual void brewCoffeeGrinds() const = 0;
    virtual void pourInCup() const = 0;

    // 钩子方法,子类可以重写以改变行为
    virtual bool wantsMilk() const {
        return true;
    }

    // 钩子方法的具体实现
    virtual void addMilk() const {
        std::cout << "Adding milk to coffee." << std::endl;
    }
};

// 具体类,实现模板方法中的抽象方法
class DarkRoast : public Coffee {
public:
    void boilWater() const override {
        std::cout << "Boiling water for dark roast coffee." << std::endl;
    }

    void brewCoffeeGrinds() const override {
        std::cout << "Dripping coffee through dark roast grinds." << std::endl;
    }

    void pourInCup() const override {
        std::cout << "Pouring dark roast coffee into a cup." << std::endl;
    }

    // 重写钩子方法以改变行为
    bool wantsMilk() const override {
        return false; // Dark roast coffee doesn't want milk
    }
};

// 具体类,实现模板方法中的抽象方法
class Espresso : public Coffee {
public:
    void boilWater() const override {
        std::cout << "Boiling water for espresso." << std::endl;
    }

    void brewCoffeeGrinds() const override {
        std::cout << "Dripping coffee through espresso grinds." << std::endl;
    }

    void pourInCup() const override {
        std::cout << "Pouring espresso into a small cup." << std::endl;
    }

    // 可以选择重写钩子方法,但在这个例子中保持默认行为
};

int main() {
    DarkRoast darkRoast;
    Espresso espresso;

    std::cout << "Making a dark roast coffee:" << std::endl;
    darkRoast.prepareRecipe();

    std::cout << "\nMaking an espresso coffee:" << std::endl;
    espresso.prepareRecipe();

    return 0;
}

e. 应用场景

  1. 算法框架:当需要定义一个算法的框架,并允许子类在不改变算法结构的情况下重定义算法的某些步骤时,可以使用模板方法模式。
  2. 代码复用:当多个类有共同的算法逻辑,但某些步骤的具体实现不同时,可以使用模板方法模式来提取共同部分,减少代码重复。
  3. 复杂过程控制:当一个过程需要按照固定的步骤执行,但某些步骤的具体实现可能因情况而异时,可以使用模板方法模式来组织代码。
  4. 扩展性需求:当需要扩展现有系统的功能,而又不希望修改现有代码时,可以考虑使用模板方法模式来添加新的实现。

3. 观察者模式(Observer)

a. 介绍

观察者模式(Observer Pattern)定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象,这个主题对象在状态变化时,会通知所有观察者对象,使得它们能够自动更新自己。这种模式有时又称作发布-订阅模式、模型-视图模式。在C++中,观察者模式通过类的继承和接口的实现来构建这种一对多的依赖关系。

b. 特点

  1. 抽象耦合:观察者和被观察者是抽象耦合的,一个目标仅仅知道他有一系列的观察者,每个都符合抽象观察者的接口,但是不知道任何一个观察者属于哪一具体类。这样目标和观察者之间的耦合是抽象的和最小的。
  2. 触发机制:观察者模式建立了一套触发机制,支持广播通信。当被观察者的状态发生变化时,它会通知所有注册的观察者,而不需要指定具体的接受者。
  3. 可扩展性:观察者模式符合“开闭原则”,即增加新的具体观察者无须修改原有的系统代码,只需实现抽象观察者接口并注册到被观察者即可。

c. 优缺点

优点

  1. 降低耦合度:观察者模式降低了系统各模块之间的耦合度,增强了系统的可扩展性和灵活性。
  2. 支持广播通信:观察者模式支持广播通信,使得一个被观察者可以通知多个观察者,而不需要知道具体的接受者。
  3. 符合开闭原则:观察者模式符合开闭原则,可以通过增加新的观察者来扩展系统,而不需要修改原有的代码。

缺点

  1. 性能开销:在大规模系统中,通知多个观察者可能会对性能产生影响。此外,使用动态观察者列表可能会引入内存开销。
  2. 循环依赖:如果在观察者和被观察者之间有循环依赖关系,可能会导致系统崩溃。
  3. 更新顺序:观察者被通知的顺序在某些情况下可能是重要的,但观察者模式本身并不保证特定的通知顺序。

d. 示例

以下是一个C++观察者模式的简单示例,展示了如何构建一个天气监测系统的观察者模式:

#include <iostream>
#include <vector>
#include <memory>
#include <string>

// 抽象观察者类
class Observer {
public:
    virtual void update(const std::string& message) = 0;
    virtual ~Observer() {}
};

// 具体观察者类,如显示器
class Display : public Observer {
    std::string name;
public:
    Display(const std::string& name) : name(name) {}
    void update(const std::string& message) override {
        std::cout << name << " received message: " << message << std::endl;
    }
};

// 抽象被观察者类(主题)
class Subject {
    std::vector<std::shared_ptr<Observer>> observers;
protected:
    void attach(std::shared_ptr<Observer> observer) {
        observers.push_back(observer);
    }
    void detach(std::shared_ptr<Observer> observer) {
        observers.erase(std::remove(observers.begin(), observers.end(), observer), observers.end());
    }
    void notify(const std::string& message) {
        for (auto& observer : observers) {
            observer->update(message);
        }
    }
};

// 具体被观察者类,如天气数据
class WeatherData : public Subject {
    float temperature;
    float humidity;
    float pressure;
public:
    void measurementsChanged() {
        notify(measurements());
    }
    void setMeasurements(float temperature, float humidity, float pressure) {
        this->temperature = temperature;
        this->humidity = humidity;
        this->pressure = pressure;
        measurementsChanged();
    }
private:
    std::string measurements() {
        return "Current conditions: " +
               std::to_string(temperature) + "F degrees and " +
               std::to_string(humidity) + "% humidity and " +
               std::to_string(pressure) + " Pa pressure";
    }
};

int main() {
    WeatherData weatherData;

    Display currentDisplay("Current conditions display");
    Display forecastDisplay("Forecast display");

    weatherData.attach(std::make_shared<Display>(currentDisplay));
    weatherData.attach(std::make_shared<Display>(forecastDisplay));

    weatherData.setMeasurements(80, 65, 30.4f);

    weatherData.detach(std::make_shared<Display>(forecastDisplay));

    weatherData.setMeasurements(82, 70, 29.2f);

    return 0;
}

在这个示例中,WeatherData是被观察者(主题),它维护了一个观察者列表,并在其状态(温度、湿度、气压)发生变化时通知所有注册的观察者(Display对象)。Display是具体观察者类,它实现了Observer接口中的update方法,用于接收并处理来自被观察者的通知。

e. 应用场景

观察者模式广泛应用于事件处理系统、数据更新通知、电商订阅、消息订阅、状态更新以及动态图形等场景。在这些场景中,一个对象的状态变化需要通知其他多个对象,并且这些对象需要自动更新以反映这种变化。观察者模式通过定义一对多的依赖关系,使得这种通知和更新变得简单而高效。

4. 状态模式(State)

a. 介绍

状态模式是一种行为设计模式,它允许一个对象在其内部状态发生改变时改变它的行为,从而使得该对象看起来像是改变了其类。状态模式主要用于实现一个对象在多种状态转换时能够自动切换到正确的行为。在状态模式中,状态的变更引起了行为的变更,从外部看起来好像这个对象对应的类发生了改变一样。

b. 特点

  1. 状态表示:状态模式使用类来表示状态,每一个状态都是一个类,这些类实现了相同的行为接口。
  2. 状态切换:状态模式在状态类中封装了状态转换的逻辑,使得状态的切换对于客户端而言是透明的。
  3. 行为封装:状态模式将对象在不同状态下的不同行为封装在一个个状态类中,通过设置不同的状态对象可以让环境对象拥有不同的行为。

c. 优缺点

优点

  1. 封装性:状态的变更逻辑和动作执行封装在状态对象中,易于维护和扩展。
  2. 去除庞大的条件分支语句:状态模式通过把各种状态转移逻辑分布到状态对象中,去掉了原系统中对象的行为巨大且复杂的条件分支语句。
  3. 集中化状态管理:所有状态相关的代码都存在于某个State子类中,易于理解。
  4. 维护和扩展更简单:新增状态只需增加新的状态类,不需改变已有的状态类和上下文。

缺点

  1. 类膨胀:如果状态很多,而且状态的逻辑也很复杂,就可能会产生很多的状态类,导致系统变得非常庞大。
  2. 依赖性:状态模式把各种状态对象的逻辑都分布在各个状态类中,每个状态类都依赖上下文对象,这增加了各个类之间的依赖性。
  3. 对开闭原则的支持不好:增加新的状态类需要修改那些负责状态转换的源代码,否则无法转换到新增状态;而且修改某个状态类的行为也需修改对应类的源代码。

d. 示例

以下是一个简单的状态模式示例,用于模拟一个游戏角色的不同状态(站立、跳跃、蹲下):

// 状态基类
class State {
public:
    virtual ~State() {}
    virtual void handleInput(CharacterContext& context, char input) = 0;
    virtual void update(CharacterContext& context) = 0;
};

// 具体状态类:站立状态
class StandingState : public State {
public:
    void handleInput(CharacterContext& context, char input) {
        if (input == 'D') {
            context.changeState(new DuckingState());
        } else if (input == 'J') {
            context.changeState(new JumpingState());
        }
    }
    void update(CharacterContext& context) {
        std::cout << "Character is standing." << std::endl;
    }
};

// 具体状态类:跳跃状态
class JumpingState : public State {
public:
    void handleInput(CharacterContext& context, char input) {
        if (input == 'D') {
            context.changeState(new DuckingState());
        }
    }
    void update(CharacterContext& context) {
        std::cout << "Character is jumping." << std::endl;
    }
};

// 具体状态类:蹲下状态
class DuckingState : public State {
public:
    void handleInput(CharacterContext& context, char input) {
        if (input == 'S') {
            context.changeState(new StandingState());
        }
    }
    void update(CharacterContext& context) {
        std::cout << "Character is ducking." << std::endl;
    }
};

// 环境类
class CharacterContext {
public:
    CharacterContext(State* initialState) : currentState(initialState) {}
    ~CharacterContext() { delete currentState; }
    void handleInput(char input) { currentState->handleInput(*this, input); }
    void changeState(State* newState) { delete currentState; currentState = newState; }
    void update() { currentState->update(*this); }

private:
    State* currentState;
};

// 使用示例
int main() {
    CharacterContext context(new StandingState());
    context.handleInput('J'); // 切换到跳跃状态
    context.update(); // 输出:Character is jumping.
    context.handleInput('D'); // 切换到蹲下状态
    context.update(); // 输出:Character is ducking.
    context.handleInput('S'); // 切换回站立状态
    context.update(); // 输出:Character is standing.
    return 0;
}

e. 应用场景

状态模式在实际开发中具有较高的使用频率,适用于以下场景:

  1. 对象的行为依赖于它的状态:状态的改变将导致行为的变化。
  2. 存在大量与对象状态有关的条件语句:这些条件语句的出现会导致代码的可维护性和灵活性变差,不能方便地增加和删除状态,并且导致客户类与类库之间的耦合增强。

具体的应用实例包括:

  1. 电梯升降系统的设计:存在打开、关闭、运行、停止状态,各个状态下将有不同的行为。
  2. 投票系统的设计:根据投票次数判断投票状态,如正常投票、重复投票、恶意投票、黑名单状态等。
  3. 游戏角色的状态管理:如角色的生命值、魔法值等状态的变化会导致角色行为的变化。
  4. 银行取款系统的设计:存在正常状态、透支状态、冻结状态等,不同状态下将有不同行为。
  5. 操作系统的任务调度:存在等待状态、就绪状态、运行状态、停止状态等。

5. 命令模式(Command)

a. 介绍

命令模式允许将请求封装为一个对象,从而使得可以参数化客户端请求、将请求排队或者记录请求日志,以及支持可撤销的操作。在C++中,命令模式通常由一个抽象命令类、具体命令类、命令接收者类和调用者类组成。该模式的核心思想是将请求封装成对象,从而实现请求的发送者和接收者之间的解耦,增加系统的灵活性和可扩展性。

b. 特点*

  1. 封装性:命令模式将请求封装成对象,使得请求的发送者和接收者之间不需要直接交互,而是通过命令对象进行通信。
  2. 灵活性:由于命令对象封装了请求的所有信息,因此可以很容易地添加新的命令类,而不需要修改现有的代码。
  3. 可撤销性:命令模式可以记录每个命令的执行状态,从而实现撤销和重做功能。
  4. 队列处理:命令模式支持将命令对象存储在一个队列中,从而实现命令的排队执行。

c. 优缺点

优点

  1. 降低系统耦合度:命令模式将请求发送者和接收者解耦,使得两者不需要直接交互,提高了系统的灵活性和可扩展性。
  2. 容易扩展:由于命令对象完全封装了请求相关的所有信息,因此可以很容易地添加新的命令类,而不需要修改现有的代码。
  3. 支持撤销和恢复操作:命令模式可以记录每个命令的执行状态,从而实现撤销和重做功能。
  4. 支持日志和事务:命令对象封装了请求相关的所有信息,因此可以很容易地实现日志和事务功能。

缺点

  1. 代码复杂性增加:使用命令模式会增加代码的复杂性,因为需要为每个具体的命令对象创建一个类,这会导致类的数量增加。
  2. 内存消耗增加:使用命令模式还会增加内存消耗,因为需要为每个具体的命令对象创建一个实例。
  3. 执行效率降低:每个具体的命令对象需要维护其状态信息,并且每次执行都需要创建一个新的命令对象实例,这可能会导致性能问题。

d. 示例

以下是一个简单的C++命令模式示例:

#include <iostream>
#include <vector>

// 抽象命令类
class Command {
public:
    virtual ~Command() {}
    virtual void execute() = 0;
};

// 具体命令类
class ConcreteCommand : public Command {
private:
    Receiver* receiver;
public:
    ConcreteCommand(Receiver* recv) : receiver(recv) {}
    void execute() {
        receiver->action();
    }
};

// 命令接收者类
class Receiver {
public:
    void action() {
        std::cout << "Receiver::action" << std::endl;
    }
};

// 调用者类
class Invoker {
private:
    Command* command;
public:
    void setCommand(Command* cmd) {
        command = cmd;
    }
    void executeCommand() {
        if (command) {
            command->execute();
        }
    }
};

int main() {
    // 创建接收者对象
    Receiver* receiver = new Receiver();
    
    // 创建具体命令对象并设置接收者
    Command* command = new ConcreteCommand(receiver);
    
    // 创建调用者对象并设置命令
    Invoker* invoker = new Invoker();
    invoker->setCommand(command);
    
    // 执行命令
    invoker->executeCommand();
    
    // 释放内存
    delete receiver;
    delete command;
    delete invoker;
    
    return 0;
}

在这个示例中,我们定义了一个抽象命令类Command,一个具体命令类ConcreteCommand,一个命令接收者类Receiver,以及一个调用者类Invoker。客户端代码通过创建接收者对象、具体命令对象以及调用者对象,并将具体命令对象设置给调用者对象,最后通过调用者对象执行命令。

e. 应用场景

  1. 系统需要将请求调用者和请求接收者解耦:命令模式可以将调用者和接收者之间的耦合关系降低,使得调用者不需要知道接收者的具体实现细节,只需要通过命令对象来调用接收者的方法。
  2. 系统需要在不同的时间指定请求、将请求排队和执行请求:命令模式可以将命令对象存储在一个队列中,从而实现命令的排队执行。
  3. 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作:命令模式可以记录每个命令的执行状态,从而实现撤销和重做功能。
  4. 系统需要将一组操作组合在一起,即支持宏命令:命令模式可以将多个命令组合成一个宏命令,从而实现复杂的操作序列。
  5. 系统需要支持动态添加命令:命令模式可以在运行时动态地添加新的命令,而不需要修改已有的代码,这使得系统更加灵活和可扩展。

6. 迭代器模式(Iterator)

a. 介绍

迭代器模式(Iterator Pattern)是一种行为型设计模式,它提供了一种方法顺序访问集合对象中的各个元素,而不暴露该对象的内部表示。在C++中,迭代器通常是一个类对象,它支持自增操作和解引用操作,用于遍历集合中的所有元素。通过迭代器模式,可以提高代码的可维护性和可扩展性,同时还可以与其他设计模式结合使用,实现更复杂的功能。

b. 特点

  1. 分离遍历行为:迭代器模式将集合对象的遍历行为从集合对象中分离出来,由迭代器类负责遍历操作,使得集合对象的内部结构和实现细节对外部代码透明。
  2. 统一接口:迭代器模式为遍历不同的集合结构提供了一个统一的接口,外部代码可以通过迭代器接口访问集合对象中的元素,而无需关心集合对象的内部实现。
  3. 支持多种遍历方式:迭代器模式可以支持多种遍历方式,如正向遍历、反向遍历、随机访问等,满足不同的遍历需求。

c. 优缺点

优点

  1. 提高代码的可维护性和可扩展性:通过将遍历算法与容器对象分离,可以使算法更加灵活和可复用,从而提高代码的可维护性和可扩展性。
  2. 封装容器对象的遍历:迭代器模式封装了容器对象的遍历过程,隐藏了容器的内部实现细节,从而提高了代码的安全性。
  3. 支持泛型编程:迭代器模式支持泛型编程,可以遍历不同类型的容器对象,从而提高了代码的通用性和复用性。

缺点

  1. 可能引入一些性能开销:在使用迭代器遍历容器对象时,每次访问元素都需要进行解引用操作,这可能会导致一些额外的开销。
  2. 可能增加代码复杂度:在实现迭代器模式时,需要考虑迭代器对象的安全性、正确性等问题,这可能会增加代码的复杂度。
  3. 不支持并发访问:由于迭代器对象的遍历是基于容器对象的内部状态的,所以在多线程并发访问时可能会出现问题。

d. 示例

以下是一个基于C++ vector的迭代器实现的例子:

#include <vector>
#include <iostream>

template <typename T>
class Iterator {
public:
    Iterator(std::vector<T>* data, size_t index) : data_(data), index_(index) {}

    Iterator& operator++() {
        ++index_;
        return *this;
    }

    T& operator*() {
        return (*data_)[index_];
    }

    bool operator==(const Iterator& other) const {
        return data_ == other.data_ && index_ == other.index_;
    }

    bool operator!=(const Iterator& other) const {
        return !(*this == other);
    }

private:
    std::vector<T>* data_;
    size_t index_;
};

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    Iterator<int> it(&vec, 0);

    while (it != Iterator<int>(&vec, vec.size())) {
        std::cout << *it << " ";
        ++it;
    }

    std::cout << std::endl;
    return 0;
}

在这个例子中,我们定义了一个Iterator模板类,用于遍历std::vector<T>类型的容器。Iterator类包含了构造函数、自增操作、解引用操作以及等于操作等成员函数。在main函数中,我们创建了一个std::vector<int>类型的容器,并使用Iterator类对其进行遍历。

e. 应用场景

迭代器模式在C++中广泛应用于各种容器类对象,如vector、list、set等。以下是一些具体的应用场景:

  1. 数据的分层访问:当数据源复杂且需要逐步解析时,迭代器使得上层代码可以按需获取数据,而不必关心底层实现的细节。
  2. 流式处理:在大数据处理场景,通常采用惰性加载或逐行读取的方式,只在需要时才请求下一个元素,类似于缓存机制。迭代器模式可以很好地支持这种流式处理方式。
  3. 算法封装:许多高级算法,如排序、搜索和图计算等,都可以用迭代器来驱动,这样既通用又灵活。
  4. 数据库查询:数据库API通常会提供游标,用户借助游标逐条获取查询结果,而无需了解具体的SQL执行细节。迭代器模式可以模拟这种游标机制。
  5. 文件系统开发:在读取大文件或目录树时,使用迭代器模式来依次访问每个文件或子目录,避免一次性加载所有内容导致内存溢出。

综上所述,迭代器模式在C++中是一种非常有用的设计模式,它提供了遍历集合对象的方法,同时隐藏了集合对象的内部实现细节,使得代码更加简洁、安全和可维护。

7. 责任链模式(Chain of Responsibility)

a. 介绍

责任链模式(Chain of Responsibility Pattern),又称为职责链模式。它允许对象以链式的方式组织起来,以便对请求进行处理。在这种模式中,多个对象都有可能接收并处理同一个请求,而这些对象被链接成一条链,请求会沿着这条链传递,直到被某个对象处理为止。责任链模式为多个对象处理同一请求提供了一个灵活的机制,而无需在发送者和多个请求处理者之间显式地指定耦合关系。

在责任链模式中,每个处理者对象都包含对下一个处理者的引用,从而形成了一个处理者链。当一个请求到达某个处理者时,该处理者会先判断自己是否能处理该请求。如果能处理,则直接处理;如果不能处理,则将该请求传递给链中的下一个处理者。这样,请求会沿着链一直传递,直到被某个处理者处理,或者请求到达了链的末尾而未被处理。

b. 特点

责任链模式的主要特点包括:

  1. 多个处理者:责任链模式允许有多个处理者对象,它们按照一定顺序链接在一起。
  2. 请求传递:当一个请求到来时,它会沿着处理者链传递,直到被某个处理者处理为止。
  3. 动态决定处理者:具体哪个处理者处理请求,是在运行时动态决定的,而不是在编译时静态指定的。
  4. 松散耦合:责任链模式实现了发送者和接收者之间的松散耦合,发送者只需要将请求发送到链的起点,而不需要知道哪个处理者最终会处理该请求。

c. 优缺点

责任链模式的优点:

  1. 降低耦合度:责任链模式将请求的发送者和接收者解耦,发送者只需要知道链的起点,而不需要知道具体的处理者。
  2. 增强灵活性:可以在运行时改变链中处理者的顺序和数量,从而方便地调整处理逻辑。
  3. 简化对象:对象只需要知道如何将请求发送到链上,而不需要知道链的结构细节,这简化了对象的设计。
  4. 分布处理:可以让多个对象都有机会处理请求,从而将单个请求的处理分散到多个类中,提高了系统的可扩展性和可维护性。

责任链模式的缺点:

  1. 请求可能未被处理:在某些情况下,请求可能会到达链的末尾都没有被处理,这可能导致系统错误或异常。
  2. 性能问题:由于请求的处理需要经过多个对象,可能会影响系统的性能,特别是在链较长时。
  3. 复杂性增加:系统设计更加复杂,需要维护链中的顺序和关系,增加了系统的复杂性和调试难度。
  4. 可能导致循环调用:如果建链不当,可能造成循环调用,导致系统进入死循环。

d. 示例

以下是一个简单的责任链模式示例,用于演示如何在C++中实现该模式:

#include <iostream>
#include <memory>

// 抽象处理者
class Handler {
public:
    virtual ~Handler() {}
    virtual void HandleRequest(int request) = 0;
    virtual void SetNext(std::unique_ptr<Handler> next) = 0;
protected:
    std::unique_ptr<Handler> next;
};

// 具体处理者A
class ConcreteHandlerA : public Handler {
public:
    void SetNext(std::unique_ptr<Handler> next) override {
        this->next = std::move(next);
    }
    void HandleRequest(int request) override {
        if (request >= 0 && request < 10) {
            std::cout << "ConcreteHandlerA handled request " << request << std::endl;
        } else if (next) {
            next->HandleRequest(request);
        } else {
            std::cout << "No handler could process request " << request << std::endl;
        }
    }
};

// 具体处理者B
class ConcreteHandlerB : public Handler {
public:
    void SetNext(std::unique_ptr<Handler> next) override {
        this->next = std::move(next);
    }
    void HandleRequest(int request) override {
        if (request >= 10 && request < 20) {
            std::cout << "ConcreteHandlerB handled request " << request << std::endl;
        } else if (next) {
            next->HandleRequest(request);
        } else {
            std::cout << "No handler could process request " << request << std::endl;
        }
    }
};

// 客户端代码
int main() {
    // 创建处理者对象并使用智能指针管理
    std::unique_ptr<Handler> handlerA = std::make_unique<ConcreteHandlerA>();
    std::unique_ptr<Handler> handlerB = std::make_unique<ConcreteHandlerB>();

    // 构建处理者链
    handlerA->SetNext(std::move(handlerB));

    // 发送请求给链的第一个处理者
    handlerA->HandleRequest(5);  // 由ConcreteHandlerA处理
    handlerA->HandleRequest(15); // 由ConcreteHandlerB处理
    handlerA->HandleRequest(25); // 无人处理,输出错误信息

    return 0;
}

在这个示例中,Handler是抽象处理者,ConcreteHandlerAConcreteHandlerB是具体处理者。客户端代码创建了这两个处理者对象,并将它们链接在一起形成一条处理者链。然后,客户端向链的第一个处理者发送请求,请求会沿着链传递,直到被某个处理者处理为止。

e. 应用场景

  1. 多个对象可以处理同一请求:当系统中有多个对象可以处理同一个请求时,但具体由哪个对象处理需要在运行时动态决定。
  2. 请求者不明确:当请求者不明确哪个对象应该处理请求时,可以将请求发送给多个对象中的一个,由该对象决定是否处理该请求或将其传递给下一个对象。
  3. 动态指定处理者:当需要在不指定具体接收者的前提下向一个或多个对象发出请求时,可以使用责任链模式。
  4. 系统已存在处理者链:当系统中已经存在一个由处理者对象组成的链时,可以使用责任链模式来扩展或修改该链的处理逻辑。

例如,在请假审批系统中,可以使用责任链模式来处理不同级别的审批请求。每个级别的审批者都是一个处理者,它们按照一定顺序链接在一起形成一条审批链。当有一个请假请求到来时,它会沿着审批链传递,直到被某个审批者处理为止。这样可以灵活地调整审批流程,而不需要修改每个审批者的代码。

8. 备忘录模式(Memento)

a. 介绍

C++中的备忘录模式(Memento Pattern)允许在不破坏封装性的前提下,捕获并存储一个对象的内部状态,并在需要时将其恢复到之前的状态。该模式主要由三个角色组成:发起人(Originator)、备忘录(Memento)和管理者(Caretaker)。

  • 发起人:负责创建一个备忘录,用以记录当前时刻自身的内部状态,并可使用备忘录恢复内部状态。
  • 备忘录:负责存储发起人的内部状态,并可以防止发起人以外的其他对象访问备忘录。
  • 管理者:负责管理备忘录,但不能对备忘录的内容进行操作或检查。

备忘录模式的核心在于保证发起人对象的状态可以在不影响封装性和隐藏性的情况下被保存和恢复。

b. 特点

备忘录模式的特点主要包括:

  1. 封装性保护:通过备忘录对象存储发起人的内部状态,避免了直接暴露发起人的内部结构,从而保护了封装性。
  2. 状态恢复:允许在需要时从备忘录中恢复发起人的状态,实现了状态的回溯。
  3. 灵活性:发起人可以根据需要决定备忘录存储自己的哪些内部状态,提供了灵活性。
  4. 职责分离:发起人负责创建和恢复状态,管理者负责保存和传递备忘录,实现了职责的分离。

c. 优缺点

备忘录模式的优点:

  1. 提供了对象状态的保存和恢复功能:使得系统更加灵活,能够应对状态变化的需求。
  2. 简化了撤销/重做机制:通过保存和恢复状态,可以方便地实现撤销和重做功能。
  3. 提升了系统性能:在某些场景下,通过缓存状态结果,可以避免重复计算,提升系统性能。

备忘录模式的缺点:

  1. 内存消耗:如果需要保存的状态数量很多,可能会占用较多的内存资源。
  2. 复杂性增加:如果应用不当,可能会使代码结构更加复杂,增加维护难度。
  3. 状态管理难度:管理者需要妥善管理备忘录对象,避免状态丢失或混乱。

d. 示例

以下是一个简单的C++备忘录模式示例,模拟了一个文本编辑器的撤销功能:

#include <iostream>
#include <string>
#include <vector>

// 备忘录类:保存状态
class TextMemento {
public:
    TextMemento(const std::string& text) : text_(text) {}
    const std::string& getText() const { return text_; }
private:
    std::string text_;
};

// 发起人类:保存状态、创建备忘录、恢复状态
class TextEditor {
public:
    void setText(const std::string& text) { text_ = text; }
    const std::string& getText() const { return text_; }
    TextMemento createMemento() { return TextMemento(text_); }
    void restoreMemento(const TextMemento& memento) { text_ = memento.getText(); }
private:
    std::string text_;
};

// 管理者类:保存备忘录、恢复备忘录
class TextCaretaker {
public:
    void addMemento(TextMemento memento) { history.push_back(memento); }
    TextMemento getMemento(int index) const { return history[index]; }
private:
    std::vector<TextMemento> history;
};

int main() {
    TextEditor editor;
    TextCaretaker caretaker;
    
    editor.setText("Hello, World!");
    caretaker.addMemento(editor.createMemento());
    
    editor.setText("Goodbye!");
    caretaker.addMemento(editor.createMemento());
    
    std::cout << "Current Text: " << editor.getText() << std::endl;
    
    editor.restoreMemento(caretaker.getMemento(0));
    std::cout << "After Undo: " << editor.getText() << std::endl;
    
    editor.restoreMemento(caretaker.getMemento(1));
    std::cout << "After Redo: " << editor.getText() << std::endl;
    
    return 0;
}

运行结果:

Current Text: Goodbye!
After Undo: Hello, World!
After Redo: Goodbye!

e. 应用场景

备忘录模式在C++中的应用场景广泛,主要包括:

  1. 撤销操作:在文本编辑器、图形编辑器等软件中,实现撤销功能。
  2. 保存游戏进度:在游戏中保存玩家的进度,以便下次继续游戏。
  3. 保存用户设置:在软件中保存用户的设置信息,以便在用户重新打开软件时恢复之前的设置。
  4. 数据库事务管理:在数据库连接的事务中,保存某个时刻的数据库连接状态,以便在出错或需要回滚时能够恢复到之前的状态。

9. 访问者模式(Visitor)

a. 介绍

C++中的访问者模式(Visitor Pattern)提供了一种将数据结构中的元素与作用于这些元素的操作分离的方式。该模式通过定义一个访问者类,来改变一个元素类的执行算法。访问者模式使得你能够在不改变元素类的前提下,定义作用于这些元素的新操作。它主要用于数据结构相对稳定,但操作经常变化的场景。

b. 特点

访问者模式的特点主要包括:

  1. 行为与数据结构的分离:访问者模式将数据结构中的元素与对这些元素的操作分离,使得可以在不修改数据结构的情况下增加新的操作。
  2. 扩展性好:由于行为与数据结构的分离,新的操作可以很容易地通过添加新的访问者类来实现,而不需要修改现有的数据结构。
  3. 符合单一职责原则:访问者模式通过将操作封装在访问者类中,使得每个类只负责自己的职责,符合单一职责原则。

c. 优缺点

访问者模式的优点:

  1. 封装性:可以在不改变元素类的情况下定义新的操作,增加了代码的封装性。
  2. 扩展性:新的访问者可以很容易地添加新的操作,而不需要修改原有的数据结构或操作。
  3. 灵活性:访问者模式允许在运行时动态地选择执行哪种操作,增加了代码的灵活性。

访问者模式的缺点:

  1. 增加了代码的复杂度:由于需要定义多个接口和实现类,访问者模式可能会使代码变得更加复杂。
  2. 可能违反封装原则:访问者可能需要访问元素的内部细节,这通常意味着元素必须暴露一些原本应该是私有的数据。
  3. 对象结构变化困难:如果经常改变对象结构的元素类,那么维护访问者模式将会非常麻烦。

d. 示例

以下是一个简单的C++访问者模式示例,用于展示如何在一个对象结构中应用访问者模式:

#include <iostream>
#include <vector>
#include <memory>

// 访问者接口
class Visitor {
public:
    virtual ~Visitor() {}
    virtual void visit(class ConcreteElement1* element) = 0;
    virtual void visit(class ConcreteElement2* element) = 0;
};

// 元素接口
class Element {
public:
    virtual ~Element() {}
    virtual void accept(Visitor* visitor) = 0;
};

// 具体元素类1
class ConcreteElement1 : public Element {
public:
    void accept(Visitor* visitor) override {
        visitor->visit(this);
    }
    void show() {
        std::cout << "ConcreteElement1" << std::endl;
    }
};

// 具体元素类2
class ConcreteElement2 : public Element {
public:
    void accept(Visitor* visitor) override {
        visitor->visit(this);
    }
    void show() {
        std::cout << "ConcreteElement2" << std::endl;
    }
};

// 具体访问者类
class ConcreteVisitor : public Visitor {
public:
    void visit(ConcreteElement1* element) override {
        element->show();
        std::cout << "Visited by ConcreteVisitor on ConcreteElement1" << std::endl;
    }
    void visit(ConcreteElement2* element) override {
        element->show();
        std::cout << "Visited by ConcreteVisitor on ConcreteElement2" << std::endl;
    }
};

// 对象结构类
class ObjectStructure {
private:
    std::vector<std::shared_ptr<Element>> elements;

public:
    void attach(std::shared_ptr<Element> element) {
        elements.push_back(element);
    }
    void detach(std::shared_ptr<Element> element) {
        elements.erase(std::remove(elements.begin(), elements.end(), element), elements.end());
    }
    void accept(Visitor* visitor) {
        for (const auto& element : elements) {
            element->accept(visitor);
        }
    }
};

int main() {
    ObjectStructure os;
    os.attach(std::make_shared<ConcreteElement1>());
    os.attach(std::make_shared<ConcreteElement2>());

    ConcreteVisitor visitor;
    os.accept(&visitor);

    return 0;
}

在这个示例中,我们定义了一个访问者接口Visitor,两个具体元素类ConcreteElement1ConcreteElement2,以及一个具体访问者类ConcreteVisitorObjectStructure类用于管理元素对象的集合,并允许访问者访问这些元素。在main函数中,我们创建了一个ObjectStructure对象,并向其中添加了两个元素。然后,我们创建了一个ConcreteVisitor对象,并通过ObjectStructureaccept方法让访问者访问这些元素。

e. 应用场景

  1. 数据结构稳定,但操作经常变化:当数据结构相对稳定,但需要对数据结构中的元素进行多种不同的操作时,可以使用访问者模式来分离操作与数据结构,从而在不修改数据结构的情况下增加新的操作。
  2. 需要避免操作污染元素类:当不希望将多种不同的操作直接添加到元素类中,以避免污染元素类的代码时,可以使用访问者模式将操作封装在访问者类中。
  3. 需要实现双重分发:在某些情况下,需要根据元素类型和操作类型来决定执行哪种操作时,可以使用访问者模式来实现双重分发(Double Dispatch),即根据两个对象的类型来决定调用哪个方法。
;