这是【Dart 教程系列第 49 篇】,如果觉得有用的话,欢迎关注专栏。
博文当前所用 Flutter SDK:3.22.1、Dart SDK:3.4.1
文章目录
- 一:什么是策略设计模式?
- 二:为什么要使用策略设计模式?(举例说明)
- 三:如何使用策略设计模式?(举例说明)
- 3-1:定义抽象策略角色 Strategy
- 3-2:实现具体的策略角色 Concrete Strategy
- 3-3:创建环境角色 Context
- 3-4:创建特定策略对象,并将其传递给环境角色 Context
- 四:策略模式的优缺点
- 五:策略模式的其它应用
一:什么是策略设计模式?
策略设计模式是行为型设计模式之一,它在 Gof Book 书中的描述如下:
在计算机编程中,策略模式是一种行为软件设计模式,允许在运行时选择算法。代码不是直接实现单个算法,而是接收运行时指令,决定使用哪一组算法。
标准策略模式的 UML 如下图所示
由上图可以看出
- Strategy(策略)- 也可以叫
抽象策略角色
,用以声明一个支持所有算法的接口,并通过 Context 来执行特定策略的方法; - Concrete Strategy(具体策略)- 也可以叫
具体策略角色
,使用 Strategy 接口实现不同的算法。Context 只是使用这个接口,并不关心算法的具体实现; - Context(上下文)- 也可以叫
环境角色
,保存对 Strategy 对象的引用,但不依赖于算法的实现方式。
最后由客户端创建一个特定的策略对象,并将其传递给 Context 即可。
二:为什么要使用策略设计模式?(举例说明)
策略模式允许在运行时选择算法或行为,将算法的使用和实现分离,提高系统的灵活性和可扩展性。
举例说明:
在举例策略模式之前,我们先来看一下,同样的需求,如果不使用策略模式,而是使用一般的 if…else 条件语句来处理会有什么问题。
某视频剪辑类 APP 提供了不同的工具如链接转文字、视频转文字、智能配音和去水印等功能,APP 刚上线为吸引更多的用户,所以允许用户免费使用这些工具。因为目前只有一种免费的支付方式,此时后端定义的获取订单号接口只需要传入工具的 id 就可以了,所以你的代码可能是这样写的。
... tag1
/// 支付状态
Future<bool> payStatus(int toolId) async {// 创建工具订单号final String orderNo = await getToolsOrder(toolId: toolId);// 根据订单号后等待工具处理的结果final bool res = await doSomething(orderNo);return res;
}
后来用户量上来了,APP 内对使用工具做了以下调整。将原先的免费使用工具更改为每天可免费使用某工具一定的次数,免费次数用完后再使用工具需要通过观看广告后才可以。现在的支付方式增加到了两种,为此你定义了一个支付类型的枚举
/// 支付类型
enum PayOrderType {free, // 免费ad, // 看广告
}
后端提供的获取订单号接口也增加了一个支付类型的入参,只针对免费支付类型而言,此时 tag1 代码需要做如下调整
... tag2
Future<bool> payStatus(int toolId, PayOrderType payOrderType) async {if (payOrderType == PayOrderType.free) {final String orderNo = await getToolsOrder(toolId: toolId, payOrderType: PayOrderType.free);final bool res = await doSomething(orderNo);return res;}... 暂时省略部分代码return false;
}
可以对比下 tag1 和 tag2 的代码修改了哪里,不知道你发现什么问题了没,如果没有也没关系,我们继续往下看。
如果是看广告类型的支付方式,调用第三方广告 SDK 时,要求需要传入广告位的标识 key,为此 tag2 的代码不得不再增加一个入参 positionKey,并增加调用观看广告的方法以及是否观看完的判断,如下代码所示。
... tag3
Future<bool> payStatus(int toolId, PayOrderType payOrderType, String positionKey) async {if (payOrderType == PayOrderType.free) {final String orderNo = await getToolsOrder(toolId: toolId, payOrderType: PayOrderType.free);final bool res = await doSomething(orderNo);return res;}else if (payOrderType == PayOrderType.ad) {final String orderNo = await getToolsOrder(toolId: toolId, payOrderType: PayOrderType.ad);// 看广告final ADResult adRes = await openRewardAD(positionKey);if (adRes.code != 1) {return false;}final bool res = await doSomething(orderNo);return res;}return false;
}
这样写也能实现需求,但不知道你发现一个问题没,随着支付方式的增加,payStatus 方法就会不断的根据实际情况增加入参并实现相关支付方式的代码,且不断的增加新的 else if 判断,这违背了六大设计原则之一的 OCP(Open Close Principle)开放封闭原则,也就是对扩展开放,但对修改关闭
。
我们应该需要这么一种设计理念,当后面再增加新的需求时,应该是在不变动当前正常运行的代码下,通过其他方式新增代码实现新需求。如果为了新需求而改动原有代码,可能会造成其他调用原本代码的地方发生预期之外的错误。
此时,我们的主角策略模式,终于要闪亮登场了。
三:如何使用策略设计模式?(举例说明)
基于目录二的需求,我们通过观察发现,无论是哪种支付方式,我们都是先根据传入的工具 id 创建订单号,然后在工具处理完成后返回结果,所以可以把这个行为抽象出来一个接口,也就是使用策略模式的第一步。
3-1:定义抽象策略角色 Strategy
/// 策略公共接口
abstract class IPayStrategy {Future<bool> payStatus(int toolId);
}
3-2:实现具体的策略角色 Concrete Strategy
对于目录二的免费支付方式而言,具体的实现如下代码所示
/// 免费支付策略
final class PayStrategyByFree implements IPayStrategy {@overrideFuture<bool> payStatus(int toolId) async {final String orderNo = await getToolsOrder(toolId: toolId, payOrderType: PayOrderType.free);final bool res = await doSomething(orderNo);return res;}
}
对于目录二的看广告支付方式而言,具体的实现如下代码所示
/// 看广告支付策略
final class PayStrategyByAD implements IPayStrategy {final String positionKey; // 广告位标识 keyPayStrategyByAD(this.positionKey);@overrideFuture<bool> payStatus(int toolId) async {final String orderNo = await getToolsOrder(toolId: toolId, payOrderType: PayOrderType.ad);// 看广告final ADResult adRes = await openRewardAD(positionKey);if (adRes.code != 1) {return false;}final bool res = await doSomething(orderNo);return res;}
}
可以看出,看广告时所需的广告位标识 key,由支付策略类的构造函数传入。
3-3:创建环境角色 Context
抽象策略和具体策略都已实现,现在创建环境角色 Context,其持有对抽象策略的引用,并决定使用哪种策略。
/// 支付上下文,持有一个策略对象的引用
class PayContext {final IPayStrategy iPayStrategy;PayContext({required this.iPayStrategy});Future<bool> getPayStatus(int toolId) async {return iPayStrategy.payStatus(toolId);}
}
3-4:创建特定策略对象,并将其传递给环境角色 Context
一切的铺垫都已完成,光说不练假把式,现在就让我们把策略模式应用在支付方式上吧。
// 工具是否免费
bool isFree = false;
// 支付策略
late IPayStrategy strategy;
// 免费的支付策略
if (isFree) {strategy = PayStrategyByFree();
}
// 看广告的支付策略
else {strategy = PayStrategyByAD();
}
// 支付状态(传入具体的支付策略)
final bool success = await PayContext(iPayStrategy: strategy).getPayStatus(123456);
// ... 根据支付状态处理后续业务
这里首先根据条件创建了不同的策略对象,然后把策略对象传递给了环境角色 Context,由环境角色 Context 负责调用具体的策略方法。后面如果再增加其他的支付方式的话,只需要再声明一个策略类并实现具体的算法即可。相比较使用 if…else 的条件语句来说,对外提供了扩展,对内又限制修改,符合 OCP 原则。
至此,关于什么是策略模式以及如何使用策略模式便介绍到这里了。
四:策略模式的优缺点
没有最好的设计模式,只用相对合适的设计模式。策略模式的优缺点如下
优点:
- 算法可以自由切换
- 避免使用了多重条件判断
- 扩展性良好
缺点:
- 策略类会逐渐增多
- 所有策略类都需要对外暴露
五:策略模式的其它应用
除了本文举例的支付方式可以使用策略模式外,符合策略模式定义和使用场景的都可以使用该模式。如
- 支付选项策略。如支付类型是使用微信、支付宝、信用卡还是银行转账等支付类型。
- 游戏伤害计算。如使用不同的招式,技能,为不同的攻击定义不同的算法。
- 排序算法。如把不同的算法(冒泡排序、快排排序、选择排序等)通过不同的策略类实现,最终调用公共的 Sort 接口。
- 电商平台优惠卷系统。如不同的优惠卷有不同的使用策略,例如满减、打折、买赠等方式。
- …
还有很多其它案例就不一一说明了,知道什么时候该用策略模式以及如何使用策略模式即可,以不变应万变。
你的问题得到解决了吗?欢迎在评论区留言。
赠人玫瑰,手有余香,如果觉得文章不错,希望可以给个一键三连,感谢。
技术是一点一点积累的,大神也不是一天就可以达到的。原地不动就是退步,所以每天进步一点点。
结束语
最后,附上一句格言:"好学若饥,谦卑若愚",望共勉。