设计模式综合案例1:设计一个日志系统(外观+策略+责任链)
日志系统,说白了就是给程序装个「黑匣子」。我做了这么多年C++开发,几乎每个项目都离不开它。但有意思的是,很多人写日志就是简单调个printf,等到线上出问题要排查时,才发现日志要么太多刷爆磁盘,要么太少啥也没记下来。
今天咱们就动手搭一个像样的日志系统。用三个设计模式组合起来:外观模式统一接口,策略模式支持多种输出方式,责任链模式控制日志级别过滤。嗯,这套组合拳我在好几个生产项目里都用过,效果不错。
先看看整体架构
我习惯先画一张图,把各个模块的关系理清楚。这样写代码时心里有数,不会东一榔头西一棒子。
这张图你看懂了吗?客户端只跟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()) {}
};
file和line带上,排查问题时能直接定位到代码行。我习惯用宏来封装调用,比如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及以上。链的顺序决定了过滤的严格程度。
第四步:外观模式——统一入口
外观模式,就是给客户端一个简单的门面。客户端不需要知道内部有责任链、有策略,只需要调用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,改一个地方牵一发动全身。
好了,这个案例就到这。代码你可以直接拿去用,但建议根据实际场景调整。毕竟,没有银弹,只有适合的解决方案。