1. 設計模式
設計模式是軟件開發中反復出現的問題的通用解決方案,它是一套套被反復使用、多數人知曉、經過分類編目的代碼設計經驗總結。
設計模式并非具體的代碼實現,而是針對特定問題的抽象設計思路和方法論。它描述了在特定場景下,如何組織類、對象、接口等元素,以解決常見的設計問題,比如如何降低代碼耦合度、提高復用性、增強可維護性等。
設計模式主要有以下幾個特點:
- 通用性:適用于不同的編程語言和應用場景,只要面臨相似的設計問題,就可以借鑒相應模式。
- 經驗性:是眾多開發者在長期實踐中總結出來的最佳實踐,經過了實踐的驗證。
- 抽象性:關注的是結構和交互關系,而非具體實現細節。
常見的設計模式分類(以經典的 GoF 設計模式為例):
- 創建型模式:用于處理對象創建機制,如單例模式(保證一個類僅有一個實例)、工廠模式(隱藏對象創建的細節)、建造者模式(分步構建復雜對象)等。
- 結構型模式:關注類和對象的組合方式,如適配器模式(使不兼容的接口能一起工作)、裝飾器模式(動態給對象添加功能)、代理模式(為對象提供代理以控制訪問)等。
- 行為型模式:描述對象之間的交互和職責分配,如觀察者模式(對象狀態變化時通知依賴它的對象)、策略模式(封裝不同算法,可動態切換)、迭代器模式(提供遍歷集合的統一接口)等。
合理使用設計模式可以讓代碼更具可讀性、靈活性和可擴展性,尤其在大型項目開發中,能幫助團隊形成共識,提高協作效率。但需注意,設計模式并非銀彈,不應過度使用,而應根據實際問題選擇合適的模式。
2. 策略模式
策略模式(Strategy Pattern)是一種行為型設計模式,它定義了一系列算法(或行為),并將每個算法封裝起來,使它們可以相互替換。這種模式讓算法的變化獨立于使用算法的客戶端,從而實現了算法的靈活切換和復用。
當一個問題有多種解決方案(算法),且這些方案可能隨需求變化時,策略模式可以避免使用大量的if-else或switch語句,使代碼更清晰、易維護。
策略模式的核心思想是分離算法的定義與使用:
- 將不同的算法(策略)封裝成獨立的類
- 客戶端通過統一接口調用不同的策略,無需關心具體實現
- 可以在運行時動態切換策略,而不影響客戶端代碼。
策略模式通常包含三個核心角色:
- 環境類(Context): 持有策略對象的引用;提供接口給客戶端使用,本身不實現具體算法;負責在運行時切換策略。
- 抽象策略接口(Strategy): 定義所有具體策略類的公共接口;聲明算法的核心方法。
- 具體策略類(Concrete Strategy): 實現抽象策略接口,提供具體的算法實現;可以有多個不同的具體策略類。
簡單來說,環境類負責目標功能的大部分實現,而有多種策略可選的核心算法部分則交由具體策略類來實現。環境類包含抽象策略接口類的引用(或指針),指向具體策略類,環境類通過該引用或者指針來調用具體策略類提供的具體算法。
3. 用策略模式設計日志類
我們的目標是輸出如下格式的日志:
[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] [程序對應的源文件] [行數] - 日志信息
其中,日志等級最常見的劃分方法是分為五個等級:
enum class LogLevel
{DEBUG, // 調試信息INFO, // 普通信息WARNNING, // 警告ERROR, // 錯誤(并未導致程序退出)FATAL // 致命(導致程序退出)
};
將日志輸出到什么地方?這是日志類最核心的可選策略部分,據此我們可以確定以下設計:
- 環境類:Log日志類,負責日志信息的生成與封裝。
- 抽象策略接口:LogStrategy日志策略類,負責提供Write接口用于輸出日志。
- 具體策略類:實現Write接口,將日志輸出到不同的目的地。
3.1 日志策略類
class LogStrategy
{
public:virtual ~LogStrategy() = default;virtual void Write(const std::string &message) = 0;protected:MutexModule::mutex _mutex;
};
這里的互斥鎖是為了保證在多線程環境下的互斥輸出,避免各個線程輸出的日志之間相互雜糅。
3.2 具體策略類
// 控制臺輸出策略
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 日志類
首先我們要明確日志類的大致框架:
- 成員變量:指向策略對象的指針(抽象類無法實例化,只能用其指針指向具體策略類)。
- 構造函數:初始化策略對象指針,默認采用控制臺輸出策略。
- 成員函數:設置策略的接口,完成日志輸出的接口。
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;
};
接下來,就差日志輸出的接口需要完成,在使用時,我們希望能達到如下效果:
#include <iostream>
#include "log.hpp"
using namespace LogModule;int main()
{Logger logger;logger.Log(LogLevel::DEBUG, __FILE__, __LINE__) << "hello" << " world!";
}
也就是支持可變參數,并用流插入的形式傳參。為了實現這一點,我們需要設計一個內部類,作為Log函數的返回值。
這個內部類需要重載 " << " 操作符,并在析構函數當中,將拼接起來的日志輸出:
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){_loginfo += message;return *this;}~Message(){_logger._strategy->Write(_loginfo);}private:Logger &_logger; // 引用外部logger類, 方便使用策略進行刷新std::string _loginfo; // 一條合并完成的,完整的日志信息
};
于是,我們可以這樣來設計Log函數:
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 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對象,并加入如下的宏:
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>())
使用示例:
#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 完整代碼
#pragma once
#include <iostream>
#include <fstream>
#include <memory>
#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, // 普通信息WARNNING, // 警告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, 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::WARNNING:return "WARNNING";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){_loginfo += message;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>())
}