C++ 基于多设计模式下的同步&异步⽇志系统-1准备工作

一.项目介绍

项⽬介绍
本项⽬主要实现⼀个⽇志系统, 其主要⽀持以下功能:
• ⽀持多级别⽇志消息
• ⽀持同步⽇志和异步⽇志
• ⽀持可靠写⼊⽇志到控制台、⽂件以及滚动⽂件中
• ⽀持多线程程序并发写⽇志
• ⽀持扩展不同的⽇志落地⽬标地

二.日志系统的三种实现方式

实现方式原理简述优点缺点适用场景
1. 控制台输出 (printf/std::cout)直接在控制台输出日志信息,不进行落地文件记录简单、直观、便于开发调试无法记录历史日志、对线上调试不适用本地开发调试、单线程程序
2. 同步写日志在当前业务线程中执行日志格式化 + 写入文件操作,每条日志调用都同步 write()实现简单、数据可靠每条日志都阻塞主流程,尤其在高并发下 write IO 成为性能瓶颈简单后端系统、低并发写日志场景
3. 异步写日志主线程仅负责将日志写入缓冲区,由专门线程写日志到文件高性能、非阻塞、不影响业务流程,适合高并发实现复杂、涉及线程、锁、双缓冲,落地时间略有延迟高性能服务、后台系统、分布式

三.相关技术知识补充

1 不定参宏函数

#include <iostream>
#include <cstdarg>
#define LOG(fmt, ...) printf("[%s:%d] " fmt "\n", __FILE__, __LINE__,##__VA_ARGS__)
int main()
{
    LOG("%s-%s", "hello", "wws);
    return 0;
}

之前rpc项目介绍过。

解释 ##__VA_ARGS__

__VA_ARGS__ 是 C 语言宏的可变参数,它允许宏接受不定数量的参数。
## 用于处理 "参数为空" 的情况,它的作用是:
如果 __VA_ARGS__ 为空,就去掉前面的 ' , ',防止格式错误。
如果 __VA_ARGS__ 有内容,它会正常展开。

2.C⻛格不定参函数

#include <iostream>
#include <cstdarg>
void printNum(int n, ...)
{
    va_list al;
    va_start(al, n); // 让al指向n参数之后的第⼀个可变参数
    for (int i = 0; i < n; i++)
    {
        int num = va_arg(al, int); // 从可变参数中取出⼀个整形参数
        std::cout << num << std::endl;
    }
    va_end(al); // 清空可变参数列表--其实是将al置空
}
int main()
{
    printNum(3, 11, 22, 33);
    printNum(5, 44, 55, 66, 77, 88);
    return 0;
}

printNum(int n, ...)的作用就是打印n个整型

1. va_list al;

定义一个变参处理变量 va_list 它是 C 语言提供的一个宏(实际上是一个结构体指针类型),专门用来处理 ... 这些变长参数。你可以把它理解为:一个“变参读取器”指针。

2.va_start(al, n);

初始化变参指针(定位起点)它告诉 al“变长参数”是从 n之后开始的,且后面参数的个数为n。(C 语言没有反射或参数数量的机制,编译器也不会告诉你 ... 有几个参数。必须通过最后一个确定参数的地址来推断后面变参的起始地址,这就是 va_start 的原理)

3.va_arg(al, int);

使用 va_arg() 逐个读取参数(大小为第二个类型的大小)。从 al 指向的地方读取一个 int 类型的值,并且把 al 自动向后移动。

4. va_end(ap);

清理资源(让 ap 无效)清空指针

#include <iostream>
#include <cstdarg>
void myprintf(const char *fmt, ...)
{
    // int vasprintf(char **strp, const char *fmt, va_list ap);
    char *res;
    va_list al;
    va_start(al, fmt);
    int len = vasprintf(&res, fmt, al);
    va_end(al);
    std::cout << res << std::endl;
    free(res);
}
int main()
{
    myprintf("%s-%d", "⼩明", 18);
    return 0;
}

myprintf 的函数,能像 printf 一样,接收格式字符串和多个参数,把结果 格式化成字符串,并通过 std::cout 打印出来。

简单来说 类比printf,fmt就相当于"%d%s%c" ap里面按顺序保存的就是 整型 字符串 字符类型的参数,

int vasprintf(char **strp, const char *fmt, va_list ap);

参数说明:

  1. strp (char **):

    • 这是一个指向字符指针的指针。vasprintf 会将格式化后的字符串存储在由 strp 指向的内存中。内存是动态分配的,因此调用者不需要事先为这个字符串分配空间,调用完成后需要使用 free 函数释放这块内存。
  2. fmt (const char *):

    • 格式化字符串,定义了如何格式化可变参数(与 printf 中使用的格式字符串相同)。
  3. ap (va_list):

    • 一个 va_list 类型的对象,它保存了传递给函数的可变参数列表。通常使用 va_start 宏来初始化这个 va_list 对象。

返回值:

  • 如果成功,返回格式化后的字符串的长度(不包括终止的空字符)。
  • 如果出错,返回 -1,并且不会分配内存。

vasprintf 动态分配了内存来存储格式化后的字符串,调用者需要使用 free 来释放这块内存,避免内存泄漏。

fmt 是格式字符串:"%s-%d"

va_start(al, fmt); 告诉 al:从参数 fmt 后面的地方开始读取变参("小明", 18)。

vasprintf(&res, fmt, al);

这个函数做了三件事:

1.根据 fmt 和 al,拼出格式化后的字符串

2.自动调用 malloc 分配内存,存放结果字符串;

3.把 res 设为这个字符串的地址。

free(res);

因为 vasprintf 分配了堆内存,你必须用 free 手动释放,否则会内存泄漏。

3.C++⻛格不定参函数


#include <iostream>
void xprintf()
{
    std::cout << std::endl;
}
template <typename T, typename... Args>
void xprintf(const T &value, Args &&...args)
{
    std::cout << value << " ";
    if ((sizeof...(args)) > 0)
    {
        xprintf(std::forward<Args>(args)...);
    }
    else
    {
        xprintf();
    }
}
int main()
{
    xprintf("wws");
    xprintf("wws", 666);
    xprintf("wws", "0721", 666);
    return 0;
}

参数说明:

  • T:当前要处理的第一个参数

  • Args...:剩下的变长参数包

行为流程:

  1. 打印当前的 value

  2. 如果还有参数(sizeof...(args) > 0)就递归调用 xprintf(...)

  3. 否则,调用 xprintf()(终点),输出一个换行

  • ... 在前面:定义一个 参数包

  • ... 在后面:展开一个 参数包

(std::forward<Args>(args)...) 完美转发剩余的参数,右值传递完还是右值,左值还是左值。

为什么要写xprintf()参数为空的特化函数?

模板会一直展开直到参数为空

模板的递归展开并不会因为你进入 else 分支而立即停止递归。递归停止是通过“没有更多参数”来控制的。关键点是 你在调用 xprintf() 时,会不断把剩余的参数传递给下一个递归调用,直到参数包为空。

所以执行else后,并不会结束,还会继续递归直到参数包为空,所以必须写参数包为空的特化函数。

4.C++ 文件流fstream

C++ 标准库提供了三个主要的文件流类,都定义在 <fstream> 头文件中:

功能继承关系
ifstream输入文件流(只读)继承自 istream
ofstream输出文件流(只写)继承自 ostream
fstream输入/输出文件流(读写)继承自 iostream

ifstream 默认读文件方式打开:

  • tellg(): 获取当前读取位置

  • seekg(): 设置读取位置

  • read(): 读取二进制数据

ofstream 默认写文件方式打开:

  • tellp(): 获取当前写入位置

  • seekp(): 设置写入位置

  • write(): 写入二进制数据

fstream 可读可写 同时包含read write

open()打开文件 ifstream打开文件时会默认加上std::ios::in 同理ofstream默认加out

模式标志描述
std::ios::in以读取方式打开文件(ifstream 默认包含)
std::ios::out以写入方式打开文件(ofstream 默认包含,会清空现有内容)
std::ios::app追加模式,所有写入都添加到文件末尾
std::ios::ate打开时定位到文件末尾(但后续写入位置可改变)
std::ios::trunc如果文件已存在,先清空内容(ofstream 默认包含)
std::ios::binary二进制模式(避免文本转换)

ofstream 相关函数:

1. tellp() - 获取当前写入位置

功能:返回当前写入指针的位置(类型为 std::streampos

std::ofstream out("test.txt");
out << "Hello";
std::streampos pos = out.tellp();  // pos = 5(5个字符后)

2. seekp() - 设置写入位置

功能:移动写入指针到指定位置

两种重载形式

// 绝对定位
seekp(std::streampos pos);

// 相对定位  从dir位置向后偏移offset大小(为正) 负数就向前偏移
seekp(std::streamoff offset, std::ios::seekdir dir);

定位基准(seekdir

  • std::ios::beg:从文件开头计算

  • std::ios::cur:从当前位置计算

  • std::ios::end:从文件末尾计算

读取文件所有内容的操作:

不建议用size_t接收文件大小,一方面失败会返回-1,二可能会越界超出范围。

std::ifstream ifs("./logfile/test.log",std::ios::binary);//binary二进制读取避免文本模式下的换行符转换(如 \r\n 转 \n),确保偏移计算准确
    if(ifs.is_open()==false){std::cout<<"读文件打开失败\n";return -1;}
    ifs.seekg(0,std::ios::end);//从end位置向后偏移0字节 相当于读位置直接到最后一个字节
    size_t fsize = ifs.tellg();//获取当前读位置相较于起始位置的偏移量 (此时就是文件的大小)
    ifs.seekg(0,std::ios::beg);//重新把读位置放到开头 为后面读取做准备

计算文件大小也可以用C++17中的std::filesystem::file_size

用uintmax_t 最大无符号整型接收,失败不会返回-1,所有可以用无符号

#include <filesystem>
namespace fs = std::filesystem;

uintmax_t size = fs::file_size("large.bin");  // 返回类型为 uintmax_t

关键优势(相比 tellg()

特性file_sizetellg()
返回类型uintmax_t (平台最大无符号整型)std::streampos (实现定义)
最大支持文件大小理论支持到 16EB (2^64字节)依赖实现(32位系统通常4GB限制)
错误处理抛出异常或返回错误码需手动检查-1
执行效率直接查询文件系统元数据需要打开文件并移动指针
多平台一致性高度统一各平台实现差异大

ifstream 相关函数:

1. write() - 写入二进制数据

功能:将内存中的二进制数据写入文件

std::ostream& write(const char* buffer, std::streamsize count);

参数

  • buffer:指向要写入的数据的指针(通常需要类型转换)

  • count:要写入的字节数

    struct Person {
        int id;
        char name[20];
        double salary;
    };
    
    Person p = {101, "Alice", 8500.50};
    
    std::ofstream out("person.dat", std::ios::binary);
    out.write(reinterpret_cast<char*>(&p), sizeof(Person));

2. read() - 读取二进制数据

功能:从文件读取二进制数据到内存

std::istream& read(char* buffer, std::streamsize count);

参数

  • buffer:存储数据的缓冲区的地址 

  • count:要读取的字节数

    std::ifstream in("data.bin", std::ios::binary);
    char buffer[100];
    
    in.read(buffer, sizeof(buffer));
    std::streamsize bytesRead = in.gcount(); // 获取实际读取的字节数

二进制文件必须显式指定 std::ios::binary 模式

一、文本模式 vs 二进制模式的核心区别

当不指定 binary 模式时,文件流默认处于 文本模式,在这种模式下:

  1. 换行符转换(主要区别):

    • 写入时:\n → \r\n(Windows系统)

    • 读取时:\r\n → \n

    • 这种转换会导致二进制数据被破坏

在Windows系统会发生

  1. 遇到字节 0x0A\n)时,系统自动插入 0x0D\r

  2. 实际写入文件的内容变为:0x0D 0x0A 0x0B 0x0C 0x0D

  3. 完全破坏了原始数据

系统文本模式换行符二进制模式
Windows\r\n ↔ \n无转换
Linux/Mac无转换无转换
旧版Mac\r ↔ \n无转换

5.线程间同步机制condition_variable

std::condition_variable 是 C++11 引入的 线程间同步机制,主要用于:

✅ 在多线程中 一个线程等待某个条件成立,另一个线程通知它。

不能单独使用必须配合 std::mutex 进行。

mutex 控制资源访问

condition_variable 控制“时机”执行

功能描述
线程等待(阻塞)线程可以调用 wait() 在条件变量上等待,直到被其他线程唤醒。
线程唤醒(通知)其他线程可以调用 notify_one()notify_all() 唤醒等待的线程。
避免死循环等待条件变量配合互斥锁使用,可以避免主动轮询(busy waiting)带来的资源浪费。

三个核心函数

1.wait()  让线程阻塞等待一个条件

原型一(基础版):

void wait(std::unique_lock<std::mutex>& lock);

原型二(带谓词):

template <class Predicate>
void wait(std::unique_lock<std::mutex>& lock, Predicate pred);

功能:

  • 阻塞当前线程,直到被唤醒;

  • 必须传入锁std::unique_lock),线程会在内部释放锁、等待、被唤醒后再重新加锁;

  • 带谓词版本会自动检查条件是否成立,成立继续往下运行,不成立阻塞住 直到成立。

    std::mutex mtx;
    std::condition_variable cv;
    bool ready = false;
    
    void worker() {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [] { return ready; });  // 等待 ready == true
        std::cout << "线程被唤醒,ready == true\n";
    }
    

2. notify_one()  唤醒一个等待线程

void notify_one();

功能:

  • 唤醒一个被 wait() 阻塞的线程

  • 如果没有线程在等,它什么也不做;

  • 多个线程 wait() 时,它只叫醒一个

  • 唤醒一个被 wait() 阻塞的线程

  • 如果没有线程在等,它什么也不做;

  • 多个线程 wait() 时,它只叫醒一个

3. notify_all()  唤醒所有等待线程

void notify_all();

功能:

  • 唤醒所有wait() 上等待的线程;

  • 被唤醒的线程会一个一个重新加锁,然后判断谓词是否成立。

四.设计模式

1.六大设计原则

1.单一职责原则(SRP)

定义:一个类只负责一项职责。

应用:

  • Logger 只负责组织和发起日志输出。

  • Formatter 专注于格式化日志内容。

  • Sink 专注于日志“落地”(文件/控制台等输出方式)。

  • LogMsg 专注于日志数据结构封装。


🔓 2. 开闭原则(OCP)

定义:对扩展开放,对修改关闭。

应用:

  • 增加新的日志输出格式、日志落地方式(如新增 TCP 日志输出)→ 新增类即可,无需改动原逻辑。

  • 格式化模块通过解析 %d %m %t 等 pattern 字符串,支持灵活扩展。


🔁 3. 里氏替换原则(LSP)

定义:子类对象可以替代父类对象使用。

应用:

  • 所有日志输出类继承自抽象类 LogSink,只要实现 log() 方法,就能无缝替换。

  • SyncLogger / AsyncLogger 都继承自 Logger,任何需要 Logger 的地方都可以使用这两个实现。


🔌 4. 依赖倒置原则(DIP)

定义:高层模块不应该依赖底层模块,二者都应该依赖抽象。

应用:

  • 所有 Sink 都通过 LogSink::ptr 操作,具体使用的是哪个子类并不关心。

  • 日志器的创建通过 Builder 构建,调用方不直接依赖 Logger 实现类。


🧼 5. 接口隔离原则(ISP)

定义:类不应依赖它不使用的方法。

应用:

  • Formatter::format() 只依赖 LogMsg 数据,不暴露额外无关的操作。

  • Logger 类的 debug/info/warn/... 分开封装,调用者按需使用。


🧍 6. 迪米特法则(LoD)

定义:只与直接朋友通信,降低耦合。

应用:

  • 日志器通过 Logger::Builder 封装所有配置细节,调用者无需了解 Sink/Formatter 等底层实现。

  • 管理器 loggerManager 提供统一接口 getLogger(),外部无需知道 Logger 的创建细节。


总结一句话:

“用抽象构建框架,用实现扩展细节”,整个日志系统正是依据这一原则,通过设计模式把每个模块解耦,提升了系统的灵活性与可扩展性。

2.单例模式

单例模式是一种常见的设计模式,它保证一个类只有一个实例,并提供一个全局访问点。单例模式有两种实现方式 饿汉模式和懒汉模式

饿汉模式

程序启动时就会创建⼀个唯⼀的实例对象。 因为单例对象已经确定, 所以⽐较适⽤于多
线程环境中, 多线程获取单例对象不需要加锁, 可以有效的避免资源竞争, 提⾼性能。

//1.饿汉模式
class Singleton
{
private:
    static Singleton _eton;//在类内进行声明
    Singleton()// 私有构造函数
    :_data(66)
    {
        std::cout<<"单例对象构造"<<std::endl;
    }
    ~Singleton() {} // 私有析构函数
    Singleton (const Singleton&)=delete;//禁止拷贝
    Singleton&operator=(const Singleton&)=delete;//禁止赋值
private:
    int _data;
public:
    static Singleton& getInstance()
    {
        return _eton;
    }
};
Singleton Singleton:: _eton;//类外定义

1.构造析构私有 拷贝赋值函数禁止delete且私有

2.类内声明 静态成员变量 类外定义(程序运行时自动实例化)

3.类中提供静态函数(不需要类对象就能调用),用来获取单例对象。

优点

  • 线程安全:由于单例对象在程序启动时就已经创建,多个线程在调用 getInstance() 时无需加锁,可以避免资源竞争,因此性能较高。

  • 简单:代码结构简单,容易理解和实现。

缺点

  • 提前创建:单例对象会在程序启动时就创建,即使在程序运行过程中并不需要这个实例,也会被创建,这可能导致不必要的资源浪费。

  • 不可延迟加载:如果创建单例对象的过程非常复杂或资源消耗很大,程序启动时就会受到影响。

适用场景

  • 适合在程序启动时就需要加载的资源,例如配置管理、日志系统等。

  • 适用于实例的创建比较轻量,或者实例的创建和销毁不会占用太多资源的场景。

懒汉模式

第⼀次使⽤要使⽤单例对象的时候创建实例对象。如果单例对象构造特别耗时或者耗费济
源(加载插件、加载⽹络资源等), 可以选择懒汉模式, 在第⼀次使⽤的时候才创建对象。

//2.懒汉模式
class Singleton
{
private:
    Singleton()// 私有构造函数
    :_data(66)
    {
        std::cout<<"单例对象构造"<<std::endl;
    }
    ~Singleton() {} // 私有析构函数
    Singleton (const Singleton&)=delete;//禁止拷贝
    Singleton&operator=(const Singleton&)=delete;//禁止赋值
private:
    int _data;
public:
    static Singleton& getInstance()
    {
        static Singleton _eton;//只有第一次调用时创建实例(C++11 此时线程安全不需要加锁)
        return _eton;
    }
};

1.构造析构私有 拷贝赋值函数禁止delete且私有

2.在获取单例对象时getInstance()内部创建单例对象(static对象只会初始化一次)

优点

  • 延迟创建:单例对象只有在真正需要时才会被创建,避免了不必要的资源浪费,适用于实例化过程耗时或消耗资源的情况。

  • 线程安全使用 C++11 的 static 关键字保证静态局部变量在多线程环境下的安全初始化。

缺点

  • 延迟加载开销:虽然避免了程序启动时的资源消耗,但在首次调用 getInstance() 时,会有一定的延迟开销。

  • 复杂度较高:相比饿汉模式,懒汉模式的实现稍微复杂一些,尤其是早期版本的 C++,静态局部变量的线程安全性没有保证,需要额外的锁机制。

适用场景

  • 适合实例化开销较大、资源消耗较多的单例对象,或者对象的创建是延迟的、条件不固定的情况。

特性饿汉模式懒汉模式
实例化时机程序启动时即创建实例第一次调用 getInstance() 时创建实例
线程安全默认线程安全静态局部变量保证线程安全(C++11后)
内存消耗启动时即创建,可能浪费资源只有在首次访问时才创建,节省内存资源
性能更高性能,无锁定和延迟初次调用有延迟,可能有少许性能开销
实现复杂度简单易实现稍复杂,涉及线程安全和延迟加载
适用场景启动时必须加载的对象,资源轻量对象创建耗时或资源消耗较大的情况

3.工厂模式

1.简单工厂模式

通过一个统一的工厂类,根据传入的参数判断创建哪种产品(对象)。

所有产品类的创建逻辑都集中在一个工厂类中。

客户端
  ↓
SimpleFactory::create("苹果") or "香蕉"
  ↓
返回具体产品(Apple / Banana)

只有一个工厂,根据传入类型的不同,来生产不同的对象。

class Fruit {
public:
    virtual void show() = 0;
};

class Apple : public Fruit {
public:
    void show() override { std::cout << "我是苹果\n"; }
};

class Banana : public Fruit {
public:
    void show() override { std::cout << "我是香蕉\n"; }
};

class FruitFactory {
public:
    static std::shared_ptr<Fruit> createFruit(const std::string &type) {
        if (type == "apple") return std::make_shared<Apple>();
        if (type == "banana") return std::make_shared<Banana>();
        return nullptr;
    }
};

✅ 优点:

  • 简单易懂、实现成本低。

  • 客户端不需要知道具体产品类名,只需要告诉工厂“我要什么”。

❌ 缺点:

  • 违反开闭原则:添加新产品必须修改工厂代码。

  • 工厂类过于臃肿,职责过重,易造成维护困难。

✅ 适用场景:

  • 产品种类较少,变动不频繁的小项目或初期开发阶段。

方法二:模板函数

template<typename T, typename... Args>
static std::shared_ptr<T> create(Args&&... args)
{
    return std::make_shared<T>(std::forward<Args>(args)...);
}

符合开闭原则

2.工厂方法模式

每个产品类对应一个具体工厂类。

抽象出一个工厂接口,具体工厂负责创建对应的产品。

客户端只需使用对应的工厂,不再传入类型参数。

客户端
  ↓
AppleFactory::create()        BananaFactory::create()
  ↓                             ↓
返回 Apple                    返回 Banana

有多个工厂,一个子类就对应一个工厂。

class Fruit {
public:
    virtual void show() = 0;
};

class Apple : public Fruit {
public:
    void show() override { std::cout << "我是苹果\n"; }
};

class Banana : public Fruit {
public:
    void show() override { std::cout << "我是香蕉\n"; }
};

class FruitFactory {
public:
    virtual std::shared_ptr<Fruit> createFruit() = 0;
};

class AppleFactory : public FruitFactory {
public:
    std::shared_ptr<Fruit> createFruit() override {
        return std::make_shared<Apple>();
    }
};

class BananaFactory : public FruitFactory {
public:
    std::shared_ptr<Fruit> createFruit() override {
        return std::make_shared<Banana>();
    }
};

✅ 优点:

  • 遵循开闭原则:新增产品只需新增产品类和工厂类,无需修改现有代码。

  • 更加符合“职责单一”的设计原则。

❌ 缺点:

  • 每新增一个产品都要新增一个工厂类,类数量增多。

  • 不适合产品种类太多的场景,维护成本较高。

✅ 适用场景:

  • 产品变化频繁,且对扩展性有要求的中大型项目。

3.抽象工厂模式

不再是创建“单一”产品,而是创建产品族(多个功能相关的产品对象)。

定义一组工厂接口,每个工厂可以创建多个类型的产品。

有多种物品,水果 动物... 里面还可以细分苹果 香蕉,狗 羊

每一种物品对应一个工厂,每个工厂中有具体对象生成函数

所有工厂都继承于一个抽象工厂。

抽象工厂(AbstractFactory)
       ↓
  ------------------------
  ↓                      ↓
水果工厂(FruitFactory)   动物工厂(AnimalFactory)
  ↓                      ↓
createApple()            createDog()
createBanana()           createSheep()
#include <iostream>
#include <memory>
#include <string>

// ==== 抽象产品 ====
class Fruit {
public:
    virtual void show() = 0;
    virtual ~Fruit() = default;
};

class Animal {
public:
    virtual void voice() = 0;
    virtual ~Animal() = default;
};

// ==== 具体产品 ====
class Apple : public Fruit {
public:
    void show() override {
        std::cout << "我是苹果🍎" << std::endl;
    }
};

class Banana : public Fruit {
public:
    void show() override {
        std::cout << "我是香蕉🍌" << std::endl;
    }
};

class Dog : public Animal {
public:
    void voice() override {
        std::cout << "汪汪汪🐶" << std::endl;
    }
};

class Sheep : public Animal {
public:
    void voice() override {
        std::cout << "咩咩咩🐑" << std::endl;
    }
};

// ==== 抽象工厂接口 ====
class AbstractFactory {
public:
    virtual ~AbstractFactory() = default;
};

// ==== 水果工厂接口 ====
class FruitFactory : public AbstractFactory {
public:
    virtual std::shared_ptr<Fruit> createApple() = 0;
    virtual std::shared_ptr<Fruit> createBanana() = 0;
};

// ==== 动物工厂接口 ====
class AnimalFactory : public AbstractFactory {
public:
    virtual std::shared_ptr<Animal> createDog() = 0;
    virtual std::shared_ptr<Animal> createSheep() = 0;
};

// ==== 水果工厂实现 ====
class ConcreteFruitFactory : public FruitFactory {
public:
    std::shared_ptr<Fruit> createApple() override {
        return std::make_shared<Apple>();
    }
    std::shared_ptr<Fruit> createBanana() override {
        return std::make_shared<Banana>();
    }
};

// ==== 动物工厂实现 ====
class ConcreteAnimalFactory : public AnimalFactory {
public:
    std::shared_ptr<Animal> createDog() override {
        return std::make_shared<Dog>();
    }
    std::shared_ptr<Animal> createSheep() override {
        return std::make_shared<Sheep>();
    }
};

// ==== 工厂选择器 ====
class FactorySelector {
public:
    enum class Type { FRUIT, ANIMAL };

    static std::shared_ptr<AbstractFactory> getFactory(Type type) {
        if (type == Type::FRUIT) {
            return std::make_shared<ConcreteFruitFactory>();
        } else {
            return std::make_shared<ConcreteAnimalFactory>();
        }
    }
};

// ==== 使用示例 ====
int main() {
    // 选择水果工厂
    auto fruitFactory = std::dynamic_pointer_cast<FruitFactory>(
        FactorySelector::getFactory(FactorySelector::Type::FRUIT));
    
    auto apple = fruitFactory->createApple();
    apple->show();

    auto banana = fruitFactory->createBanana();
    banana->show();

    // 选择动物工厂
    auto animalFactory = std::dynamic_pointer_cast<AnimalFactory>(
        FactorySelector::getFactory(FactorySelector::Type::ANIMAL));

    auto dog = animalFactory->createDog();
    dog->voice();

    auto sheep = animalFactory->createSheep();
    sheep->voice();

    return 0;
}

✅ 优点:

  • 遵循开闭原则,支持产品族的统一创建

  • 便于对产品进行分组管理,提高模块间协作性。

❌ 缺点:

  • 系统复杂度提高,类之间的依赖关系增多。

  • 如果要添加新产品(而不是产品族),修改成本大(会破坏工厂接口)。

✅ 适用场景:

  • 一个系统需要成组创建多个互相依赖的对象

  • 比如 GUI 库中,不同操作系统(Windows/Mac/Linux)下的按钮、菜单、文本框需要成套配合。

4.建造者模式

建造者模式是⼀种创建型设计模式, 使⽤多个简单的对象⼀步⼀步构建成⼀个复杂的对象,能够将⼀个复杂的对象的构建与它的表⽰分离,提供⼀种创建对象的最佳⽅式。主要⽤于解决对象的构建过于复杂的问题。
建造者模式主要基于四个核⼼类实现:
• 抽象产品类:定义复杂对象结构、属性和接口
• 具体产品类:⼀个具体的产品对象类
• 抽象Builder类:创建⼀个产品对象所需的各个部件的抽象接⼝
• 具体产品的Builder类:实现抽象接⼝,构建各个部件
• 指挥者Director类:统⼀组建过程,提供给调⽤者使⽤,通过指挥者按顺序来构造产品

抽象产品类:需要设置的属性

具体产品类:不同产品设置的属性不同

抽象Builder类:设置对应的属性的接口

具体产品的Builder类:具体怎么设置产品属性 (实现接口)

指挥者Director类:设置属性的先后顺序

// 1. 抽象产品类
class Computer {
    std::string _board, _display, _os;
    void setBoard(...); void setDisplay(...); virtual void setOs() = 0;
};

// 2. 具体产品类
class MacBook : public Computer {
    void setOs() override { _os = "Mac OS X"; }
};

// 3. 抽象建造者
class Builder {
    virtual void buildBoard(...) = 0;
    virtual void buildDisplay(...) = 0;
    virtual void buildOs() = 0;
    virtual Computer::ptr build() = 0;
};

// 4. 具体建造者
class MacBookBuilder : public Builder {
    Computer::ptr _computer;
    void buildBoard(...) override { _computer->setBoard(...); }
    ...
};

// 5. 指挥者
class Director {
    Builder::ptr _builder;
    void construct(...) {
        _builder->buildBoard(...);
        _builder->buildDisplay(...);
        _builder->buildOs();
    }
};
int main()
{
    Builder* buidler = new MackBookBuilder();
    std::unique_ptr<Director> pd(new Director(buidler));
    pd->construct("英特尔主板", "VOC显⽰器");
    Computer::ptr computer = buidler->build();
    std::cout << computer->toString();
    return 0;
}
角色类名职责描述
抽象产品类Computer定义电脑的组成部分(主板、显示器、系统)以及接口
具体产品类MacBook继承 Computer,实现操作系统的设定
抽象建造者Builder定义构建各部分(主板、显示器、OS)和最终组装的接口
具体建造者MacBookBuilder实现构建过程,封装构建细节,返回构造结果
指挥者(Director)Director控制建造流程,调用建造者接口完成构造

5.代理模式


代理模式指代理控制对其他对象的访问, 也就是代理对象控制对原对象的引⽤。在某些情况下,⼀个对象不适合或者不能直接被引⽤访问,⽽代理对象可以在客⼾端和⽬标对象之间起到中介的作⽤。

代理模式的结构包括⼀个是真正的你要访问的对象(⽬标类)、⼀个是代理对象。⽬标对象与代理对象实现同⼀个接⼝,先访问代理类再通过代理类访问⽬标对象。代理模式分为静态代理、动态代理:
静态代理指的是,在编译时就已经确定好了代理类和被代理类的关系。也就是说,在编译时就已经确定了代理类要代理的是哪个被代理类。
• 动态代理指的是,在运⾏时才动态⽣成代理类,并将其与被代理类绑定。这意味着,在运⾏时才能确定代理类要代理的是哪个被代理类。

实现了一个“租房场景”的静态代理模式

  • 房东(Landlord)被代理对象(目标对象)

  • 中介(Intermediary)代理对象

  • 通过中介代理类来控制、增强对房东租房功能的访问

#include <iostream>
#include <string>

// 抽象租房接口
class RentHouse {
public:
    virtual void rentHouse() = 0;
    virtual ~RentHouse() = default;
};

// 房东类:目标对象(实际提供租房服务)
class Landlord : public RentHouse {
public:
    void rentHouse() override {
        std::cout << "房东:将房子租出去\n";
    }
};

// 中介代理类:代理对象,封装对房东的访问并增强功能
class Intermediary : public RentHouse {
public:
    void rentHouse() override {
        std::cout << "中介:发布招租启示\n";
        std::cout << "中介:带人看房\n";
        _landlord.rentHouse();  // 委托给房东完成真正的租房
        std::cout << "中介:租后负责维修服务\n";
    }

private:
    Landlord _landlord;  // 中介内部持有真实房东对象
};

// 客户端调用
int main() {
    Intermediary intermediary;
    intermediary.rentHouse();  // 客户通过代理租房
    return 0;
}
→ 中介的 rentHouse()
    → 发布招租启事
    → 带人看房
    → 调用房东的 rentHouse()(真正租出)
    → 负责租后维修

房东只做了“租出去”这一件事,其他琐事都由中介代理处理,体现了代理模式“控制访问 + 功能增强”的特性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值