一.项目介绍
项⽬介绍
本项⽬主要实现⼀个⽇志系统, 其主要⽀持以下功能:
• ⽀持多级别⽇志消息
• ⽀持同步⽇志和异步⽇志
• ⽀持可靠写⼊⽇志到控制台、⽂件以及滚动⽂件中
• ⽀持多线程程序并发写⽇志
• ⽀持扩展不同的⽇志落地⽬标地
二.日志系统的三种实现方式
实现方式 原理简述 优点 缺点 适用场景 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);
参数说明:
strp
(char **
):
- 这是一个指向字符指针的指针。
vasprintf
会将格式化后的字符串存储在由strp
指向的内存中。内存是动态分配的,因此调用者不需要事先为这个字符串分配空间,调用完成后需要使用free
函数释放这块内存。
fmt
(const char *
):
- 格式化字符串,定义了如何格式化可变参数(与
printf
中使用的格式字符串相同)。
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...
:剩下的变长参数包行为流程:
打印当前的
value
如果还有参数(
sizeof...(args) > 0
)就递归调用xprintf(...)
否则,调用
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_size
tellg()
返回类型 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
模式时,文件流默认处于 文本模式,在这种模式下:
换行符转换(主要区别):
写入时:
\n
→\r\n
(Windows系统)读取时:
\r\n
→\n
这种转换会导致二进制数据被破坏
在Windows系统会发生:
遇到字节
0x0A
(\n
)时,系统自动插入0x0D
(\r
)实际写入文件的内容变为:
0x0D 0x0A 0x0B 0x0C 0x0D
完全破坏了原始数据
系统 文本模式换行符 二进制模式 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()(真正租出)
→ 负责租后维修
房东只做了“租出去”这一件事,其他琐事都由中介代理处理,体现了代理模式“控制访问 + 功能增强”的特性。