[操作系统] 策略模式进行日志模块设计


### 文章目录

  • [@[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++日志系统练习案例,建议读者在理解基础上动手编码,实现自己的日志模块,加深对设计模式的掌握。

相关推荐
吃个糖糖9 分钟前
MFC 调用海康相机进行软触发
c++·数码相机·mfc
@我漫长的孤独流浪14 分钟前
最短路与拓扑(2)
数据结构·c++·算法
٩( 'ω' )و26037 分钟前
哈希表的实现01
数据结构·c++·哈希算法·散列表
靡樊1 小时前
网络基础概念
linux·服务器·网络·c++·学习
君鼎1 小时前
TCP/IP-——C++编程详解
网络·c++·tcp/ip
whoarethenext1 小时前
c/c++爬虫总结
c语言·c++·爬虫
一只努力学习的Cat.1 小时前
C++:二叉搜索树
开发语言·c++
<但凡.1 小时前
C++修炼:多态
开发语言·c++·算法
工藤新一¹1 小时前
深度理解指针(2)
c++·指针·c 语言·深度理解指针
hjjdebug1 小时前
std::ratio<1,1000> 是什么意思?
c++·模板类·模板参数·模板函数·std所属的ratio