当前位置: 首页 > news >正文

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


文章目录

    • @[toc]
    • 一、什么是设计模式?
    • 二、日志系统的基本构成
    • 三、策略模式在日志系统中的落地实现
      • ✦ 1. 策略基类 LogStrategy
      • ✦ 2. 具体策略类
        • ▸ 控制台输出:ConsoleLogStrategy
        • ▸ 文件输出:FileLogStrategy
    • 四、日志等级枚举与转换函数
    • 五、日志时间戳格式化
    • 六、日志核心类 Logger 与内部类 LogMessage
      • ✦ 1. Logger 类
      • ✦ 2. LogMessage 类(Logger 内部类)
    • 七、使用宏简化调用
    • 八、完整使用示例
    • 九、总结

在当今IT行业中,程序开发已不仅仅是写代码,更重要的是“写好代码”。在系统开发中,日志系统是一个不可或缺的模块,承担着问题定位、性能分析、安全审计等重要职责。而借助于设计模式,我们可以构建一个更灵活、可拓展、维护性更强的日志系统。本篇博客将结合C++代码示例,深入讲解策略模式在日志系统中的应用


一、什么是设计模式?

在软件开发中,为了解决一些通用、重复出现的问题,业界总结出一套“最佳实践”方案,这就是设计模式。设计模式并非代码模板,而是对问题的抽象解决思路。

在本项目中,我们采用了策略模式(Strategy Pattern),它的核心思想是:定义一组算法,将每一个算法封装起来,并且使它们可以互换使用。也就是说,行为的变化不影响使用它的对象本身。


二、日志系统的基本构成

一个合格的日志系统通常需要具备以下信息:

  • 时间戳:记录事件发生的准确时间;
  • 日志等级:例如 DEBUG、INFO、WARNING、ERROR、FATAL;
  • 日志内容:需要打印的事件信息;
  • 元数据(可选):如源文件名、行号、线程ID、进程ID等,辅助定位问题。

我们希望实现的日志输出格式如下:

[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - hello world

三、策略模式在日志系统中的落地实现

策略模式的精髓是将不同的行为封装为不同的策略类,而日志模块的“行为”就是日志的输出方式(比如输出到终端或写入文件)。

✦ 1. 策略基类 LogStrategy

这是所有具体策略类的基类,定义了统一接口:

class LogStrategy {
public:virtual void SyncLog(const std::string &message) = 0; // 刷新日志 ,写成纯虚函数,派生类必须强制实现该函数virtual ~LogStrategy() = default; // 析构函数:写成使用默认的析构函数
};

这是一个纯虚函数接口,用于实现不同的日志写入方式。


✦ 2. 具体策略类

▸ 控制台输出:ConsoleLogStrategy
// 显示器打印日志的策略 : 子类
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
// 文件打印日志的策略 : 子类
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 追加写入日志;
  • 同样使用互斥锁保护写入过程。

四、日志等级枚举与转换函数

日志等级被定义为强类型枚举:

enum class LogLevel { DEBUG, INFO, WARNING, ERROR, FATAL };

转换函数 Level2Str(LogLevel) 用于将枚举转为字符串,便于日志输出格式化。

enum class 的枚举值是限定在其枚举类型本身的作用域内的。必须使用枚举类型名和 :: 操作符来访问枚举值,这避免了命名冲突。


五、日志时间戳格式化

时间的格式由 GetTimeStamp() 函数生成:

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,所以要+1900curr_tm.tm_mon+1, // 操作系统获取的月份是从0开始的0 ~ 11,所以要+1curr_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 临时对象:
LogMessage operator()(LogLevel level, std::string name, int line)

该对象负责构建一条完整的日志记录,构造完直接销毁。


✦ 2. LogMessage 类(Logger 内部类)

// 表示的是未来的一条日志
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() 完成日志刷新:
~LogMessage()
{if (_logger._fflush_strategy){/*内部类的作用:*/// 这就是为什么要写成内部类,因为内部类可以访问外部类的私有成员// 这样就可以使用logger对象中的fflush_strategy策略类,然后使用成员函数进行刷新在指定位置_logger._fflush_strategy->SyncLog(_loginfo);}
}

这是一种 RAII(资源获取即初始化)思想的应用,确保日志一定在对象生命周期结束时输出。


七、使用宏简化调用

定义了以下宏方便用户调用:

// 使用宏,简化用户操作,获取文件名和行号
#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++预定义宏,会在宏展开时替换为当前源文件和行号。


八、完整使用示例

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;
}

输出示例:

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

相关文章:

  • 新能源汽车三电质量护盾:蓝光三维扫描技术显身手
  • ultralytics中tasks.py---parse_model函数解析
  • VSCode python配置
  • 基于策略的强化学习方法之策略梯度(Policy Gradient)详解
  • Axure设计之轮播图——案例“一图一轮播”
  • LLM笔记(一)基本概念
  • Kotlin 协程实战:实现异步值加载委托,对值进行异步懒初始化
  • 【C++】模板(初阶)
  • 数据库字段唯一性修复指南:从设计缺陷到规范实现
  • 嵌入式设计模式基础--C语言的继承封装与多态
  • 基于Python的量化交易实盘部署与风险管理指南
  • Spark的基础介绍
  • 玛哈特矫平机:金属板材加工中的“平整大师”
  • Spring Cloud Gateway 聚合 Swagger 文档:一站式API管理解决方案
  • 游戏引擎学习第278天:将实体存储移入世界区块
  • 基于springboot+vue的医院门诊管理系统
  • 鸿蒙OSUniApp 制作个人信息编辑界面与头像上传功能#三方框架 #Uniapp
  • Go 语言 net/http 包使用:HTTP 服务器、客户端与中间件
  • 【MySQL】自适应哈希详解:作用、配置以及如何查看
  • 5 WPF中的application对象介绍
  • 普京确定俄乌谈判俄方代表团名单
  • KPL“王朝”诞生背后:AG和联赛一起迈向成熟
  • 视频|王弘治:王太后,“先天宫斗圣体”?
  • 国内首家破产的5A景区游客爆满,洛阳龙潭大峡谷:破产并非因景观不好
  • 佩斯科夫:若普京认为必要,将公布土耳其谈判俄方代表人选
  • 火车站员工迟到,致出站门未及时开启乘客被困?铁路部门致歉