C++高手进阶:从日志管理系统的封装看设计中对第三方库的隐藏

前言

上文中介绍了怎么在自己的项目中设计自己的日志管理系统。

没有看过的读者可以去看下原文:
》》》 C++高手进阶:如何设计自己的日志管理系统

但是实际上真实的业务开发中,开发者通常不期望去让其它依赖本模块的人知道自己是怎样设计的,特别是自己依赖了哪些第三方库。

那么,这就涉及到一个第三库隐藏的问题。即怎么隐藏自己的设计思路,只让被依赖方看到设计者想让他看到的,而隐藏掉不想让他看到的。

本文就此进行展开讲解下。

日志管理系统的封装

本文还是以前文提到的日志管理系统为例,来看下如何将第三方依赖库进行隐藏。

当我们没有对第三方库进行封装时,我们通常会在头文件中出现这些三方库的痕迹。如:

  • 包含了第三方头文件
  • 对第三方库中的类进行了前置声明
  • 在成员变量中包含了第三方库中的某些指针。

这些种种,如果是头文件自己用还好,如果将这种头文件暴露给其他模块或外部模块用,就太没代码品味了,同时有可能会为公司招来一些版权许可的麻烦

最好的就是做到外界不感知。

问题把脉

我们回到上一篇文章中,看看它有哪些问题。

这里我摘出代码片段看下:

class BASIC_TOOL_EXPORT Log {
public:
    enum class Level {
        INFO = 0,
        WARN,
        ERROR
    };

    static void init(Level level);
    static void uninit();

    // 获取日志存储路径及日志记录时间
    static void getLogInfos(std::string& logPath, std::string& timeStr);

    static void info(const char* format, ...);
    static void warn(const char* format, ...);
    static void error(const char* format, ...);

    static void info(const wchar_t* format, ...);
    static void warn(const wchar_t* format, ...);
    static void error(const wchar_t* format, ...);

    static void flush();

public:
    log();
    ~log();

public:
    spdlog::logger* m_pLogger = nullptr;
    std::string m_logPath;
};

在这个设计中,我们可以清楚的看到,它在成员basis中包含了spdloglogger指针。

对这种情况我们如何进行隐藏呢?

改造方案

通常比较好的做法是对 Log 类拆出接口类和实现类,将 spdlog::logger* m_pLogger = nullptr; 放到实现类中,这样就可以隐藏实现细节。

class BASIC_TOOL_EXPORT Log {
public:
    enum class Level {
        INFO = 0,
        WARN,
        ERROR
    };

    static void init(Level level);
    static void uninit();

    // 获取日志存储路径及日志记录时间
    static void getLogInfos(std::string& logPath, std::string& timeStr);

    static void info(const char* format, ...);
    static void warn(const char* format, ...);
    static void error(const char* format, ...);

    static void info(const wchar_t* format, ...);
    static void warn(const wchar_t* format, ...);
    static void error(const wchar_t* format, ...);

    static void flush();

public:
    log();
    ~log();
};
class LogImpl final
{
    using Type = Log::Level;
public:
    LogImpl();
    ~LogImpl();

    void init(Type type);
    void uninit();

    void output(const char* fmt, const va_list args, Type type);
    void output(const wchar_t* fmt, const va_list args, Type type);
    void output(const char* fmt, Type type);

    void flush();

    spdlog::logger* m_pLogger = nullptr;
    std::string m_logPath;
    std::string m_logTimeStr;
};

那么,通过这样的设计,就可以将第三方库的实现细节隐藏起来,只暴露接口。

那么,能否更进一步,将 class Log 中的构造函数和析构函数也隐藏起来,只保留需要暴露给用户使用的接口?

我们先回想下,我们最开始设计的时候准备怎么给用户使用?

我们设计的是,期望用户只包含头文件,就能够直接使用类似于 static void info(const char* format, ...); 这样的接口。

那么,我们如何才能做到这一点呢?

class Log 的构造函数、析构函数直接去掉,只保留静态函数。我们知道静态成员函数的调用不需要创建对象,即不依赖类的实例,因此可以直接调用。那就不需要构造函数和析构函数了。

那么实现类又是如何实现构造和析构的呢?类的实例又是什么时候被创建的呢?

通常日志的初始化时间是随着程序启动时,就初始化的,那时候就会调用 Log::init() 方法,来调用 LogImpl::init() 方法。并且整个程序运行的过程中,只有一个实现类的实例,能做到这点的有两种方式:

  1. 实现类采用单例模式,即保证只有一个实例。
  2. 创建静态实例,让这个对象的生命周期与程序的生命周期保持一致。

笔者在此采用第二种做法,即创建 static LogImpl g_logImpl;
这样保证:静态对象的构造发生在控制权传递给 main 函数之前,且只发生一次;静态对象的析构发生在 main 函数执行完成之后。

我们在来看下改造之后的代码:

class BASIC_TOOL_EXPORT Log {
public:
    enum class Level {
        INFO = 0,
        WARN,
        ERROR
    };

    static void init(Level level);
    static void uninit();

    // 获取日志存储路径及日志记录时间
    static void getLogInfos(std::string& logPath, std::string& timeStr);

    static void info(const char* format, ...);
    static void warn(const char* format, ...);
    static void error(const char* format, ...);

    static void info(const wchar_t* format, ...);
    static void warn(const wchar_t* format, ...);
    static void error(const wchar_t* format, ...);

    static void flush();
};
class LogImpl
{
    using Type = Log::Level;
public:
    LogImpl();
    ~LogImpl();

    void init(Type type);
    void uninit();

    void output(const char* fmt, const va_list args, Type type);
    void output(const wchar_t* fmt, const va_list args, Type type);
    void output(const char* fmt, Type type);

    void flush();

    spdlog::logger* m_pLogger = nullptr;
    std::string m_logPath;
    std::string m_logTimeStr;
};

static LogImpl g_logImpl; 
void Log::init(Level level)
{
    g_logImpl.init(level);
}

void Log::uninit()
{
    flush();
    g_logImpl.uninit();
}

void Log::flush()
{
    g_logImpl.flush();
}

void Log::getLogInfos(std::string& logPath, std::string& logTimeStr)
{
    logPath = g_logImpl.m_logPath;
    logTimeStr = g_logImpl.m_logTimeStr;
}

void Log::info(const char* fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    g_logImpl.output(fmt, args, Log::Level::INFO);
    va_end(args);
}

void Log::warn(const char* fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    g_logImpl.output(fmt, args, Log::Level::WARN);
    va_end(args);
}


void Log::error(const char* fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    g_logImpl.output(fmt, args, Log::Level::ERROR);
    va_end(args);
}

void Log::info(const wchar_t* fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    g_logImpl.output(fmt, args, Log::Level::INFO);
    va_end(args);
}

void Log::warn(const wchar_t* fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    g_logImpl.output(fmt, args, Log::Level::WARN);
    va_end(args);
}

void Log::error(const wchar_t* fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    g_logImpl.output(fmt, args, Log::Level::ERROR);
    va_end(args);
}

这样,整个层次就清晰许多了。

不过,还是有点瑕疵:
就是用户使用的时候是这样的:

// example
#include "log.h"

void func()
{
    ...
    Log::info("This is a info log");
    Log::info("This is a info log: %.3f, %.3f, %.3f", data.x, data.y, data.z);
    Log::error("This is a error log");
    ...
}

这样写,用户需要知道 Log 类,才能使用 infowarnerror 等接口。本着怎么好用,怎么设计的原则,我不期望用户感知到或是过多关注到 Log 这个类,那么我们开启对它的再次封装:

#define LOG_INFO(...) Log::info(__VA_ARGS__)
#define LOG_WARN(...) Log::warn(__VA_ARGS__)
#define LOG_ERROR(...) Log::error(__VA_ARGS__)

#define LOG_ERROR_LOCATION Log::error("The error occurred in the line: %d, file : %s", __LINE__, __FILE__)

#define LOG_FLUSH Log::flush()

我们用宏来代替可变参函数,这样用户只需要关注 LOG_INFOLOG_WARNLOG_ERROR 这几个宏即可。

其中涉及到可变参的技巧知识,如果不了解,可以参考我之前的文章:
》》 C++可变参技巧揭秘:从函数到模板,一网打尽

这样,用户只需要包含 log.h 头文件,就可以使用 LOG_INFOLOG_WARNLOG_ERROR 这几个宏来输出日志了。

// example
#include "log.h"

void func()
{
    ...
    LOG_INFO("This is a info log");
    LOG_INFO("This is a info log: %.3f, %.3f, %.3f", data.x, data.y, data.z);
    LOG_ERROR("This is a error log");
    ...
}

这样用户使用起来就简单了,而且隐藏了 Log 类的实现细节,达到了用户只能看到设计者想让他看到的目的。

总结

我们通过对日志管理系统的封装,将第三方库的实现细节隐藏起来,只暴露接口。

同时,我们又通过宏来简化用户的使用,让用户只关注 LOG_INFOLOG_WARNLOG_ERROR 这几个宏即可。

至此改造完成!

大家如果有不清楚的,欢迎在评论区交流。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值