spdlog源码阅读:05.spdlog中的设计模式

前言:从功能到架构,探寻 spdlog 的设计之美

在本系列之前的文章中,我们深入剖析了 spdlog 的异步机制、核心组件,并且尝试拓展实现一个压缩sink,还尝试进行了性能优化实践。

我们理解了 spdlog 在功能和性能上“做了什么”以及“怎么做的”。现在,我们跳出具体实现的细节,从设计模式的角度重新看一下这个库,

深入探讨 spdlog 是如何巧妙地运用策略模式 (Strategy)组合模式 (Composite)工厂模式 (Factory)、以及单例模式 (Singleton),来构建其灵活、高效且用户友好的体系的。

1. 策略模式 (Strategy Pattern):解耦日志行为的核心

策略模式的核心思想是:定义一系列算法,将每一个算法封装起来,并使它们可以相互替换。

为什么使用策略模式?

对于一个日志库而言,其核心任务是记录日志,但日志的“行为”是多变的:

  • 日志消息应该以什么样的格式 (Format)呈现?
  • 日志消息应该被发送到哪个目的地 (Destination)

spdlog 通过策略模式,将 logger 的核心记录逻辑与具体的格式化策略输出策略完全解耦。

在 spdlog 中是如何实现的?

spdlog 中有两处经典的策略模式应用:Formatter 对 Sink 的策略Sink 对 Logger 的策略

a) Formatter:定义日志的“格式化策略”

spdlog 定义了一个 formatter 接口,所有具体的格式化器都实现这个接口。sink 对象则拥有 (has-a) 一个 formatter 策略。

// spdlog/formatter.h

class formatter {  
public:  
virtual ~formatter() = default;  
virtual void format(const details::log_msg &msg, memory_buf_t &dest) = 0;  
virtual std::unique_ptr<formatter> clone() const = 0;  
};

每个 sink 对象都持有一个 formatter。当 sink 需要记录日志时,它不关心如何格式化,而是把这个任务委托给它持有的 formatter 策略对象。

// spdlog/sinks/base_sink.h  
template <typename Mutex>  
class SPDLOG_API base_sink : public sink {  
public:  
// ...  
    void log(const details::log_msg &msg) final override  
    {  
        std::lock_guard<Mutex> lock(mutex_);  

        sink_it_(formatted); // sink_it_ 是子类实现的具体输出逻辑 , 实际调用 formatter_ 的 format 方法,执行格式化策略 
    }

protected:  
// 持有一个 formatter 策略  
    std::unique_ptr<spdlog::formatter> formatter_;  
    Mutex mutex_;  
// ...  
};

b) Sink:定义日志的“输出策略”

同样地,spdlog 定义了一个 sink 接口,代表不同的输出目的地策略。logger 类则拥有 (has-a) 一个或多个 sink 策略。

// spdlog/sinks/sink.h  
class  sink {  
public:  
    virtual ~sink() = default;  
    virtual void log(const details::log_msg &msg) = 0;  
    virtual void flush() = 0;  
    virtual void set_pattern(const std::string &pattern) = 0;  
    virtual void set_formatter(std::unique_ptr<spdlog::formatter> sink_formatter) = 0;  
// ...  
};  

logger 类中持有一个 sink 的集合。当用户调用 logger->info(...) 时,logger 将日志消息打包后,委托给它持有的所有 sink 策略对象去处理。

class SPDLOG_API logger {
public:
    ......
protected:
    std::string name_;
    std::vector<sink_ptr> sinks_;
    spdlog::level_t level_{level::info};
    spdlog::level_t flush_level_{level::off};
    err_handler custom_err_handler_{nullptr};
    details::backtracer tracer_;
    }

下面是logger,sink和formatter三个类的类图:

logger_sink

这样做有什么好处?

  • 高度解耦: logger、sink、formatter 三者职责清晰,logger 不关心输出细节,sink 不关心格式化细节。
  • 极强的扩展性: 用户可以轻松创建自己的 sink(如写入数据库)和 formatter(如输出为 JSON),并与现有 logger 无缝集成。
  • 灵活性: 可以在运行时动态地为 sink 更换 formatter,改变日志格式。

2. 组合模式 (Composite Pattern):统一处理多个目的地

组合模式的核心思想是:将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。

为什么使用组合模式?

我们常常希望一条日志能同时输出到多个地方,比如控制台和文件。组合模式使得 logger 在处理这种情况时无需区分是在跟一个 sink 还是多个 sink 打交道。

在 spdlog 中是如何实现的?

如上节代码所示,spdlog 的 logger 类中持有一个 std::vector<sink_ptr> 成员 sinks_。当记录日志时,logger::sink_it_() 方法会简单地遍历这个 vector,并对其中的每一个 sink 调用 log() 方法。

这里的实现完美地体现了组合模式的核心思想——统一对待单个对象和对象集合。对于 logger(客户端)来说,它处理 sinks_ 的逻辑是完全一致的,无论这个 vector 中只有一个 sink 还是有十个。

spdlog 还提供了一个更经典的组合模式实现——dist_sink,它本身是一个 sink,内部也包含一个 sink 的集合,其 log() 方法就是遍历并调用内部所有 sink 的 log()。

这样做有什么好处?

  • 简化客户端代码: logger 的实现变得非常简单,一个循环就解决了向所有目的地输出的问题。
  • 灵活性: 用户可以自由地向 logger 的 sinks_ 列表添加或移除 sink,动态地改变日志的输出组合。

3. 工厂模式 (Factory Pattern):简化对象的创建过程

在 spdlog 中,工厂模式更多的是以工厂函数 (Factory Function) 结合 模板化的工厂结构体 (Factory Struct) 的形式体现,它提供了一个集中的、简化的方式来创建复杂的、预配置好的 logger 对象。

为什么使用工厂模式?

手动创建一个功能完备的 logger 对象比较繁琐。尤其是异步的logger,还需要去手动创建线程池和设置队列大小。工厂模式将这些复杂的创建逻辑封装起来,向用户提供一个简单、易用的创建接口。

在 spdlog 中是如何实现的?

spdlog 提供了一系列工厂函数,如 basic_logger_mt。这些函数巧妙地利用模板参数来接收一个“工厂”类型,这个工厂类型决定了是创建同步 logger还是异步 logger。

// 一个异步文件日志的创建示例  
void async_example()  
{  
// 使用 async_factory 作为模板参数,创建异步 logger  
    auto async_file = spdlog::basic_logger_mt<spdlog::async_factory>("async_file_logger", "logs/async_log.txt");  
}
// 这是同步工厂的实现  
struct synchronous_factory {  
template <typename Sink, typename... SinkArgs>  
static std::shared_ptr<spdlog::logger> create(std::string logger_name, SinkArgs &&...args) {  
    auto sink = std::make_shared<Sink>(std::forward<SinkArgs>(args)...);  
    auto new_logger = std::make_shared<spdlog::logger>(std::move(logger_name), std::move(sink));  
    details::registry::instance().initialize_logger(new_logger);  
    return new_logger;  
}  
};
// 这是异步工厂的实现  
template<async_overflow_policy OverflowPolicy = async_overflow_policy::block>  
struct async_factory_impl  
{  
    template<typename Sink, typename... SinkArgs>  
    static std::shared_ptr<async_logger> create(std::string logger_name, SinkArgs &&... args)  
    {  
    // ... (省略创建和获取全局线程池的逻辑) ...  
    auto sink = std::make_shared<Sink>(std::forward<SinkArgs>(args)...);  
    auto new_logger = std::make_shared<async_logger>(std::move(logger_name), std::move(sink),  
    std::move(tp), OverflowPolicy);  
    details::registry::instance().initialize_logger(new_logger);  
    return new_logger;  
    }  
};

    using async_factory = async_factory_impl<async_overflow_policy::block>;
    .......

最终的工厂函数 basic_logger_mt 接收这个工厂类型作为模板参数,并调用其 create 方法。

template <typename Factory = spdlog::synchronous_factory>  
inline std::shared_ptr<logger> basic_logger_mt(const std::string &logger_name,  
                                               const filename_t &filename,  
                                               bool truncate = false,  
                                               const file_event_handlers &event_handlers = {}) 
{  
    // 调用传入的 Factory 的静态 create 方法来创建 logger  
    return Factory::template create<sinks::basic_file_sink_mt>(logger_name, filename, truncate,event_handlers);  
}

下面是工厂调用的时序图:

factory

这样做有什么好处?

  • 极大地简化了用户的使用: 用户可以用一行代码创建一个功能完备的同步或异步 logger。
  • 高度灵活与解耦: 通过模板参数注入不同的工厂实现,使得创建不同类型 logger 的逻辑得以复用和解耦。
  • 保证了最佳实践: 工厂函数创建的 logger 通常是经过预配置的、符合最佳实践的实例(例如,_mt 后缀的都是线程安全的)。

通过这种模板方法和手段实现工厂也是C++实现工厂模式的一个优点,避免了重复创建无意义的工厂接口类,而是通过模板去实现静态多态

4. 单例模式 (Singleton Pattern):提供全局便捷访问

单例模式确保一个类只有一个实例,并提供一个全局访问点。

为什么使用单例模式?

对于许多简单的应用场景,用户不希望手动创建和管理 logger 实例,而是希望有一个像 printf 一样方便的全局日志函数。spdlog 通过单例模式提供了一个全局的 logger 注册表 (registry) 和一个默认的 logger。

在 spdlog 中是如何实现的?

spdlog 提供了全局的日志函数,如 spdlog::info, spdlog::error 等。

#include "spdlog/spdlog.h"

int main()   
{  
    // 直接使用全局函数进行日志记录  
    spdlog::info("Welcome to spdlog!");  
    spdlog::error("Some error message with arg: {}", 1);  
}

这些全局函数内部都通过调用 default_logger_raw() 来获取一个默认的 logger 实例。

   template <typename... Args>  
inline void debug(format_string_t<Args...> fmt, Args &&...args) {  
// 调用默认 logger 的 debug 方法  
    default_logger_raw()->debug(fmt, std::forward<Args>(args)...);  
}

而这个默认的 logger 由 spdlog::registry 类管理。registry 类自身就是通过单例模式实现的,它使用 C++11 之后线程安全的“Meyers' Singleton”模式。

// spdlog/details/registry.h  
class SPDLOG_API registry {  
public:  
// ...  
// 提供一个静态方法 instance() 来获取唯一的实例  
static registry &instance();

    std::shared_ptr<logger> default_logger();  
    void set_default_logger(std::shared_ptr<logger> new_default_logger);  
    void register_logger(std::shared_ptr<logger> new_logger);  
    // ...  
private:  
    registry(); // 构造函数私有化  
    ~registry();

    std::unordered_map<std::string, std::shared_ptr<logger>> loggers_;  
    std::shared_ptr<logger> default_logger_;  
    // ...  
};

// spdlog/details/registry.cpp  
SPDLOG_INLINE registry &registry::instance() {  
    // C++11 保证了静态局部变量的初始化是线程安全的  
    static registry s_instance;  
    return s_instance;  
}

这样做有什么好处?

  • 便捷性: 为用户提供了极其方便的全局日志接口,大大降低了入门和日常使用的复杂度。
  • 全局注册与管理: 提供了一个中心化的位置来注册、获取和管理所有命名的 logger,方便统一配置。
  • 平衡了便利与灵活: spdlog 在提供单例便利的同时,也允许用户创建和管理自己的 logger 实例,不与全局注册表绑定。这种设计非常出色,兼顾了易用性和灵活性,避免了单例模式在大型项目中可能带来的强耦合和测试困难问题。

结论

通过对 spdlog 中设计模式的剖析,我们不难发现,这些经典模式并非孤立存在,而是相互协作,共同构成了 spdlog 优雅的架构:

  • 策略模式 提供了核心的灵活性,使得日志的格式化和输出行为可以被轻松替换和扩展。
  • 组合模式 使得 logger 能够统一、透明地处理单个或多个输出目的地,简化了客户端逻辑。
  • 工厂模式 (结合模板) 则封装了复杂的对象创建过程,为用户提供了简洁、易用的入口。
  • 单例模式 为简单的使用场景提供了极大的便利,并提供了一个全局的管理中心。
posted @ 2025-08-21 19:26  ToBrightmoon  阅读(27)  评论(0)    收藏  举报

© ToBrightmoon. All Rights Reserved.

Powered by Cnblogs & Designed with ❤️ by Gemini.

湘ICP备XXXXXXXX号-X