在C++开发中,我们经常会遇到这样的场景:某个算法的整体结构是固定的,但其中某些步骤的具体实现可能因情况而异。这时候,模板方法模式(Template Method Pattern)就派上了大用场。这个设计模式允许我们在不改变算法结构的情况下,重新定义算法中的某些步骤。
我第一次真正理解这个模式的价值是在开发一个跨平台文件解析器时。当时需要处理不同格式的日志文件,但它们的解析流程其实大同小异——都是先读取文件头,然后解析内容,最后验证数据完整性。只有每个步骤的具体实现方式因文件格式而异。如果为每种格式都写一套完整流程,不仅重复劳动,而且后期维护简直是噩梦。
模板方法模式通过将不变的部分(算法骨架)放在基类中实现,而变化的部分(具体步骤)留给子类实现,完美解决了这个问题。这种"好莱坞原则"(Don't call us, we'll call you)的设计,让父类控制流程,子类只需关注自己需要改变的部分。
提示:模板方法模式属于行为型设计模式,与策略模式不同,它通过继承而非组合来改变部分算法行为。
一个典型的模板方法模式实现包含以下关键组件:
cpp复制class AbstractClass {
public:
// 模板方法,定义算法骨架
void TemplateMethod() {
PrimitiveOperation1();
PrimitiveOperation2();
// ... 其他操作
}
protected:
// 基本操作,由子类实现
virtual void PrimitiveOperation1() = 0;
virtual void PrimitiveOperation2() = 0;
// 钩子方法(可选),提供默认实现
virtual void Hook() {}
};
class ConcreteClass : public AbstractClass {
protected:
void PrimitiveOperation1() override {
// 具体实现
}
void PrimitiveOperation2() override {
// 具体实现
}
// 可选择性地覆盖钩子方法
void Hook() override {
// 自定义实现
}
};
模板方法(Template Method):
基本操作(Primitive Operations):
DoSomething()或OperationX()钩子方法(Hook Operations):
让我们通过一个更完整的例子来理解这个模式的实际应用。假设我们要开发一个支持多种格式的配置文件处理器。
cpp复制class ConfigFileParser {
public:
// 模板方法
void ParseFile(const std::string& filename) {
OpenFile(filename);
ValidateHeader();
ParseContent();
if (NeedPostProcessing()) {
PostProcess();
}
CloseFile();
}
virtual ~ConfigFileParser() = default;
protected:
// 基本操作
virtual void OpenFile(const std::string& filename) = 0;
virtual void ValidateHeader() = 0;
virtual void ParseContent() = 0;
// 钩子方法
virtual bool NeedPostProcessing() const { return false; }
virtual void PostProcess() {}
virtual void CloseFile() {}
// 公共工具方法
std::string ReadLine() {
// 实现读取一行数据的公共逻辑
}
};
cpp复制class JsonConfigParser : public ConfigFileParser {
public:
JsonConfigParser() : file_(nullptr) {}
protected:
void OpenFile(const std::string& filename) override {
file_ = fopen(filename.c_str(), "r");
if (!file_) throw std::runtime_error("Failed to open file");
}
void ValidateHeader() override {
auto line = ReadLine();
if (line != "{") throw std::runtime_error("Invalid JSON header");
}
void ParseContent() override {
// 具体JSON解析逻辑
}
void CloseFile() override {
if (file_) fclose(file_);
}
private:
FILE* file_;
};
class XmlConfigParser : public ConfigFileParser {
protected:
void OpenFile(const std::string& filename) override {
// XML特定的文件打开逻辑
}
void ValidateHeader() override {
// 验证XML声明
}
void ParseContent() override {
// XML解析逻辑
}
bool NeedPostProcessing() const override {
return true; // XML需要后处理
}
void PostProcess() override {
// XML特定的后处理
}
};
对于性能敏感的场合,我们可以使用奇异递归模板模式(CRTP)来实现编译期的模板方法:
cpp复制template <typename T>
class ConfigParserBase {
public:
void Parse(const std::string& filename) {
static_cast<T*>(this)->OpenFile(filename);
static_cast<T*>(this)->ParseImpl();
static_cast<T*>(this)->CloseFile();
}
};
class FastJsonParser : public ConfigParserBase<FastJsonParser> {
public:
void OpenFile(const std::string& filename) {
// 实现
}
void ParseImpl() {
// 实现
}
void CloseFile() {
// 实现
}
};
有时我们需要在运行时选择不同的算法变体,这时可以结合策略模式:
cpp复制class ParsingStrategy {
public:
virtual ~ParsingStrategy() = default;
virtual void Parse(ConfigFileParser& parser) = 0;
};
class StandardParsing : public ParsingStrategy {
void Parse(ConfigFileParser& parser) override {
parser.ParseFile("config.cfg");
}
};
class ConfigProcessor {
public:
void Process(std::unique_ptr<ParsingStrategy> strategy) {
strategy->Parse(*parser_);
}
private:
std::unique_ptr<ConfigFileParser> parser_;
};
过度继承问题:
灵活性不足:
违反LSP原则:
将虚函数调用内联化:
cpp复制class OptimizedParser {
public:
void Parse() {
// 非虚接口(NVI)模式
doParse();
}
private:
virtual void doParse() = 0;
};
使用final关键字:
cpp复制class FinalParser : public ConfigFileParser {
void ParseContent() final {
// 禁止子类进一步覆盖
}
};
缓存频繁使用的数据:
cpp复制class CachedParser : public ConfigFileParser {
protected:
void ParseContent() override {
if (cache_.empty()) {
BuildCache();
}
// 使用缓存数据
}
private:
std::vector<std::string> cache_;
};
测试模板方法类时,需要注意:
cpp复制TEST(ConfigFileParserTest, BasicFlow) {
MockParser parser;
EXPECT_CALL(parser, OpenFile(_));
EXPECT_CALL(parser, ValidateHeader());
EXPECT_CALL(parser, ParseContent());
parser.ParseFile("test.cfg");
}
cpp复制template <typename... Steps>
class GenericParser {
public:
void Parse() {
(Steps::Execute(), ...);
}
};
struct OpenStep {
static void Execute() { /* 打开文件 */ }
};
struct ParseStep {
static void Execute() { /* 解析内容 */ }
};
using MyParser = GenericParser<OpenStep, ParseStep>;
cpp复制class LambdaParser {
public:
template <typename F1, typename F2>
LambdaParser(F1&& open, F2&& parse)
: open_(std::forward<F1>(open)),
parse_(std::forward<F2>(parse)) {}
void Parse() {
open_();
parse_();
}
private:
std::function<void()> open_;
std::function<void()> parse_;
};
C++20引入了concept,可以更好地约束模板方法:
cpp复制template <typename T>
concept Parser = requires(T t) {
{ t.OpenFile(std::string{}) } -> std::same_as<void>;
{ t.ParseContent() } -> std::same_as<void>;
};
template <Parser T>
void ProcessFile(T& parser) {
parser.OpenFile("file");
parser.ParseContent();
}
与工厂方法模式:
与策略模式:
与观察者模式:
与命令模式:
在实际项目中,我经常将这些模式组合使用。比如在一个数据处理框架中,使用模板方法定义处理流程,用策略模式选择不同的算法实现,用工厂方法创建处理器实例,再用观察者模式监控处理进度。这种组合提供了既规范又灵活的设计方案。