

### 文章目录
- [@[toc]](#文章目录 @[toc] 一、什么是设计模式? 二、日志系统的基本构成 三、策略模式在日志系统中的落地实现 ✦ 1. 策略基类 LogStrategy ✦ 2. 具体策略类 ▸ 控制台输出:ConsoleLogStrategy ▸ 文件输出:FileLogStrategy 四、日志等级枚举与转换函数 五、日志时间戳格式化 六、日志核心类 Logger 与内部类 LogMessage ✦ 1. Logger 类 ✦ 2. LogMessage 类(Logger 内部类) 七、使用宏简化调用 八、完整使用示例 九、总结)
- [一、什么是设计模式?](#文章目录 @[toc] 一、什么是设计模式? 二、日志系统的基本构成 三、策略模式在日志系统中的落地实现 ✦ 1. 策略基类 LogStrategy ✦ 2. 具体策略类 ▸ 控制台输出:ConsoleLogStrategy ▸ 文件输出:FileLogStrategy 四、日志等级枚举与转换函数 五、日志时间戳格式化 六、日志核心类 Logger 与内部类 LogMessage ✦ 1. Logger 类 ✦ 2. LogMessage 类(Logger 内部类) 七、使用宏简化调用 八、完整使用示例 九、总结)
- [二、日志系统的基本构成](#文章目录 @[toc] 一、什么是设计模式? 二、日志系统的基本构成 三、策略模式在日志系统中的落地实现 ✦ 1. 策略基类 LogStrategy ✦ 2. 具体策略类 ▸ 控制台输出:ConsoleLogStrategy ▸ 文件输出:FileLogStrategy 四、日志等级枚举与转换函数 五、日志时间戳格式化 六、日志核心类 Logger 与内部类 LogMessage ✦ 1. Logger 类 ✦ 2. LogMessage 类(Logger 内部类) 七、使用宏简化调用 八、完整使用示例 九、总结)
- [三、策略模式在日志系统中的落地实现](#文章目录 @[toc] 一、什么是设计模式? 二、日志系统的基本构成 三、策略模式在日志系统中的落地实现 ✦ 1. 策略基类 LogStrategy ✦ 2. 具体策略类 ▸ 控制台输出:ConsoleLogStrategy ▸ 文件输出:FileLogStrategy 四、日志等级枚举与转换函数 五、日志时间戳格式化 六、日志核心类 Logger 与内部类 LogMessage ✦ 1. Logger 类 ✦ 2. LogMessage 类(Logger 内部类) 七、使用宏简化调用 八、完整使用示例 九、总结)
- [✦ 1. 策略基类 LogStrategy](#文章目录 @[toc] 一、什么是设计模式? 二、日志系统的基本构成 三、策略模式在日志系统中的落地实现 ✦ 1. 策略基类 LogStrategy ✦ 2. 具体策略类 ▸ 控制台输出:ConsoleLogStrategy ▸ 文件输出:FileLogStrategy 四、日志等级枚举与转换函数 五、日志时间戳格式化 六、日志核心类 Logger 与内部类 LogMessage ✦ 1. Logger 类 ✦ 2. LogMessage 类(Logger 内部类) 七、使用宏简化调用 八、完整使用示例 九、总结)
- [✦ 2. 具体策略类](#文章目录 @[toc] 一、什么是设计模式? 二、日志系统的基本构成 三、策略模式在日志系统中的落地实现 ✦ 1. 策略基类 LogStrategy ✦ 2. 具体策略类 ▸ 控制台输出:ConsoleLogStrategy ▸ 文件输出:FileLogStrategy 四、日志等级枚举与转换函数 五、日志时间戳格式化 六、日志核心类 Logger 与内部类 LogMessage ✦ 1. Logger 类 ✦ 2. LogMessage 类(Logger 内部类) 七、使用宏简化调用 八、完整使用示例 九、总结)
- [▸ 控制台输出:ConsoleLogStrategy](#文章目录 @[toc] 一、什么是设计模式? 二、日志系统的基本构成 三、策略模式在日志系统中的落地实现 ✦ 1. 策略基类 LogStrategy ✦ 2. 具体策略类 ▸ 控制台输出:ConsoleLogStrategy ▸ 文件输出:FileLogStrategy 四、日志等级枚举与转换函数 五、日志时间戳格式化 六、日志核心类 Logger 与内部类 LogMessage ✦ 1. Logger 类 ✦ 2. LogMessage 类(Logger 内部类) 七、使用宏简化调用 八、完整使用示例 九、总结)
- [▸ 文件输出:FileLogStrategy](#文章目录 @[toc] 一、什么是设计模式? 二、日志系统的基本构成 三、策略模式在日志系统中的落地实现 ✦ 1. 策略基类 LogStrategy ✦ 2. 具体策略类 ▸ 控制台输出:ConsoleLogStrategy ▸ 文件输出:FileLogStrategy 四、日志等级枚举与转换函数 五、日志时间戳格式化 六、日志核心类 Logger 与内部类 LogMessage ✦ 1. Logger 类 ✦ 2. LogMessage 类(Logger 内部类) 七、使用宏简化调用 八、完整使用示例 九、总结)
- [四、日志等级枚举与转换函数](#文章目录 @[toc] 一、什么是设计模式? 二、日志系统的基本构成 三、策略模式在日志系统中的落地实现 ✦ 1. 策略基类 LogStrategy ✦ 2. 具体策略类 ▸ 控制台输出:ConsoleLogStrategy ▸ 文件输出:FileLogStrategy 四、日志等级枚举与转换函数 五、日志时间戳格式化 六、日志核心类 Logger 与内部类 LogMessage ✦ 1. Logger 类 ✦ 2. LogMessage 类(Logger 内部类) 七、使用宏简化调用 八、完整使用示例 九、总结)
- [五、日志时间戳格式化](#文章目录 @[toc] 一、什么是设计模式? 二、日志系统的基本构成 三、策略模式在日志系统中的落地实现 ✦ 1. 策略基类 LogStrategy ✦ 2. 具体策略类 ▸ 控制台输出:ConsoleLogStrategy ▸ 文件输出:FileLogStrategy 四、日志等级枚举与转换函数 五、日志时间戳格式化 六、日志核心类 Logger 与内部类 LogMessage ✦ 1. Logger 类 ✦ 2. LogMessage 类(Logger 内部类) 七、使用宏简化调用 八、完整使用示例 九、总结)
- [六、日志核心类 Logger 与内部类 LogMessage](#文章目录 @[toc] 一、什么是设计模式? 二、日志系统的基本构成 三、策略模式在日志系统中的落地实现 ✦ 1. 策略基类 LogStrategy ✦ 2. 具体策略类 ▸ 控制台输出:ConsoleLogStrategy ▸ 文件输出:FileLogStrategy 四、日志等级枚举与转换函数 五、日志时间戳格式化 六、日志核心类 Logger 与内部类 LogMessage ✦ 1. Logger 类 ✦ 2. LogMessage 类(Logger 内部类) 七、使用宏简化调用 八、完整使用示例 九、总结)
- [✦ 1. Logger 类](#文章目录 @[toc] 一、什么是设计模式? 二、日志系统的基本构成 三、策略模式在日志系统中的落地实现 ✦ 1. 策略基类 LogStrategy ✦ 2. 具体策略类 ▸ 控制台输出:ConsoleLogStrategy ▸ 文件输出:FileLogStrategy 四、日志等级枚举与转换函数 五、日志时间戳格式化 六、日志核心类 Logger 与内部类 LogMessage ✦ 1. Logger 类 ✦ 2. LogMessage 类(Logger 内部类) 七、使用宏简化调用 八、完整使用示例 九、总结)
- [✦ 2. LogMessage 类(Logger 内部类)](#文章目录 @[toc] 一、什么是设计模式? 二、日志系统的基本构成 三、策略模式在日志系统中的落地实现 ✦ 1. 策略基类 LogStrategy ✦ 2. 具体策略类 ▸ 控制台输出:ConsoleLogStrategy ▸ 文件输出:FileLogStrategy 四、日志等级枚举与转换函数 五、日志时间戳格式化 六、日志核心类 Logger 与内部类 LogMessage ✦ 1. Logger 类 ✦ 2. LogMessage 类(Logger 内部类) 七、使用宏简化调用 八、完整使用示例 九、总结)
- [七、使用宏简化调用](#文章目录 @[toc] 一、什么是设计模式? 二、日志系统的基本构成 三、策略模式在日志系统中的落地实现 ✦ 1. 策略基类 LogStrategy ✦ 2. 具体策略类 ▸ 控制台输出:ConsoleLogStrategy ▸ 文件输出:FileLogStrategy 四、日志等级枚举与转换函数 五、日志时间戳格式化 六、日志核心类 Logger 与内部类 LogMessage ✦ 1. Logger 类 ✦ 2. LogMessage 类(Logger 内部类) 七、使用宏简化调用 八、完整使用示例 九、总结)
- [八、完整使用示例](#文章目录 @[toc] 一、什么是设计模式? 二、日志系统的基本构成 三、策略模式在日志系统中的落地实现 ✦ 1. 策略基类 LogStrategy ✦ 2. 具体策略类 ▸ 控制台输出:ConsoleLogStrategy ▸ 文件输出:FileLogStrategy 四、日志等级枚举与转换函数 五、日志时间戳格式化 六、日志核心类 Logger 与内部类 LogMessage ✦ 1. Logger 类 ✦ 2. LogMessage 类(Logger 内部类) 七、使用宏简化调用 八、完整使用示例 九、总结)
- [九、总结](#文章目录 @[toc] 一、什么是设计模式? 二、日志系统的基本构成 三、策略模式在日志系统中的落地实现 ✦ 1. 策略基类 LogStrategy ✦ 2. 具体策略类 ▸ 控制台输出:ConsoleLogStrategy ▸ 文件输出:FileLogStrategy 四、日志等级枚举与转换函数 五、日志时间戳格式化 六、日志核心类 Logger 与内部类 LogMessage ✦ 1. Logger 类 ✦ 2. LogMessage 类(Logger 内部类) 七、使用宏简化调用 八、完整使用示例 九、总结)
在当今IT行业中,程序开发已不仅仅是写代码,更重要的是"写好代码"。在系统开发中,日志系统 是一个不可或缺的模块,承担着问题定位、性能分析、安全审计等重要职责。而借助于设计模式 ,我们可以构建一个更灵活、可拓展、维护性更强的日志系统。本篇博客将结合C++代码示例,深入讲解策略模式在日志系统中的应用。
一、什么是设计模式?
在软件开发中,为了解决一些通用、重复出现的问题,业界总结出一套"最佳实践"方案,这就是设计模式。设计模式并非代码模板,而是对问题的抽象解决思路。
在本项目中,我们采用了策略模式(Strategy Pattern) ,它的核心思想是:定义一组算法,将每一个算法封装起来,并且使它们可以互换使用。也就是说,行为的变化不影响使用它的对象本身。
二、日志系统的基本构成
一个合格的日志系统通常需要具备以下信息:
- 时间戳:记录事件发生的准确时间;
- 日志等级:例如 DEBUG、INFO、WARNING、ERROR、FATAL;
- 日志内容:需要打印的事件信息;
- 元数据(可选):如源文件名、行号、线程ID、进程ID等,辅助定位问题。
我们希望实现的日志输出格式如下:
plain
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - hello world
三、策略模式在日志系统中的落地实现
策略模式的精髓是将不同的行为封装为不同的策略类 ,而日志模块的"行为"就是日志的输出方式(比如输出到终端或写入文件)。
✦ 1. 策略基类 LogStrategy
这是所有具体策略类的基类,定义了统一接口:
cpp
class LogStrategy {
public:
virtual void SyncLog(const std::string &message) = 0; // 刷新日志 ,写成纯虚函数,派生类必须强制实现该函数
virtual ~LogStrategy() = default; // 析构函数:写成使用默认的析构函数
};
这是一个纯虚函数接口,用于实现不同的日志写入方式。
✦ 2. 具体策略类
▸ 控制台输出:ConsoleLogStrategy
cpp
// 显示器打印日志的策略 : 子类
class ConsoleLogStrategy : public LogStrategy
{
public:
ConsoleLogStrategy()
{
}
void SyncLog(const std::string &message) override
{
LockGuard lockguard(_mutex);
std::cout << message << gsep;
}
~ConsoleLogStrategy()
{
}
private:
Mutex _mutex;
};
使用互斥锁保证线程安全,并将日志写入 std::cout
。
▸ 文件输出:FileLogStrategy
cpp
// 文件打印日志的策略 : 子类
const std::string defaultpath = "./log";
const std::string defaultfile = "my.log";
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const std::string &path = defaultpath, const std::string &file = defaultfile)
: _path(path),
_file(file)
{
LockGuard lockguard(_mutex);
if (std::filesystem::exists(_path))
{
return;
}
try
{
std::filesystem::create_directories(_path);
}
catch (const std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << '\n';
}
}
void SyncLog(const std::string &message) override
{
LockGuard lockguard(_mutex);
std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file; // "./log/" + "my.log"
std::ofstream out(filename, std::ios::app); // app : 'append' 追加写入的 方式打开
if (!out.is_open())
{
return;
}
out << message << gsep;
out.close();
}
~FileLogStrategy()
{
}
private:
std::string _path; // 日志文件所在路径
std::string _file; // 日志文件本身
Mutex _mutex;
};
- 构造函数会检查目录是否存在;
- 使用
std::ofstream
追加写入日志; - 同样使用互斥锁保护写入过程。
四、日志等级枚举与转换函数
日志等级被定义为强类型枚举:
c
enum class LogLevel { DEBUG, INFO, WARNING, ERROR, FATAL };
转换函数 Level2Str(LogLevel)
用于将枚举转为字符串,便于日志输出格式化。
enum class
的枚举值是限定在其枚举类型本身的作用域内的。必须使用枚举类型名和 ::
操作符来访问枚举值,这避免了命名冲突。
五、日志时间戳格式化
时间的格式由 GetTimeStamp()
函数生成:
cpp
std::string GetTimeStamp() // 获取当前时间
{
time_t curr = time(nullptr);
struct tm curr_tm;
localtime_r(&curr, &curr_tm);
char timebuffer[128];
snprintf(timebuffer, sizeof(timebuffer),"%4d-%02d-%02d %02d:%02d:%02d",
curr_tm.tm_year+1900, // 操作系统默认的时间戳是 year - 1900,所以要+1900
curr_tm.tm_mon+1, // 操作系统获取的月份是从0开始的0 ~ 11,所以要+1
curr_tm.tm_mday,
curr_tm.tm_hour,
curr_tm.tm_min,
curr_tm.tm_sec
);
return timebuffer;
}
使用 snprintf
以字符串形式格式化时间,保证可读性。
六、日志核心类 Logger 与内部类 LogMessage
Logger类是日志模块的核心:
✦ 1. Logger 类
- 持有一个策略指针
_fflush_strategy
,作为内部类·; - 提供
EnableConsoleLogStrategy()
和EnableFileLogStrategy()
来切换策略; - 使用重载函数
operator()
生成一个LogMessage
临时对象:
cpp
LogMessage operator()(LogLevel level, std::string name, int line)
该对象负责构建一条完整的日志记录,构造完直接销毁。
✦ 2. LogMessage 类(Logger 内部类)
cpp
// 表示的是未来的一条日志
class LogMessage
{
public:
LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger)
: _curr_time(GetTimeStamp()), // 获取当前时间
_level(level), // 日志等级
_pid(getpid()), // 进程号
_src_name(src_name), // 源文件名
_line_number(line_number), // 源文件行号
_logger(logger) // 当前日志对象所属的logger
{
/*
std::stringstream如何使用:
C++17
将格式转换成string,保存到创建的ss对象,写入stringstream流里面,将所有写入的自动转为string类型
后面可以通过 .str()接口 存入普通的string对象中
但是enum class不能直接转换成string 所以需要自定义转换函数 Level2Str(_level)
*/
// 日志的左边部分,合并起来
std::stringstream ss;
ss << "[" << _curr_time << "] "
<< "[" << Level2Str(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _src_name << "] "
<< "[" << _line_number << "] "
<< "- ";
_loginfo = ss.str();
}
// LogMessage() << "hell world" << "XXXX" << 3.14 << 1234
// 构造函数用来打印日志头信息(结构化元数据),<< 重载函数用来构建日志具体内容
template <typename T>
LogMessage &operator<<(const T &info)
{
// a = b = c =d;
// 日志的右半部分,可变的 使用LogMessage & 自身的引用返回
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this; // 返回当前对象,因为是引用,所以可以继续链式调用
}
~LogMessage()
{
if (_logger._fflush_strategy)
{
/*内部类的作用:*/
// 这就是为什么要写成内部类,因为内部类可以访问外部类的私有成员
// 这样就可以使用logger对象中的fflush_strategy策略类,然后使用成员函数进行刷新在指定位置
_logger._fflush_strategy->SyncLog(_loginfo);
}
}
private:
std::string _curr_time; // 当前时间
LogLevel _level; // 日志等级
pid_t _pid; // 进程号
std::string _src_name; // 源文件名
int _line_number; // 源文件行号
std::string _loginfo; //将以上内容合并之后,一条完整的信息
Logger &_logger; // 一个Logger对象,表示的是一个日志对象
};
这个内部类的职责是:
- 构造函数收集日志元数据(时间、等级、文件名、行号、pid等);
- 重载
operator<<
来拼接日志主体内容; - 析构函数中自动调用
SyncLog()
完成日志刷新:
cpp
~LogMessage()
{
if (_logger._fflush_strategy)
{
/*内部类的作用:*/
// 这就是为什么要写成内部类,因为内部类可以访问外部类的私有成员
// 这样就可以使用logger对象中的fflush_strategy策略类,然后使用成员函数进行刷新在指定位置
_logger._fflush_strategy->SyncLog(_loginfo);
}
}
这是一种 RAII(资源获取即初始化)思想的应用,确保日志一定在对象生命周期结束时输出。
七、使用宏简化调用
定义了以下宏方便用户调用:
cpp
// 使用宏,简化用户操作,获取文件名和行号
#define LOG(level) logger(level, __FILE__, __LINE__) // __FILE__ __LINE__ 为预定义宏 在主函数文件中使用时会替换为主函文件的文件名和行号
#define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()
#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()
其中 __FILE__
和 __LINE__
是C++预定义宏,会在宏展开时替换为当前源文件和行号。
八、完整使用示例
cpp
using namespace LogModule;
int main() {
Enable_Console_Log_Strategy(); // 选择输出到控制台
LOG(LogLevel::DEBUG) << "hello world";
Enable_File_Log_Strategy(); // 切换输出到文件
LOG(LogLevel::WARNING) << "log file output test";
return 0;
}
输出示例:
c
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - hello world
[2024-08-04 12:27:03] [WARNING] [202938] [main.cc] [18] - log file output test
九、总结
通过这套自定义日志模块的实现,我们学习并实践了策略模式的核心思想:行为的封装与可替换性 。这种设计不仅使日志系统具有更高的可拓展性(比如将来可以添加数据库日志策略、远程日志策略等),还体现了低耦合、高内聚的设计理念。
日志系统本身也利用了C++的许多优秀特性:
- 内部类实现闭包式封装;
std::stringstream
实现类型安全拼接;- RAII 保证资源自动释放和操作完成;
- 线程安全的日志输出策略。
这是一个非常经典且实用的C++日志系统练习案例,建议读者在理解基础上动手编码,实现自己的日志模块,加深对设计模式的掌握。