设计模式综合案例1:设计一个日志系统(外观+策略+责任链)

日志系统,说白了就是给程序装个「黑匣子」。我做了这么多年C++开发,几乎每个项目都离不开它。但有意思的是,很多人写日志就是简单调个printf,等到线上出问题要排查时,才发现日志要么太多刷爆磁盘,要么太少啥也没记下来。

今天咱们就动手搭一个像样的日志系统。用三个设计模式组合起来:外观模式统一接口,策略模式支持多种输出方式,责任链模式控制日志级别过滤。嗯,这套组合拳我在好几个生产项目里都用过,效果不错。

先看看整体架构

我习惯先画一张图,把各个模块的关系理清楚。这样写代码时心里有数,不会东一榔头西一棒子。

日志系统架构图(外观+策略+责任链) 客户端代码 Logger(外观) log(level, msg) / setLevel() / addHandler() 责任链:日志级别过滤 DebugHandler → InfoHandler → WarnHandler → ErrorHandler 每个Handler决定是否处理,或传给下一个 策略:输出方式 ConsoleHandler | FileHandler | NetworkHandler | DatabaseHandler 每个Handler实现相同的接口:handle(msg) 外观模式:简化调用 客户端只需调用log() 责任链模式:灵活过滤 级别不够?自动跳过 策略模式:可插拔 想加新输出?加个类就行

这张图你看懂了吗?客户端只跟Logger打交道,Logger内部把请求丢给责任链过滤级别,过滤通过后再交给策略层去实际输出。每个环节各司其职,互不干扰。

第一步:定义日志级别和消息结构

先定个规矩。日志级别从低到高,我一般用枚举:

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

消息结构体,把时间、级别、内容、位置信息都包进去:

struct LogMessage {
    LogLevel level;
    std::string content;
    std::string file;
    int line;
    std::chrono::system_clock::time_point timestamp;

    LogMessage(LogLevel lvl, std::string msg,
               const char* f = "", int ln = 0)
        : level(lvl), content(std::move(msg)),
          file(f), line(ln),
          timestamp(std::chrono::system_clock::now()) {}
};
小提示:fileline带上,排查问题时能直接定位到代码行。我习惯用宏来封装调用,比如LOG_INFO("用户登录成功")自动填入__FILE____LINE__

第二步:策略模式——多种输出方式

策略模式的核心思想:定义一组算法,把它们封装起来,并且可以互相替换。这里每个输出方式就是一个策略。

先写抽象基类:

class LogHandler {
public:
    virtual ~LogHandler() = default;
    virtual void handle(const LogMessage& msg) = 0;
};

然后实现几个具体策略。控制台输出最简单:

class ConsoleHandler : public LogHandler {
public:
    void handle(const LogMessage& msg) override {
        std::cout << "[" << levelToString(msg.level) << "] "
                  << msg.content << " (" << msg.file
                  << ":" << msg.line << ")" << std::endl;
    }

private:
    static std::string levelToString(LogLevel lvl) {
        switch (lvl) {
            case LogLevel::DEBUG: return "DEBUG";
            case LogLevel::INFO:  return "INFO";
            case LogLevel::WARN:  return "WARN";
            case LogLevel::ERROR: return "ERROR";
            case LogLevel::FATAL: return "FATAL";
            default: return "UNKNOWN";
        }
    }
};

文件输出稍微复杂点,要考虑文件打开、写入、关闭:

class FileHandler : public LogHandler {
public:
    explicit FileHandler(const std::string& filename) {
        file_.open(filename, std::ios::app);
        if (!file_.is_open()) {
            throw std::runtime_error("无法打开日志文件: " + filename);
        }
    }

    void handle(const LogMessage& msg) override {
        file_ << "[" << formatTime(msg.timestamp) << "] "
              << "[" << levelToString(msg.level) << "] "
              << msg.content << std::endl;
    }

    ~FileHandler() {
        if (file_.is_open()) file_.close();
    }

private:
    std::ofstream file_;

    static std::string formatTime(
        const std::chrono::system_clock::time_point& tp) {
        auto t = std::chrono::system_clock::to_time_t(tp);
        std::tm tm = *std::localtime(&t);
        char buf[20];
        std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm);
        return buf;
    }
};
注意:文件日志在多线程环境下要加锁。我曾经在生产环境遇到过日志文件内容错乱的问题,就是因为多个线程同时写同一个文件。解决方案:要么用互斥锁,要么每个线程写独立文件。

你还可以扩展网络输出、数据库输出等。策略模式的好处就在这里——加新功能不用改现有代码。

第三步:责任链模式——日志级别过滤

责任链模式,说白了就是一条链,每个节点决定「这事我管不管」。管就处理,不管就传给下一个。

先定义抽象节点:

class LevelFilter {
public:
    virtual ~LevelFilter() = default;

    void setNext(std::shared_ptr<LevelFilter> next) {
        next_ = std::move(next);
    }

    virtual void filter(const LogMessage& msg,
                        const std::vector<std::shared_ptr<LogHandler>>& handlers) {
        if (next_) {
            next_->filter(msg, handlers);
        }
    }

protected:
    std::shared_ptr<LevelFilter> next_;
};

然后实现具体的过滤器。比如InfoFilter只处理INFO及以上级别:

class InfoFilter : public LevelFilter {
public:
    void filter(const LogMessage& msg,
                const std::vector<std::shared_ptr<LogHandler>>& handlers) override {
        if (msg.level >= LogLevel::INFO) {
            for (auto& h : handlers) {
                h->handle(msg);
            }
        }
        // 不管是否处理,都传给下一个
        LevelFilter::filter(msg, handlers);
    }
};

类似地,WarnFilter只处理WARN及以上,ErrorFilter只处理ERROR及以上。链的顺序决定了过滤的严格程度。

核心思路:责任链的每个节点只关注自己负责的级别。DEBUG日志只走DebugFilter,ERROR日志一路绿灯直到ErrorFilter。这样每个过滤器职责单一,代码清晰。

第四步:外观模式——统一入口

外观模式,就是给客户端一个简单的门面。客户端不需要知道内部有责任链、有策略,只需要调用log()就行。

class Logger {
public:
    Logger() {
        // 默认配置:控制台输出,INFO级别
        addHandler(std::make_shared<ConsoleHandler>());
        setLevel(LogLevel::INFO);
    }

    void setLevel(LogLevel level) {
        // 构建责任链
        std::shared_ptr<LevelFilter> chain = nullptr;

        auto addFilter = [&](LogLevel lvl, auto filter) {
            auto f = std::make_shared<decltype(filter)>(filter);
            if (chain) {
                // 找到链尾
                auto tail = chain;
                while (tail->next_) tail = tail->next_;
                tail->setNext(f);
            } else {
                chain = f;
            }
        };

        // 按级别从低到高构建
        if (level <= LogLevel::DEBUG) addFilter(LogLevel::DEBUG, DebugFilter());
        if (level <= LogLevel::INFO)  addFilter(LogLevel::INFO,  InfoFilter());
        if (level <= LogLevel::WARN)  addFilter(LogLevel::WARN,  WarnFilter());
        if (level <= LogLevel::ERROR) addFilter(LogLevel::ERROR, ErrorFilter());

        filter_chain_ = chain;
    }

    void addHandler(std::shared_ptr<LogHandler> handler) {
        handlers_.push_back(std::move(handler));
    }

    void log(LogLevel level, const std::string& msg,
             const char* file = "", int line = 0) {
        LogMessage message(level, msg, file, line);
        if (filter_chain_) {
            filter_chain_->filter(message, handlers_);
        }
    }

private:
    std::vector<std::shared_ptr<LogHandler>> handlers_;
    std::shared_ptr<LevelFilter> filter_chain_;
};

第五步:用起来

客户端代码简洁得让人舒服:

int main() {
    Logger logger;

    // 添加文件输出
    logger.addHandler(std::make_shared<FileHandler>("app.log"));

    // 设置级别为WARN,DEBUG和INFO日志会被过滤掉
    logger.setLevel(LogLevel::WARN);

    // 这些不会输出
    logger.log(LogLevel::DEBUG, "调试信息", __FILE__, __LINE__);
    logger.log(LogLevel::INFO, "用户登录", __FILE__, __LINE__);

    // 这些会输出到控制台和文件
    logger.log(LogLevel::WARN, "磁盘空间不足", __FILE__, __LINE__);
    logger.log(LogLevel::ERROR, "数据库连接失败", __FILE__, __LINE__);

    return 0;
}

你看,客户端只需要关心「我要记什么级别的日志」和「日志输出到哪里」。至于内部怎么过滤、怎么分发,完全不用管。

避坑指南

我总结几个实际项目中踩过的坑:

  • 性能问题:日志系统如果写得太频繁,会成为性能瓶颈。我曾经在一个高并发服务里,日志写入占了10%的CPU。解决方案:异步日志,把写入操作丢到后台线程。
  • 日志轮转:文件日志如果不做轮转,一个文件能撑到几个GB。我建议按大小或时间自动切割,保留最近N个文件。
  • 线程安全:上面代码没加锁,多线程环境会出问题。实际使用时,Logger内部要加互斥锁,或者用无锁队列。
  • 不要记录敏感信息:密码、身份证号这些,千万别往日志里写。我见过有人把用户密码明文打日志,结果被安全审计抓了个正着。

总结一下

这个日志系统,三个模式各司其职:

模式 职责 对应模块
外观模式 简化客户端调用 Logger类
策略模式 支持多种输出方式 ConsoleHandler、FileHandler等
责任链模式 灵活控制日志级别 DebugFilter、InfoFilter等

说实话,设计模式不是用来炫技的。它们是为了解决实际问题而生的。这个日志系统里,每个模式都解决了一个具体的痛点:外观让调用简单,策略让扩展容易,责任链让过滤灵活。你想想看,如果不用这些模式,硬编码的话,代码会变成什么样?大概率是一堆if-else和switch,改一个地方牵一发动全身。

好了,这个案例就到这。代码你可以直接拿去用,但建议根据实际场景调整。毕竟,没有银弹,只有适合的解决方案。


专注资料整理