All writing Notes & observations

Design Pattern

写给新手的详细设计模式入门手册,从原理到代码,手把手带你吃透几种常用设计模式。

前言:为什么要学设计模式?

如果你是一个刚入门的 C++ 开发者,你可能会问:" 我会写for循环,会用类和对象,能把功能跑通不就行了吗?为什么要学什么设计模式?"

别急,我们来看一个真实的场景:

假设你写了一个计算器程序,一开始只支持加法。代码很简单:

1
2
3
4
5
int calculate(int a, int b, string op) {
    if (op == "add") {
        return a + b;
    }
}

后来你加了减法:

1
2
3
4
5
6
7
int calculate(int a, int b, string op) {
    if (op == "add") {
        return a + b;
    } else if (op == "sub") {
        return a - b;
    }
}

再加乘法、除法… 最后这个calculate函数变得几百行,每次加新功能都要改这个函数,改着改着就改错了,而且测试起来也特别麻烦。

这就是坏代码的典型特征:

  • 耦合度高:所有逻辑都堆在一起,改一处影响全身

  • 可维护性差:加新功能要改老代码,容易出 Bug

  • 可读性差:几百行的函数,谁看谁头疼

而设计模式,就是前辈们总结出来的,解决这类常见问题的 “最佳实践”。它不是什么黑魔法,只是一套让代码变得优雅、可维护、可扩展的通用套路。

一、设计模式入门:先搞懂这几个概念

1.1 什么是设计模式?

设计模式(Design Pattern)是对软件设计中普遍存在的各种问题,所提出的解决方案。

简单来说:

  • 它不是代码,而是解决问题的思路

  • 它不是标准,而是最佳实践

  • 它不是必须的,但学会了能让你少走很多弯路

1994 年,四位大牛(GoF, Gang of Four)出版了《设计模式:可复用面向对象软件的基础》,总结了 23 种经典的设计模式,这也是我们今天要学习的核心内容。

1.2 23 种模式的分类

这 23 种模式并不是杂乱无章的,它们可以分为三大类:

1. 创建型模式(5 种)

  • 解决的问题:对象怎么创建?

  • 核心思想:把对象的创建和使用分开,不让你到处new对象

  • 包括:单例、工厂方法、抽象工厂、建造者、原型

2. 结构型模式(7 种)

  • 解决的问题:类和对象怎么组合起来,形成更大的结构?

  • 核心思想:如何让类的组合更灵活,功能复用更方便

  • 包括:适配器、桥接、组合、装饰、外观、享元、代理

3. 行为型模式(11 种)

  • 解决的问题:对象之间怎么通信?怎么分配职责?

  • 核心思想:让对象之间的交互更解耦,行为更灵活

  • 包括:观察者、策略、模板方法、迭代器、命令、状态等

1.3 设计模式的基石:SOLID 原则

在学习具体的模式之前,你必须先理解这 5 个基本原则,因为所有的设计模式,本质上都是为了更好地遵守这几个原则。

1. 单一职责原则(SRP)

一个类,只负责一件事。

比如,一个User类,就只负责用户的数据;不要让它同时负责 “把用户数据存到数据库”,那是UserRepository的事。

好处:改需求的时候,你只需要改对应的类,不会影响其他地方。

2. 开闭原则(OCP)

对扩展开放,对修改关闭。

意思是:加新功能的时候,你应该加新的类 / 新的代码,而不是去改已经写好的、测试过的老代码。

这是最重要的一个原则!比如你要加新的支付方式,加个新类就行了,不用去改原来的支付逻辑。

3. 里氏替换原则(LSP)

子类必须能替换掉它的父类,程序运行结果不变。

比如,父类是Bird,有个fly()方法;子类Penguin(企鹅)也是鸟,但它不会飞。如果你强行让Penguin继承Bird,那调用fly()就会报错,这就违反了 LSP。

你可以把Bird拆成两个接口:Flyable(会飞的鸟)和NonFlyable(不会飞的鸟),这样就不会有问题了。

1
2
3
4
5
6
7
8
9
class Flyable {
public:
    virtual void fly() = 0;
};

class NonFlyable {
public:
    virtual void walk() = 0;
};

4. 接口隔离原则(ISP)

不要强迫客户端依赖它不需要的接口。

简单说:接口要小而精,不要搞一个大而全的胖接口。比如,你不要搞一个Animal接口,里面既有eat()、sleep(),又有fly()、swim(),因为不是所有动物都会飞会游泳。

你可以把接口拆成Eater、Sleeper、Flyer、Swimmer,每个类只实现它需要的接口,这样就更清晰了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Eater {
public:
    virtual void eat() = 0;
};

class Sleeper {
public:
    virtual void sleep() = 0;
};

class Flyer {
public:
    virtual void fly() = 0;
};

class Swimmer {
public:
    virtual void swim() = 0;
};

5. 依赖倒置原则(DIP)

依赖抽象,不要依赖具体实现。

高层模块不要依赖低层模块,两者都要依赖抽象。

比如,你开车,你只需要依赖 “车” 这个抽象(它能跑),而不要依赖具体是 “宝马” 还是 “特斯拉”。这样你换车的时候,完全不用改你开车的代码。


二、创建型模式:搞定对象的创建

2.1 单例模式(Singleton):全局唯一的实例

它解决了什么问题?

有时候,我们需要一个类,在整个程序里,只有一个实例。

比如:

  • 全局的配置管理器

  • 日志记录器

  • 数据库连接池

你肯定不想让别人随便new一个配置管理器,不然就会有好几个配置,数据就乱了。

核心思路

  1. 把构造函数私有化,不让别人随便new

  2. 自己在类里面维护唯一的实例

  3. 提供一个静态方法,让别人通过这个方法获取唯一的实例

UML 类图

C++ 代码实现(推荐!线程安全版)

C++11 之后,局部静态变量的初始化是线程安全的,这是最简单最推荐的实现方式!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
#include <string>

class Logger {
private:
    // 1. 私有构造函数!禁止外部创建实例
    Logger() {
        std::cout << "Logger 实例创建了,只会执行一次!" << std::endl;
    }

    // 2. 禁止拷贝构造和赋值操作,防止别人复制出一个新实例
    Logger(const Logger&) = delete; // 禁止拷贝构造
    Logger& operator=(const Logger&) = delete; // 禁止赋值操作

public:
    // 3.全局访问点,获取唯一的实例
    static Logger& getInstance() {
        // C++11 之后,局部静态变量的初始化是线程安全的,保证了单例模式在多线程环境下的正确性
        static Logger instance; // 局部静态变量,第一次调用时创建实例,之后直接返回
        return instance;
    }

    // 业务方法:打印日志
    void log(const std::string& message) {
        std::cout << "日志: " << message << std::endl;
    }
};

// 测试单例模式
int main() {
    // 不管你调用多少次getInstance(),都只会创建一个Logger实例
    Logger::getInstance().log("程序启动了!");
    Logger::getInstance().log("用户登录了!");

    Logger& l1 = Logger::getInstance();
    Logger& l2 = Logger::getInstance();

    // 如果输出的地址相同,说明l1和l2是同一个实例
    std::cout << "l1的地址: " << &l1 << std::endl;
    std::cout << "l2的地址: " << &l2 << std::endl;
    
    return 0;
}

运行结果

1
2
3
4
5
Logger 实例创建了,只会执行一次!
日志: 程序启动了!
日志: 用户登录了!
l1的地址: 0x7ff7c68530b0
l2的地址: 0x7ff7c68530b0

注意事项

  • 不要用饿汉式(程序启动就创建),浪费内存

  • 不要用旧的双重检查锁(DCL),C++11 之前有内存重排序的问题

  • 直接用 Meyers’ Singleton(就是上面的代码),简单又安全!


2.2 工厂模式(Factory):对象创建的 “代工厂”

它解决了什么问题?

如果你的代码里到处都是new具体的类,比如:

1
2
3
4
5
6
7
if (type == "word") {
    doc = new WordDocument();
} else if (type == "pdf") {
    doc = new PdfDocument();
} else if (type == "txt") {
    doc = new TxtDocument();
}

每次加新的文档类型,你都要改这段代码,而且这段代码会散落在你的项目里,维护起来巨麻烦。

工厂模式就是把这些new的逻辑,统一放到一个 “工厂” 里,你要什么对象,找工厂要就行了,不用自己new。

工厂方法模式:最常用的工厂

工厂方法模式,就是给每个产品,配一个对应的工厂,完美符合开闭原则!

UML 类图

C++ 代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <iostream>
#include <memory> // 引入智能指针头文件,使用std::unique_ptr来管理资源,避免内存泄漏。

// ====================== 第一步:定义产品接口 ======================
// 所有文档都要实现这个接口
class Document {
public:
    virtual ~Document() = default;   // 虚析构函数,确保子类能正确析构
    virtual void open() = 0;            // 打开文档,子类必须实现
};

// 具体产品1:Word文档
class WordDoc : public Document {
public:
    void open() override {
        std::cout << "正在打开Word文档: xxx.docx" << std::endl;
    }
};

// 具体产品2:PDF文档
class PDFDoc : public Document {
public:
    void open() override {
        std::cout << "正在打开PDF文档: xxx.pdf" << std::endl;
    }
};

// ====================== 第二步:定义工厂接口 ======================
class DocumentFactory {
public:
    virtual ~DocumentFactory() = default;   // 虚析构函数,确保子类能正确析构
    virtual std::unique_ptr<Document> createDocument() = 0;  // 创建文档,子类必须实现
};

// 具体工厂1:Word文档工厂
class WordDocFactory : public DocumentFactory {
public:
    std::unique_ptr<Document> createDocument() override {
        return std::make_unique<WordDoc>();  // 创建并返回一个Word文档实例
    }
};

// 具体工厂2:PDF文档工厂
class PDFDocFactory : public DocumentFactory {
public:
    std::unique_ptr<Document> createDocument() override {
        return std::make_unique<PDFDoc>();  // 创建并返回一个PDF文档实例
    }
};

// ====================== 第三步:使用工厂创建产品 ======================
// 客户端只依赖工厂接口,不直接依赖具体产品
void clientCode(std::unique_ptr<DocumentFactory> factory) {
    auto doc = factory->createDocument();  // 通过工厂创建文档
    doc->open();  // 打开文档
}

int main() {
    // 我要打开Word文档,就传Word工厂
    clientCode(std::make_unique<WordDocFactory>());

    // 我要打开PDF文档,就传PDF工厂
    clientCode(std::make_unique<PDFDocFactory>());

    // 以后我要加TXT文档?
    // 只需要加一个TxtDoc类,再加一个TxtDocFactory类!
    // 完全不用改clientCode里的代码!完美符合开闭原则!
    
    return 0;
}

// 这样的设计使得我们可以轻松地添加新的文档类型,而不需要修改现有的代码,符合开闭原则(Open/Closed Principle)。每个工厂负责创建一个特定类型的文档,客户端代码只依赖于工厂接口,不需要知道具体的文档实现细节。

运行结果

1
2
正在打开Word文档:xxx.docx
正在打开PDF文档:xxx.pdf

什么时候用工厂模式?

  • 你不知道运行时到底要创建哪个类的实例

  • 你想把对象的创建逻辑封装起来,统一管理

  • 你希望扩展新产品的时候,不用改老代码


三、结构型模式:搞定类的组合

3.1 适配器模式(Adapter):给旧接口做个 “转接头”

它解决了什么问题?

你有没有过这种情况:你有一个旧的类,它的接口和你现在系统需要的接口不兼容,但是你又不能改旧的代码(比如它是第三方库)。

这时候你就需要一个转接头,把旧接口转换成新接口,这就是适配器模式。

举个例子:

  • 旧的打印机有个方法叫printOld(string s)

  • 新系统需要的打印机接口是print(string s)

  • 你搞个适配器,把print的调用,转成调用旧的printOld

UML 类图

C++ 代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>
#include <string>

// 新系统期望的接口
class Printer {
public:
    virtual ~Printer() = default;
    virtual void print(const std::string& content) = 0;
};

// 旧系统的类,接口不兼容
class LegacyPrinter {
public:
    // 旧的接口,与新的接口不兼容
    void printOld(const std::string& content) {
        std::cout << "[旧系统打印]" << content << std::endl;
    }
};

// 适配器,实现新的接口,同时持有旧系统的实例
class PrinterAdapter : public Printer {
private:
    LegacyPrinter legacyPrinter;        // 持有旧的打印机

public:
    // 实现新的接口
    void print(const std::string& content) override {
        // 把新接口的调用,转发给旧的打印机
        legacyPrinter.printOld(content);
    }
};

// 客户端代码,只认识新的Printer接口
void usePrinter(Printer& printer) {
    printer.print("Hello, Adapter!");
}

int main() {
    PrinterAdapter adapter;  // 创建适配器实例
    usePrinter(adapter);     // 客户端使用适配器,实际上调用的是旧系统的打印机
    return 0;
}

// 这样做的好处是,客户端代码只依赖于新的接口(Printer),而不需要修改旧系统的代码(LegacyPrinter)。适配器模式使得两个不兼容的接口能够协同工作,增加了系统的灵活性和可扩展性。

运行结果

1
[旧系统打印] Hello, Adapter!


3.2 装饰器模式(Decorator):动态给对象 “加 buff”

它解决了什么问题?

如果你想给一个对象动态地添加功能,用继承的话,会导致 “类爆炸”。

比如,咖啡:

  • 基础咖啡

  • 咖啡 + 牛奶

  • 咖啡 + 糖

  • 咖啡 + 牛奶 + 糖

  • 咖啡 + 奶泡

  • …

如果你用继承,你要写 N 多个子类:CoffeeWithMilk、CoffeeWithSugar、CoffeeWithMilkAndSugar… 这也太蠢了。

装饰器模式就是解决这个的:你可以动态地给对象叠加功能,想加什么加什么。

UML 类图

C++ 代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <iostream>
#include <string>
#include <memory>

// 咖啡的抽象接口
class Coffee {
public:
    virtual ~Coffee() = default;
    virtual std::string getDescription() = 0;
    virtual double getPrice() = 0;
};

// 基础咖啡:浓缩咖啡
class Espresso : public Coffee {
public:
    std::string getDescription() override {
        return "浓缩咖啡";
    }
    double getPrice() override {
        return 25.0;        // 基础价25
    }
};

// 装饰器的基类,是一个抽象类,继承自Coffee,并持有一个Coffee对象的指针
class CoffeeDecorator : public Coffee {
protected:      // 保护成员,允许子类访问
    std::unique_ptr<Coffee> coffee;     // 持有被装饰的咖啡

public:
    CoffeeDecorator(std::unique_ptr<Coffee> c) : coffee(std::move(c)) {}
};

// 具体的装饰器:加牛奶
class MilkDecorator : public CoffeeDecorator {
public:
    MilkDecorator(std::unique_ptr<Coffee> c) : CoffeeDecorator(std::move(c)) {}

    std::string getDescription() override {
        return coffee->getDescription() + ", 加牛奶";
    }
    double getPrice() override {
        return coffee->getPrice() + 5.0;  // 牛奶加5元
    }
};

// 具体的装饰器:加糖
class SugarDecorator : public CoffeeDecorator {
public:
    SugarDecorator(std::unique_ptr<Coffee> c) : CoffeeDecorator(std::move(c)) {}

    std::string getDescription() override {
        return coffee->getDescription() + ", 加糖";
    }

    double getPrice() override {
        return coffee->getPrice() + 2.0;    // 加糖2元
    }
};

int main() {
    // 1. 先来一杯基础浓缩
    std::unique_ptr<Coffee> coffee = std::make_unique<Espresso>();
    std::cout << coffee->getDescription() << " | 价格:" << coffee->getPrice() << std::endl;

    // 2. 加牛奶
    coffee = std::make_unique<MilkDecorator>(std::move(coffee));  // 用装饰器包装原来的咖啡,把原来的咖啡转移到装饰器中
    std::cout << coffee->getDescription() << " | 价格:" << coffee->getPrice() << std::endl;

    // 3. 再加糖
    coffee = std::make_unique<SugarDecorator>(std::move(coffee));  // 再用装饰器包装一次
    std::cout << coffee->getDescription() << " | 价格:" << coffee->getPrice() << std::endl;
}

// 这样,我们通过装饰器模式,动态地为基础咖啡添加了牛奶和糖的功能,同时保持了代码的清晰和可维护性。每个装饰器都只关注自己添加的功能,而不需要修改原有的咖啡类,这符合开闭原则。

运行结果

1
2
3
浓缩咖啡 | 价格: 25
浓缩咖啡, 加牛奶 | 价格: 30
浓缩咖啡, 加牛奶, 加糖 | 价格: 32


四、行为型模式:搞定对象的交互

4.1 观察者模式(Observer):一对多的 “订阅 - 发布”

它解决了什么问题?

当一个对象的状态发生变化时,你要自动通知所有依赖它的对象,让它们更新。

这就是我们常说的发布 - 订阅模式!

比如:

  • 公众号发文章,所有订阅的粉丝都能收到通知

  • 气象站更新天气,所有的显示器(手机 APP、大屏、报警器)都要同步更新

UML 类图

C++ 代码实现

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#include <iostream>
#include <vector>
#include <string>
#include <memory>
#include <algorithm>  // std::remove_if用于从vector中移除元素

// 前向声明
class Subject;

// 观察者接口
class Observer {
public:
    virtual ~Observer() = default;
    virtual void update(Subject* subject) = 0;  // 收到通知时更新
};

// 主题(被观察者)接口
class Subject {
private:
    // 保存所有的观察者,用weak_ptr避免循环引用,当观察者被销毁时,weak_ptr会自动失效
    std::vector<std::weak_ptr<Observer>> observers;

public:
    virtual ~Subject() = default;

    // 订阅
    void attach(std::shared_ptr<Observer> observer) {
        observers.push_back(observer);
    }

    // 取消订阅
    void detach(std::shared_ptr<Observer> observer) {
        // 使用remove_if和erase从observers中移除与observer相同的元素,wp.lock() == observer表示找到了要移除的观察者
        observers.erase(
            std::remove_if(observers.begin(),observers.end(),
            [&](const std::weak_ptr<Observer>& wp) {
                return wp.lock() == observer;
            }),
            observers.end()
        );
    }

    // 通知所有观察者
    void notify() {
        // 遍历所有观察者,调用它们的update方法
        for (auto& wp : observers) {
            if (auto observer = wp.lock()) {
                observer->update(this);
            }
        }
    }

    // 状态接口
    virtual std::string getState() const = 0;
    virtual void setState(const std::string& state) = 0;
};

// 具体的主题:气象站
class WeatherStation : public Subject {
private:
    std::string weatherState;

public:
    std::string getState() const override {
        return weatherState;
    }

    void setState(const std::string& state) override {
        weatherState = state;
        notify();       // 状态一变,立刻通知所有人!
    }
};

// 具体的观察者
// 观察者1:当前天气显示器
class CurrentDisplay : public Observer {
public:
    void update(Subject* subject) override {
        std::cout << "【当前天气】:" << subject->getState() << std::endl;
    }
};

// 观察者2:天气预报显示器
class ForecastDisplay : public Observer {
public:
    void update(Subject* subject) override {
        std::string state = subject->getState();
        std::string forecast = (state == "晴天") ? "明天也是晴天!" : "明天可能下雨,记得带伞";
        std::cout << "【天气预报】:" << forecast << std::endl;
    }
};

int main() {
    // 创建气象站
    std::shared_ptr<Subject> station = std::make_unique<WeatherStation>();

    // 创建观察者
    std::shared_ptr<Observer> current = std::make_unique<CurrentDisplay>();
    std::shared_ptr<Observer> forecast = std::make_unique<ForecastDisplay>();

    // 订阅
    station->attach(current);
    station->attach(forecast);

    std::cout << "=== 气象站更新:晴天 ===" << std::endl;
    station->setState("晴天");

    // 小明取消了天气预报的订阅
    station->detach(forecast);

    std::cout << "=== 气象站更新:雨天 ===" << std::endl;
    station->setState("雨天");

    return 0;
}

// 这样的设计使得观察者和主题之间的耦合度非常低,观察者可以自由地订阅和取消订阅,而主题只需要负责通知观察者即可。观察者模式非常适合实现事件驱动的系统,比如 GUI 应用、消息系统等。当主题的状态发生变化时,所有依赖它的观察者都会自动收到通知并更新自己,这极大地提高了系统的灵活性和可维护性。

运行结果

1
2
3
4
5
6
=== 气象站更新:晴天 ===
【当前天气】: 晴天
【天气预报】: 明天也是晴天!

=== 气象站更新:雨天 ===
【当前天气】: 雨天


4.2 策略模式(Strategy):动态切换算法

它解决了什么问题?

如果你有多个算法,你想在运行的时候动态地切换它们,而且让算法的变化独立于使用算法的客户。

比如:

  • 排序,有时候用快排,有时候用冒泡

  • 支付,有时候用微信,有时候用支付宝

  • 促销,普通用户没折扣,VIP 打 9 折,超级 VIP 打 8 折

如果不用策略模式,你可能会写一堆if-else:

1
2
3
4
5
6
7
if (userType == "normal") {
    price = p;
} else if (userType == "vip") {
    price = p * 0.9;
} else if (userType == "svip") {
    price = p * 0.8;
}

加新的折扣类型,又要改这个函数,太麻烦了。

策略模式就是把每个算法,封装成独立的类,然后你想换哪个就换哪个。

UML 类图

C++ 代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include <iostream>
#include <memory>

// 策略接口
// 所有的折扣策略都要实现这个接口
class DiscountStrategy {
public:
    virtual ~DiscountStrategy() = default;
    virtual double calculate(double originalPrice) = 0;
};

// 具体的策略
// 普通用户:无折扣
class NormalDiscount : public DiscountStrategy {
public:
    double calculate(double originalPrice) override {
        std::cout << "普通用户,无折扣" << std::endl;
        return originalPrice;
    }
};

// VIP用户:9折
class VipDiscount : public DiscountStrategy {
public:
    double calculate(double originalPrice) override {
        std::cout << "VIP用户,打9折" << std::endl;
        return 0.9 * originalPrice;
    }
};

// 超级VIP:8折
class SvipDiscount : public DiscountStrategy {
public:
    double calculate(double originalPrice) override {
        std::cout << "超级VIP用户,打8折" << std::endl;
        return 0.8 * originalPrice;
    }
};

// 上下文:使用策略的类
class PriceCalculator {
private:
    std::unique_ptr<DiscountStrategy> strategy;     // 当前用的策略

public:
    // 构造的时候传入策略
    PriceCalculator(std::unique_ptr<DiscountStrategy> s) : strategy(std::move(s)) {}

    // 动态切换策略
    void setStrategy(std::unique_ptr<DiscountStrategy> s) {
        strategy = std::move(s);
    }

    // 计算最终价格
    double getFinalPrice(double originalPrice) {
        return strategy->calculate(originalPrice);
    }
};

int main() {
    // 一开始,默认普通用户
    PriceCalculator calculator(std::make_unique<NormalDiscount>());
    double normalPrice = calculator.getFinalPrice(100);
    std::cout << "原价100元,最终价格:" << normalPrice << std::endl;

    // 用户升级成VIP了,切换策略
    calculator.setStrategy(std::make_unique<VipDiscount>());
    double vipPrice = calculator.getFinalPrice(100);
    std::cout << "原价100元,最终价格:" << vipPrice << std::endl;

    // 用户升级成超级VIP了,切换策略
    calculator.setStrategy(std::make_unique<SvipDiscount>());
    double svipPrice = calculator.getFinalPrice(100);
    std::cout << "原价100元,最终价格:" << svipPrice << std::endl;

    // 以后要加新的折扣?比如节日折扣?
    // 加个HolidayDiscount类就行了!完全不用改Calculator的代码!

    return 0;
}

运行结果

1
2
3
4
5
6
普通用户,无折扣
原价100元,最终价格: 100
VIP用户,打9折
原价100元,最终价格: 90
超级VIP,打8折
原价100元,最终价格: 80


五、怎么学好设计模式?

5.1 不要死记硬背

很多人学设计模式,就是把 23 个模式的定义背下来,这完全没用!

你要记住的是:

  • 这个模式解决了什么问题?

  • 什么时候该用它?

  • 什么时候不该用它?

5.2 先写坏代码,再重构

最好的学习方式,不是上来就用设计模式。

  1. 先把功能写出来,哪怕代码很烂,一堆if-else

  2. 当你发现代码越来越难维护的时候,再回头看,哪个模式能解决你的问题

  3. 重构!

这样你才能真正理解,这个模式解决了什么痛点。

5.3 不要过度设计

设计模式不是银弹!不要为了用模式而用模式。

  • 如果你的项目很小,就几个类,别搞什么工厂、策略了,直接写最简单的代码就行

  • KISS 原则:Keep It Simple, Stupid! 简单比复杂更重要

5.4 从常用的模式开始学

不用一上来就把 23 个都学完,先把这几个最常用的吃透:

  1. 单例(最简单,必学)

  2. 工厂(解耦创建,必学)

  3. 观察者(事件系统,必学)

  4. 策略(算法切换,必学)

  5. 适配器(兼容旧代码,常用)

  6. 装饰器(动态加功能,常用)

剩下的,用到的时候再学也不迟。


总结

设计模式,本质上就是一套 “让代码变好” 的经验总结。它不是什么高深的黑魔法,而是帮你写出更优雅、更易维护的代码的工具。

作为小白,你不用急着一口吃成胖子,慢慢来:

  1. 先理解 SOLID 原则

  2. 逐个吃透常用的模式,动手敲代码

  3. 在实际项目中,遇到问题了,想想哪个模式能帮你

相信我,当你真正理解了设计模式,你会发现,写代码原来是一件这么爽的事!


本文章基于现代 C++(C++11 及以上)编写,所有代码均可直接编译运行。