从LSP到模板方法模式:模板方法如何保证子类行为一致,好莱坞原则

大家好,我是你们的老朋友。

今天我们来聊一个很有意思的话题——模板方法模式。说实话,这个模式是我个人非常喜欢的一个设计模式。为什么?因为它简单,却解决了一个很核心的问题:如何让子类在扩展功能的同时,不破坏父类定义的整体流程

嗯,我们先从里氏替换原则(LSP)说起。

里氏替换原则:子类不能“背叛”父类

里氏替换原则,说白了就是一句话:父类能用的地方,换成子类也应该能用。听起来很简单对吧?但我在项目中见过太多违反这个原则的代码了。

举个例子:

class Bird {
public:
    virtual void fly() {
        cout << "I can fly" << endl;
    }
};

class Penguin : public Bird {
public:
    void fly() override {
        throw runtime_error("Penguin can't fly!");
    }
};

你看,企鹅继承了鸟,但企鹅不会飞。如果你写一个函数:

void letBirdFly(Bird& bird) {
    bird.fly();
}

传入企鹅对象,程序就崩了。这就是典型的违反LSP。

我曾经在一个金融项目中遇到过类似问题。一个基类定义了“计算利息”的接口,结果某个子类直接抛异常说“不支持该操作”。那一次,整个系统的回滚让我记忆犹新。

那么,模板方法模式是怎么解决这个问题的呢?

模板方法模式:好莱坞原则

模板方法模式的核心思想,就是好莱坞原则——"Don't call us, we'll call you"。

什么意思?父类定义好整个算法的骨架,子类只需要实现某些步骤。父类控制流程,子类不能反过来调用父类。

我画了一张图,帮你理解这个关系:

AbstractClass(父类) templateMethod() { step1(); // 抽象方法 step2(); // 具体方法 step3(); // 钩子方法 } ConcreteClassA step1() { ... } step3() { ... } ConcreteClassB step1() { ... } step3() { ... } 好莱坞原则 父类控制流程 子类只实现细节 子类不能调用父类 Don't call us, we'll call you. —— 好莱坞原则

模板方法模式的经典结构

我们来看一个实际的代码例子。假设我们要做一个数据导出功能:

class DataExporter {
public:
    // 模板方法:定义导出流程
    void exportData() {
        openFile();
        writeHeader();
        writeBody();
        writeFooter();
        closeFile();
    }

    virtual ~DataExporter() = default;

protected:
    void openFile() {
        cout << "Opening file..." << endl;
    }

    void closeFile() {
        cout << "Closing file..." << endl;
    }

    // 抽象方法:子类必须实现
    virtual void writeHeader() = 0;
    virtual void writeBody() = 0;
    virtual void writeFooter() = 0;
};

class CsvExporter : public DataExporter {
protected:
    void writeHeader() override {
        cout << "Name,Age,City" << endl;
    }

    void writeBody() override {
        cout << "Alice,30,NYC" << endl;
        cout << "Bob,25,LA" << endl;
    }

    void writeFooter() override {
        cout << "Total: 2 records" << endl;
    }
};

class JsonExporter : public DataExporter {
protected:
    void writeHeader() override {
        cout << "[" << endl;
    }

    void writeBody() override {
        cout << "  {\"name\":\"Alice\",\"age\":30}," << endl;
        cout << "  {\"name\":\"Bob\",\"age\":25}" << endl;
    }

    void writeFooter() override {
        cout << "]" << endl;
    }
};

关键点:父类的 exportData() 是模板方法,它定义了“打开-写入-关闭”这个不可变的流程。子类只能改变“写入”的具体内容,不能改变流程本身。

钩子方法:给子类一点“选择权”

有时候,我们想让子类在某些步骤上有选择权。比如,有些导出格式需要加密,有些不需要。这时候可以用钩子方法

class DataExporter {
public:
    void exportData() {
        openFile();
        writeHeader();
        writeBody();
        writeFooter();
        if (needEncryption()) {  // 钩子方法
            encryptFile();
        }
        closeFile();
    }

protected:
    virtual bool needEncryption() {
        return false;  // 默认不加密
    }

    virtual void encryptFile() {
        // 默认实现为空
    }
    // ... 其他方法
};

class SecureCsvExporter : public CsvExporter {
protected:
    bool needEncryption() override {
        return true;
    }

    void encryptFile() override {
        cout << "Encrypting file with AES-256..." << endl;
    }
};

你看,钩子方法让子类可以“选择”是否执行某个步骤,但流程控制权仍然在父类手里。

我个人习惯:钩子方法的名字最好以 shouldneedcan 开头,这样读代码的人一眼就能看出这是个条件判断。

模板方法模式如何保证LSP?

回到我们最开始的问题。模板方法模式为什么能保证子类行为一致?

原因很简单:父类把流程固定死了。子类只能实现某些步骤,不能改变步骤的顺序,也不能跳过关键步骤。

你想想看,如果子类想“背叛”父类,它能做什么?它最多在某个步骤里返回一个空值,或者抛个异常。但流程本身——打开、写入、关闭——它改不了。

这就保证了:任何子类对象,调用 exportData() 的行为都是一致的。不会出现“这个子类先关闭再打开”这种奇葩情况。

对比项 普通继承 模板方法模式
流程控制 子类可重写整个方法 父类固定流程
LSP符合度 低(子类容易破坏) 高(流程不可变)
扩展方式 重写方法 实现抽象方法/钩子
代码复用 高(公共代码在父类)

实际项目中的避坑指南

我在实际项目中用过很多次模板方法模式,也踩过一些坑。这里分享几个经验:

  • 不要滥用钩子方法。钩子太多,子类的行为就不可控了。我一般控制在1-2个钩子。
  • 模板方法尽量用 final 关键字(C++11以后)。防止子类不小心重写了模板方法本身。
  • 抽象方法不要太多。3-5个抽象方法比较合适。太多了,子类实现起来很痛苦。
  • 注意命名规范。模板方法名用动词短语,比如 processOrderbuildReport。抽象方法名用 doXxx 前缀,比如 doValidatedoSave

我曾经在一个项目中,把模板方法设计成了虚函数,结果某个新来的同事直接重写了整个模板方法,把流程改得面目全非。从那以后,我所有模板方法都加上了 final

总结

模板方法模式,说白了就是:父类定规矩,子类填细节

它和里氏替换原则的关系很微妙。LSP要求子类不能破坏父类的行为,而模板方法模式通过固定流程,从设计上就杜绝了这种破坏。两者相辅相成。

好莱坞原则听起来很酷,但核心思想很简单:别打电话给我们,我们会打给你。父类掌握控制权,子类安心做自己的事就好。

嗯,今天就聊到这里。记住,设计模式不是银弹,但模板方法模式在处理“固定流程+可变步骤”的场景下,确实是个好选择。


专注资料整理