Linux笔记---策略模式与日志

1. 设计模式

设计模式是软件开发中反复出现的问题的通用解决方案 ,它是一套套被反复使用、多数人知晓、经过分类编目的代码设计经验总结

设计模式并非具体的代码实现,而是针对特定问题的抽象设计思路和方法论。它描述了在特定场景下,如何组织类、对象、接口等元素,以解决常见的设计问题,比如如何降低代码耦合度、提高复用性、增强可维护性等。

设计模式主要有以下几个特点

  • 通用性:适用于不同的编程语言和应用场景,只要面临相似的设计问题,就可以借鉴相应模式。
  • 经验性:是众多开发者在长期实践中总结出来的最佳实践,经过了实践的验证。
  • 抽象性:关注的是结构和交互关系,而非具体实现细节。

常见的设计模式分类(以经典的 GoF 设计模式为例):

  • 创建型模式:用于处理对象创建机制,如单例模式(保证一个类仅有一个实例)、工厂模式(隐藏对象创建的细节)、建造者模式(分步构建复杂对象)等。
  • 结构型模式:关注类和对象的组合方式,如适配器模式(使不兼容的接口能一起工作)、装饰器模式(动态给对象添加功能)、代理模式(为对象提供代理以控制访问)等。
  • 行为型模式:描述对象之间的交互和职责分配,如观察者模式(对象状态变化时通知依赖它的对象)、策略模式(封装不同算法,可动态切换)、迭代器模式(提供遍历集合的统一接口)等。

合理使用设计模式可以让代码更具可读性灵活性可扩展性,尤其在大型项目开发中,能帮助团队形成共识,提高协作效率。但需注意,设计模式并非银弹,不应过度使用,而应根据实际问题选择合适的模式。

2. 策略模式

**策略模式(Strategy Pattern)是一种行为型设计模式,它定义了一系列算法(或行为),并将每个算法封装起来,使它们可以相互替换。**这种模式让算法的变化独立于使用算法的客户端,从而实现了算法的灵活切换和复用。

当一个问题有多种解决方案(算法),且这些方案可能随需求变化时,策略模式可以避免使用大量的if-else或switch语句,使代码更清晰、易维护。

策略模式的核心思想分离算法的定义与使用

  • 将不同的算法(策略)封装成独立的类
  • 客户端通过统一接口 调用不同的策略,无需关心具体实现
  • 可以在运行时动态切换策略,而不影响客户端代码。

策略模式通常包含三个核心角色:

  • 环境类(Context): 持有策略对象的引用;提供接口给客户端使用,本身不实现具体算法;负责在运行时切换策略。
  • 抽象策略接口(Strategy): 定义所有具体策略类的公共接口;声明算法的核心方法。
  • 具体策略类(Concrete Strategy): 实现抽象策略接口,提供具体的算法实现;可以有多个不同的具体策略类。

简单来说,环境类负责目标功能的大部分实现,而有多种策略可选的核心算法部分则交由具体策略类来实现。环境类包含抽象策略接口类的引用(或指针),指向具体策略类,环境类通过该引用或者指针来调用具体策略类提供的具体算法。

3. 用策略模式设计日志类

我们的目标是输出如下格式的日志:

bash 复制代码
[2025-07-21 14:10:56] [DEBUG] [244601] [main.cpp] [11] - hello world!
[2025-07-21 14:10:56] [DEBUG] [244601] [main.cpp] [12] - hello world!
[2025-07-21 14:10:56] [DEBUG] [244601] [main.cpp] [13] - hello world!

[时间] [日志等级] [进程pid] [程序对应的源文件] [行数] - 日志信息

其中,日志等级最常见的划分方法是分为五个等级:

cpp 复制代码
enum class LogLevel
{
    DEBUG,    // 调试信息
    INFO,     // 普通信息
    WARNNING, // 警告
    ERROR,    // 错误(并未导致程序退出)
    FATAL     // 致命(导致程序退出)
};

将日志输出到什么地方?这是日志类最核心的可选策略部分,据此我们可以确定以下设计:

  • 环境类:Log日志类,负责日志信息的生成与封装。
  • 抽象策略接口:LogStrategy日志策略类,负责提供Write接口用于输出日志。
  • 具体策略类:实现Write接口,将日志输出到不同的目的地。

3.1 日志策略类

cpp 复制代码
class LogStrategy
{
public:
    virtual ~LogStrategy() = default;
    virtual void Write(const std::string &message) = 0;

protected:
    MutexModule::mutex _mutex;
};

这里的互斥锁是为了保证在多线程环境下的互斥输出,避免各个线程输出的日志之间相互杂糅。

3.2 具体策略类

cpp 复制代码
// 控制台输出策略
class ConsoleStrategy : public LogStrategy
{
public:
    void Write(const std::string &message) override
    {
        MutexModule::LockGuard lockguard(_mutex);
        std::cout << message << std::endl;
    }
};

// 文件输出策略
class FileStrategy : public LogStrategy
{
public:
    explicit FileStrategy(const std::string &path = "./log/test.log")
        : _file(path, std::ios::app)
    {
        if (!_file.is_open())
        {
            throw std::runtime_error("无法打开日志文件: " + path);
        }
    }

    void Write(const std::string &message) override
    {
        MutexModule::LockGuard lockguard(_mutex);
        _file << message << std::endl;
    }

    ~FileStrategy()
    {
        _file.close();
    }

private:
    std::ofstream _file;
};

3.3 日志类

首先我们要明确日志类的大致框架:

  • 成员变量:指向策略对象的指针(抽象类无法实例化,只能用其指针指向具体策略类)。
  • 构造函数:初始化策略对象指针,默认采用控制台输出策略。
  • 成员函数:设置策略的接口,完成日志输出的接口。
cpp 复制代码
class Logger
{
public:
    // 构造时指定策略(默认控制台输出)
    explicit Logger(std::unique_ptr<LogStrategy> strategy = std::make_unique<ConsoleStrategy>())
        : _strategy(std::move(strategy))
    {}

    void SetStrategy(std::unique_ptr<LogStrategy> strategy)
    {
        _strategy = move(strategy);
    }

    // 日志输出接口
    void Log()
    {}

private:
    std::unique_ptr<LogStrategy> _strategy;
};

接下来,就差日志输出的接口需要完成,在使用时,我们希望能达到如下效果:

cpp 复制代码
#include <iostream>
#include "log.hpp"
using namespace LogModule;

int main()
{
    Logger logger;
    logger.Log(LogLevel::DEBUG, __FILE__, __LINE__) << "hello" << " world!";
}

也就是支持可变参数,并用流插入的形式传参。为了实现这一点,我们需要设计一个内部类,作为Log函数的返回值。

这个内部类需要重载 " << " 操作符,并在析构函数当中,将拼接起来的日志输出:

cpp 复制代码
class Message
{
public:
    Message(LogLevel level, const std::string filename, int line, Logger &logger)
        : _logger(logger)
    {
        char buffer[128];
        sprintf(buffer, "[%s] [%s] [%d] [%s] [%d] - ",
                GetTimeStamp().c_str(), LevelToString(level).c_str(), getpid(), filename.c_str(), line);
        _loginfo = buffer;
    }

    template <typename T>
    Message &operator<<(const T &message)
    {
        std::stringstream buffer;
        buffer << message;
        _loginfo += buffer.str();
        return *this;
    }

    ~Message()
    {
        _logger._strategy->Write(_loginfo);
    }

private:
    Logger &_logger;      // 引用外部logger类, 方便使用策略进行刷新
    std::string _loginfo; // 一条合并完成的,完整的日志信息
};

于是,我们可以这样来设计Log函数:

cpp 复制代码
Message Log(LogLevel level, const std::string filename, int line)
{
    return Message(level, filename, line, *this);
}

除此之外,为了使得日志输出函数的使用更加方便,我们还可以提供仿函数接口:

cpp 复制代码
Message operator()(LogLevel level, const std::string filename, int line)
{
    return Message(level, filename, line, *this);
}

或者

Message operator()(LogLevel level, const std::string filename, int line)
{
    return Log(level, filename, line);
}

3.4 使用宏来简化操作

我们注意到在调用Log或者伪函数时,每次都传入的后两个参数是固定不变的;除此之外,SetStrategy接口每次调用也需要手动使用make_unique接口(或其他方式)。

于是,我们可以在头文件当中直接定义一个Logger对象 ,并加入如下的

cpp 复制代码
Logger logger;
#define LOG(level) logger(level, __FILE__, __LINE__)
// 或者 #define LOG(level) logger.Log(level, __FILE__, __LINE__)
#define USE_CONSOLE_STRATEGY() logger.SetStrategy(std::make_unique<ConsoleStrategy>())
#define USE_FILE_STRATEGY() logger.SetStrategy(std::make_unique<FileStrategy>())

使用示例:

cpp 复制代码
#include <iostream>
#include "log.hpp"
using namespace LogModule;

int main()
{
    LOG(LogLevel::DEBUG) << "hello world!";
    LOG(LogLevel::DEBUG) << "hello world!";
    LOG(LogLevel::DEBUG) << "hello world!";
    USE_FILE_STRATEGY();
    LOG(LogLevel::DEBUG) << "hello world!";
    LOG(LogLevel::DEBUG) << "hello world!";
    LOG(LogLevel::DEBUG) << "hello world!";
}

3.5 完整代码

cpp 复制代码
#pragma once
#include <iostream>
#include <fstream>
#include <memory>
#include <sstream>
#include <ctime>
#include <sys/types.h>
#include <unistd.h>
#include "Mutex.hpp"

namespace LogModule
{
    class LogStrategy
    {
    public:
        virtual ~LogStrategy() = default;
        virtual void Write(const std::string &message) = 0;

    protected:
        MutexModule::Mutex _mutex;
    };

    // 控制台输出策略
    class ConsoleStrategy : public LogStrategy
    {
    public:
        void Write(const std::string &message) override
        {
            MutexModule::LockGuard lockguard(_mutex);
            std::cout << message << std::endl;
        }
    };

    // 文件输出策略(支持路径和追加模式)
    class FileStrategy : public LogStrategy
    {
    public:
        explicit FileStrategy(const std::string &path = "./log/test.log")
            : _file(path, std::ios::app)
        {
            if (!_file.is_open())
            {
                throw std::runtime_error("无法打开日志文件: " + path);
            }
        }

        void Write(const std::string &message) override
        {
            MutexModule::LockGuard lockguard(_mutex);
            _file << message << std::endl;
        }

        ~FileStrategy()
        {
            _file.close();
        }

    private:
        std::ofstream _file;
    };

    enum class LogLevel
    {
        DEBUG,    // 调试信息
        INFO,     // 普通信息
        WARNING, // 警告
        ERROR,    // 错误
        FATAL     // 致命
    };

    std::string GetTimeStamp()
    {
        time_t cur = time(nullptr);
        struct tm tinfo;
        localtime_r(&cur, &tinfo);
        char buffer[128];
        sprintf(buffer, "%04d-%02d-%02d %02d:%02d:%02d", 1900 +\
                tinfo.tm_year, tinfo.tm_mon + 1, tinfo.tm_mday,
                tinfo.tm_hour, tinfo.tm_min, tinfo.tm_sec);
        return buffer;
    }

    std::string LevelToString(LogLevel level)
    {
        switch (level)
        {
        case LogLevel::DEBUG:
            return "DEBUG";
        case LogLevel::INFO:
            return "INFO";
        case LogLevel::WARNING:
            return "WARNING";
        case LogLevel::ERROR:
            return "ERROR";
        case LogLevel::FATAL:
            return "FATAL";
        default:
            return "UNKNOWN";
        }
    }

    class Logger
    {
    public:
        // 构造时指定策略(默认控制台输出)
        explicit Logger(std::unique_ptr<LogStrategy> strategy = std::make_unique<ConsoleStrategy>())
            : _strategy(std::move(strategy))
        {
        }

        void SetStrategy(std::unique_ptr<LogStrategy> strategy)
        {
            _strategy = move(strategy);
        }

        class Message
        {
        public:
            Message(LogLevel level, const std::string filename, int line, Logger &logger)
                : _logger(logger)
            {
                char buffer[128];
                sprintf(buffer, "[%s] [%s] [%d] [%s] [%d] - ",
                        GetTimeStamp().c_str(), LevelToString(level).c_str(), getpid(), filename.c_str(), line);
                _loginfo = buffer;
            }

            template <typename T>
            Message &operator<<(const T &message)
            {
                std::stringstream buffer;
                buffer << message;
                _loginfo += buffer.str();
                return *this;
            }

            ~Message()
            {
                _logger._strategy->Write(_loginfo);
            }

        private:
            Logger &_logger;      // 引用外部logger类, 方便使用策略进行刷新
            std::string _loginfo; // 一条合并完成的,完整的日志信息
        };

        Message Log(LogLevel level, const std::string filename, int line)
        {
            return Message(level, filename, line, *this);
        }

        Message operator()(LogLevel level, const std::string filename, int line)
        {
            return Log(level, filename, line);
        }

    private:
        std::unique_ptr<LogStrategy> _strategy;
    };

    Logger logger;
    #define LOG(level) logger.Log(level, __FILE__, __LINE__)
    #define USE_CONSOLE_STRATEGY() logger.SetStrategy(std::make_unique<ConsoleStrategy>())
    #define USE_FILE_STRATEGY() logger.SetStrategy(std::make_unique<FileStrategy>())
}