背景
在软件构建过程中,某些对象使用的算法可能多种多样,经常改变,如果将这些算法都编码到对象中,将会使对象变得异常复杂,而且有时候支持不使用的算法也是一个性能负担。 如何在运行时根据需要透明地更改对象的算法?将算法与对象本身解耦,从而避免上述问题?
我们还是使用这个例子,比如你要从北京去上海出差,出差的工作是不变的,但是每次去使用的交通工具却有不同的方式,可能有火车、可能飞机、可能开车。如果写程序实现,我们就可以分别将不同的交通工具方式写到不同的类中,然后再代码运行时根据不同的交通方式调用不同类的对象,从而实现在一套代码中通过运行是的调用兼容不同交通工具方式的出差这个场景。 这种定义一系列可互相替换的算法,并且每个算法独立封装,然后再运行的时候动态替换的方式就是策略模式。
定义
策略模式(Strategy Pattern)是一种常用的面向对象设计模式,它定义了一系列可互相替换的算法或策略,并将每个算法封装成独立的对象,使得它们可以在运行时动态地替换。
具体来说,策略模式定义了一系列算法,每个算法都封装在一个具体的策略类中,这些策略类实现了相同的接口或抽象类。在使用算法的时候,客户端通过一个上下文对象来调用策略类的方法,从而完成算法的执行。这样,客户端可以在运行时动态地选择不同的策略类,从而实现不同的行为。
策略模式通常由三个角色组成:
- 环境角色(Context):它是客户端代码所定义的一组接口或抽象类,表示一种算法或策略。
- 抽象策略角色(Strategy):它是一个抽象类或接口,定义了一组算法或策略的接口。
- 具体策略角色(Concrete Strategy):它是抽象策略的具体实现类,实现了策略角色定义的算法或策略。
应用场景
策略模式主要应用在以下场景:
- 当你想使用对象中各种不同的算法变体, 并希望能在运行时切换算法时, 可使用策略模式。
策略模式让你能够将对象关联至可以不同方式执行特定子任务的不同子对象, 从而以间接方式在运行时更改对象行为。
- 当你有许多仅在执行某些行为时略有不同的相似类时, 可使用策略模式。
策略模式让你能将不同行为抽取到一个独立类层次结构中, 并将原始类组合成同一个, 从而减少重复代码。
- 如果算法在上下文的逻辑中不是特别重要, 使用该模式能将类的业务逻辑与其算法实现细节隔离开来。
策略模式让你能将各种算法的代码、 内部数据和依赖关系与其他代码隔离开来。 不同客户端可通过一个简单接口执行算法, 并能在运行时进行切换。
- 当类中使用了复杂条件运算符以在同一算法的不同变体中切换时, 可使用该模式。
策略模式将所有继承自同样接口的算法抽取到独立类中, 因此不再需要条件语句。 原始对象并不实现所有算法的变体, 而是将执行工作委派给其中的一个独立算法对象。
具体的应用场景包括:
- 订单价格计算:根据不同的促销活动(如会员折扣、团购活动、满减优惠等),可以使用不同的算法来计算订单的价格。
- 支付方式选择:根据不同的支付方式(如信用卡、支付宝、微信支付等),可以使用不同的算法来完成支付过程。
- 排序算法选择:根据不同的排序算法(如冒泡排序、快速排序、归并排序等),可以使用不同的算法来排序。
- 表单验证:根据不同的表单验证规则(如必填字段、长度限制、格式要求等),可以使用不同的算法来验证表单数据的有效性。
在这些场景中,策略模式可以帮助我们将算法或策略的实现与算法或策略的使用分离开来,从而实现代码的复用、可维护性和灵活性。
模式结构
实现步骤
策略模式设的主要实现步骤:
- 定义一个策略接口或抽象类,该接口或抽象类声明了策略所需的方法,具体的策略类将实现这些方法。
- 定义具体的策略类,实现策略接口或抽象类中声明的方法。
- 定义一个上下文类,该类包含一个策略对象的引用,用于调用策略对象的方法。上下文类也可以定义一些辅助方法,用于支持策略的使用。
- 在客户端中,根据需要创建不同的策略对象,并将其传递给上下文对象。
- 在上下文对象中调用策略对象的方法,完成具体的算法或行为。
C语言代码示例
其实设计模式是一种与编程语言无关的设计思想,但是其中很重要的思想就是面向对象,所以在面向对象的语言,比如C++、Java中实现起来就非常顺手,但因为我本人是C语言出身,并且作为主要编程语言的,所以就使用了C语言来实现模板方法的设计模式。
在C语言中,由于没有面向对象的特性,策略模式的实现方式会略有不同。通常可以使用函数指针来模拟接口,使用函数指针变量来引用具体的策略函数。下面是一个简单的示例,演示了如何使用C语言实现策略模式:
#include <stdio.h>
// 1. 定义函数指针类型
typedef void (*Strategy)(int, int);
// 2. 定义具体的策略函数
void operation_add(int num1, int num2) {
printf("%d + %d = %d\n", num1, num2, num1 + num2);
}
void operation_subtract(int num1, int num2) {
printf("%d - %d = %d\n", num1, num2, num1 - num2);
}
void operation_multiply(int num1, int num2) {
printf("%d * %d = %d\n", num1, num2, num1 * num2);
}
// 3. 定义上下文结构体
typedef struct {
Strategy strategy;
} Context;
// 4. 在客户端中使用策略模式
int main() {
Context context;
// 使用加法策略
context.strategy = operation_add;
context.strategy(10, 5);
// 使用减法策略
context.strategy = operation_subtract;
context.strategy(10, 5);
// 使用乘法策略
context.strategy = operation_multiply;
context.strategy(10, 5);
return 0;
}
在上面的示例中,我们首先定义了一个函数指针类型 Strategy,该函数指针类型接受两个整型参数,并且没有返回值。接着定义了具体的策略函数 operation_add、operation_subtract 和 operation_multiply,这些函数都符合 Strategy 函数指针类型的定义。然后定义了上下文结构体 Context,该结构体包含一个函数指针类型的成员变量 strategy。最后,在 main 函数中创建一个上下文对象 context,并分别将不同的策略函数赋值给 context.strategy 成员变量,最终调用 context.strategy 函数指针来执行具体的策略函数。
需要注意的是,由于C语言没有面向对象的特性,策略模式的实现方式相对于其他编程语言会更加笨拙,代码可读性也不太好。如果需要实现更复杂的策略模式,建议使用其他面向对象的编程语言来实现。
总结
策略模式为组件提供了一系列可重用的算法,从而可以使得类型在运行时方便地根据需要在各个算法之间进行切换。并且策略模式提供了用条件判断语句以外的另一种选择,消除条件判断语句,就是在解耦合。含有许多条件判断语句的代码通常都需要策略模式。如果Strategy对象没有实例变量,那么各个上下文可以共享同一个Strategy对象,从而节省对象开销。
下面我们从设计原则的角度分析策略模式:
- 开闭原则:策略模式可以在不修改原有代码的情况下添加、删除、切换不同的策略,因此可以有效地满足开闭原则。
- 代码复用:不同的策略可以复用相同的代码,减少了代码冗余。
- 单一职责原则:将不同的算法封装在不同的策略中,实现了单一职责原则。
策略模式的主要优点在于:
- 提高了代码的复用性:将算法封装在独立的策略类中,可以避免代码重复,提高了代码的复用性。
- 易于扩展和维护:由于算法的实现与算法的使用分离开来,所以在添加新的算法或修改现有算法时,不会影响到其他算法的实现和使用,易于扩展和维护。
- 提高了代码的灵活性:在运行时动态地选择不同的策略类,可以实现不同的行为,提高了代码的灵活性。
总之,策略模式是一种简单而有效的设计模式,可以帮助我们提高代码的复用性、可维护性和灵活性。