模板方法模式是设计模式中的一种非常简单,应用也较为广泛的设计模式。其基本的思想和意图是,预先定义好一些算法框架,并把算法的具体实现延迟到子类。也就是说,我们可以使用父类来定义算法的步骤,但是每一步具体如何实现我们并不关心。当我们要实现具体的算法时,让子类继承父类,在子类中重写对应的步骤函数即可。
我们通过一个例子再来理解下这种模式的使用场景,例如:在 NLP 任务中,我们经常需要对文本预料进行一些列步骤的预处理,从而使得数据能够满足模型的训练要求。假设,我们需要对数据的预处理流程进行定义,便于其他开发者能够统一处理过程,就可以将该处理过程使用模型方法模式进行定义。比如,我们进行了下如下定义:
// 文本预处理流程定义 class TextPreprocessing { protected: // 第一步:获得文本 virtual void get_text() = 0; // 第二步:清洗文本 virtual void clean_text() = 0; // 第三步:数据存储 virtual void save_text() = 0; public: // 定义算法流程 void start_preprocessing() { this->get_text(); this->clean_text(); this->save_text(); } };
上述代码中,TextPreprocessing 类可以叫做算法模板类,该类中的 start_preprocessing 成员函数叫做模板方法,该函数的作用就是用来定义算法流程。当然,该函数的定义是灵活的,并不一定是我上面举例的方式方式,有时我们调用的每个步骤都有参数和返回值,并且上一步骤的返回值可能会作为下一个步骤函数的参数,这些行为都可以根据自己的需要来定义模板方法。
另外,细心的同学发现我将具体步骤实现函数 get_text、clean_text、save_text 都定义为了纯虚函数。这是由于模板类只定义算法基本框架,并不关心算法的具体实现。具体实现的工作将会由其子类根据具体场景来实现。所以,我们经常也说模板方法模式将其具体的实现延迟到子类来实现。
注意:包含虚函数的父类叫做抽象类,这是由于一个父类如果包含了虚函数,意味着大部分场景下,该函数会被子类重写,其行为并不是固定的,而是会随着不同的场景其实现也不同。所以,叫做抽象类。
假设:我们将要根据当前的具体场景来实现对文本数据进行预处理,就可以定义一个具体的子类来实现父类中的纯虚函数(即:具体算法步骤的实现),如下代码所示:
// 针对场景1的具体实现 class SceneOne : public TextPreprocessing { public: SceneOne() { std::cout << "子类 SceneOne 初始化函数" << std::endl; } protected: // 第一步:获得文本 virtual void get_text() { std::cout << "子类 SceneOne 函数 get_text 具体实现" << std::endl; } // 第二步:清洗文本 virtual void clean_text() { std::cout << "子类 SceneOne 函数 clean_text 具体实现" << std::endl; } // 第三步:数据存储 virtual void save_text() { std::cout << "子类 SceneOne 函数 save_text 具体实现" << std::endl; } }; // 针对场景2的具体实现 class SceneTwo : public TextPreprocessing { public: SceneTwo() { std::cout << "子类 SceneTwo 初始化函数" << std::endl; } protected: // 第一步:获得文本 virtual void get_text() { std::cout << "子类 SceneTwo 函数 get_text 具体实现" << std::endl; } // 第二步:清洗文本 virtual void clean_text() { std::cout << "子类 SceneTwo 函数 clean_text 具体实现" << std::endl; } // 第三步:数据存储 virtual void save_text() { std::cout << "子类 SceneTwo 函数 save_text 具体实现" << std::endl; } }; void test() { TextPreprocessing *one = new SceneOne; one->start_preprocessing(); TextPreprocessing *two = new SceneTwo; two->start_preprocessing(); delete one, two; }
子类 SceneOne 、SceneTwo 集成类父类,仅仅只需要重写父类的虚函数即可,不需要重写模板方法函数。如果你的算法步骤在某些特殊场景下需要修改,那么可以定义一个带有默认实现的虚模板方法函数。
需要注意的是,子类中并不是只能写父类的虚函数,也可以自定义其他函数。例如:必要的构造函数、必要的其他的业务函数。
最后,总结下该模式的使用场景:
- 一次性实现一个算法的不变部分,并将可变的行为留给子类来实现。
- 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。