【设计模式】 模板方法模式介绍及C代码实现
背景
在软件构建过程中,对于某一项任务,它常常有稳定的整体操作结构,但各个子步骤却有很多改变的需求,或者由于固有的原因(比如框架与应用之间的关系)而无法和任务的整体结构同时实现。
比如你要从北京去上海出差,出差的工作是不变的,但是使用的交通工具却有不同的方式,可能有火车、可能飞机、可能开车。如果写程序实现,则每次都要写一个不同的类,并且类中实现功能几乎一样,大量重复的逻辑,相信你已经闻到这里面的一些坏味道了。 这种整体功能稳定,但是子步骤经常改变的需求,就可以使用模板方法设计模式来优化。
定义
那什么是模板方法设计模式?
模板方法设计模式:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。这样就使得子类可以不改变一个算法的结构即可重定义(override 重写)该算法的某些特定步骤。
模板方法模式的主要思想是基于“好莱坞原则”,即“不要打电话给我们,我们会打电话给你”。这意味着在模板方法模式中,父类定义了一个算法框架,但是具体的实现由子类决定。子类可以通过继承父类,并重写父类的某些方法来实现自己的具体实现。
模板方法模式通常由两个部分组成:抽象父类和具体子类。抽象父类定义了一个算法框架,其中包含了一些抽象方法和具体方法。抽象方法由子类实现,具体方法由父类实现。具体子类通过重写抽象方法来实现自己的具体实现,从而完成整个算法。
应用场景
模板方法设计模式主要应用在以下场景:
-
当你只希望客户端扩展某个特定算法步骤, 而不是整个算法或其结构时, 可使用模板方法模式。模板方法将整个算法转换为一系列独立的步骤, 以便子类能对其进行扩展, 同时还可让超类中所定义的结构保持完整。
-
当多个类的算法除一些细微不同之外几乎完全一样时,可以使用该模式。 但其后果就是, 只要算法发生变化, 你就可能需要修改所有的类。在将算法转换为模板方法时, 你可将相似的实现步骤提取到超类中以去除重复代码。 子类间各不同的代码可继续保留在子类中。
模式结构
抽象类 (AbstractClass) 会声明作为算法步骤的方法, 以及依次调用它们的实际模板方法。 算法步骤可以被声明为 抽象类型, 也可以提供一些默认实现。
具体类 (ConcreteClass) 可以重写所有步骤, 但不能重写模板方法自身。
实现步骤
模板方法设的主要实现步骤:
-
分析目标算法, 确定能否将其分解为多个步骤。 从所有子类的角度出发, 考虑哪些步骤能够通用, 哪些步骤各不相同。
-
创建抽象基类并声明一个模板方法和代表算法步骤的一系列抽象方法。 在模板方法中根据算法结构依次调用相应步骤。 可用 final最终修饰模板方法以防止子类对其进行重写。
-
虽然可将所有步骤全都设为抽象类型, 但默认实现可能会给部分步骤带来好处, 因为子类无需实现那些方法。
-
可考虑在算法的关键步骤之间添加钩子。
-
为每个算法变体新建一个具体子类, 它必须实现所有的抽象步骤, 也可以重写部分可选步骤。
C语言代码示例
其实设计模式是一种与编程语言无关的设计思想,但是其中很重要的思想就是面向对象,所以在面向对象的语言,比如C++、Java中实现起来就非常顺手,但因为我本人是C语言出身,并且作为主要编程语言的,所以就使用了C语言来实现模板方法的设计模式。
在C语言中,我们可以通过函数指针和结构体来实现模板模式。下面是一个示例:
#include <stdio.h>
// 抽象类,定义算法框架
typedef struct {
void (*step1)(void);
void (*step2)(void);
void (*step3)(void);
void (*run)(void);
} Algorithm;
// 具体子类,实现具体步骤
typedef struct {
Algorithm super;
int data;
} MyAlgorithm;
void my_algorithm_step1(void) {
MyAlgorithm* my_algorithm = (MyAlgorithm*)Algorithm_GetThis();
printf("MyAlgorithm Step 1 with data %d\n", my_algorithm->data);
}
void my_algorithm_step2(void) {
MyAlgorithm* my_algorithm = (MyAlgorithm*)Algorithm_GetThis();
printf("MyAlgorithm Step 2 with data %d\n", my_algorithm->data);
}
void my_algorithm_step3(void) {
MyAlgorithm* my_algorithm = (MyAlgorithm*)Algorithm_GetThis();
printf("MyAlgorithm Step 3 with data %d\n", my_algorithm->data);
}
void my_algorithm_run(void) {
Algorithm* algorithm = Algorithm_GetThis();
algorithm->step1();
algorithm->step2();
algorithm->step3();
}
// 定义抽象类的构造函数
Algorithm* Algorithm_Create(void) {
Algorithm* algorithm = (Algorithm*)malloc(sizeof(Algorithm));
algorithm->step1 = NULL;
algorithm->step2 = NULL;
algorithm->step3 = NULL;
algorithm->run = NULL;
return algorithm;
}
// 定义具体子类的构造函数
MyAlgorithm* MyAlgorithm_Create(int data) {
MyAlgorithm* my_algorithm = (MyAlgorithm*)malloc(sizeof(MyAlgorithm));
my_algorithm->super.step1 = my_algorithm_step1;
my_algorithm->super.step2 = my_algorithm_step2;
my_algorithm->super.step3 = my_algorithm_step3;
my_algorithm->super.run = my_algorithm_run;
my_algorithm->data = data;
return my_algorithm;
}
// 定义获取this指针的函数,用于将函数指针转换为正确的类型
Algorithm* Algorithm_GetThis(void) {
// 这里假设调用者已经将this指针保存在全局变量中
// 实际使用时应该根据具体情况修改这个函数的实现
return (Algorithm*)this;
}
int main() {
// 创建一个MyAlgorithm的实例
MyAlgorithm* my_algorithm = MyAlgorithm_Create(42);
// 调用run函数运行算法
this = (void*)my_algorithm; // 将this指针保存在全局变量中
my_algorithm->super.run();
// 释放资源
free(my_algorithm);
return 0;
}
在这个例子中,我们定义了一个抽象类 Algorithm,它包含了算法的框架。step1、step2、step3和run这些成员变量都是函数指针,用来定义算法的具体步骤。
我们还定义了一个具体子类 MyAlgorithm,它继承了 Algorithm,并实现了具体的步骤。这里我们添加了一个 data 成员变量,用于在每个步骤中输出一些信息。
在具体子类的构造函数 MyAlgorithm_Create 中,我们将 MyAlgorithm 的各个成员变量初始化为具体的函数实现。在 run 函数中,我们按照 Algorithm 的算法框架依次调用各个步骤。
最后,在 main 函数中,我们创建了一个 MyAlgorithm 的实例,并调用了它的 run 函数运行算法。注意到我们使用了一个全局变量 this 来保存当前的 this 指针,用于在每个步骤中获取 MyAlgorithm 实例的成员变量。实际使用时,我们应该根据具体情况修改这个实现。
可以说模板方法模式是在开发过程中还是非常常见并且有用的,它可以让我们轻松地定义算法框架,并将具体的实现延迟到子类中。在 C 语言中,我们主要可以使用函数指针和结构体来实现模板模式。
总结
模板方法模式的优点在于可以封装算法的骨架,让子类专注于具体实现细节。这样可以使得代码具有更好的可维护性、可读性和可扩展性。此外,模板方法模式还能够在不改变算法框架的情况下,扩展算法的功能,从而满足不同的业务需求。
比如你可以只重写工程中一个大型算法中的特定部分,而不修改其他部分,使得算法其他部分修改对其所造成的影响减小。也可以将重复代码提取到一个超类中。
总之,模板方法模式是一种非常实用的设计模式,它可以让我们轻松地定义算法框架,并将具体的实现延迟到子类中。这种模式可以提高代码的重用性和可维护性,是面向对象编程中必不可少的一种设计思想。