策略模式实小战
"我更喜欢朝着正在编写的代码需要的方向去演化代码,当我去重构代码以解决耦合性、简单性以及表达性的问题时,可能会发现代码已经接近于一个特定的模式了。此时,我把类和变量的名字改成使用模式的名字,并且把代码的结构更改为以更正规的模式的形式。这样,代码就回归为模式"
-- 《敏捷软件开发》
不要拘泥于形式,遇到什么情况,就用最适合的方式去写代码。以下很多事例代码没有对错之分,只是用来举例,提供一些基本tips, 给大家多一些思路,也给自己做一份记录见证自己的成长。
常见问题
我们平时经常使用if else/switch case,开始可能只有两三个分支,但是到后期维护的时候经常会出现超多分支,这时圈复杂度也涨上来了。比如:
scss
func complexLogic(params) {
if (condition1) {
logic1()
} else if (condition1) {
logic2()
} else if (condition1) {
logic3()
}
....
else {
logicn()
}
}
这种方式一不小心就是代码劣化的根源。会出现的几个问题:
- 1 方法变得超级长。如果将分支逻辑抽离成类方法,那么类也会变得超级长,会影响阅读。
- 2 这么多分支逻辑的复用性不好,只能在类内部使用。而且直接引用类,也会造成不必要的依赖。
- 3 在enum的case比较多的的时候,会导致switch case膨胀。
- 4 还有一个就是当开发意识松懈的时候、偷懒、图方便让支逻辑相互耦合。
使用策略模式
看上面的代码,发现他们都有很多相似的地方,都是满足一个条件后,处理一个特定逻辑。将代码修改一下:
scss
#1 接口抽象
protocol ILogic {
func execute(params)
}
class Client {
func complexLogic(logic: ILogic) {
logic.execute(params)
}
...
}
#2 算法实现
struct Logic1: ILogic {
func execute(params) {
... // execute logic
}
}
struct Logic2: ILogic {
func execute(params) {
... // execute logic
}
}
struct Logic3: ILogic {
func execute(params) {
... // execute logic
}
}
# 3 使用
let client = Client()
let logic1 = Logic1()
let logic2 = Logic2()
let logic3 = Logic3()
// 这里举例很多人可能会有疑惑,如果要使用还不是要用ifelse区分?其实不用,在每个调用的地方就可以确定需要哪种类型了, 如果确实不适用,也没必要强行使用这种方式。
client.complexLogic(logic1)
...
client.complexLogic(logic2)
...
client.complexLogic(logic3)
上述代码可以看出,三种算法都独立出来,不会相互污染,也可以提供给其他地方复用。
很显然,这里代码量变大了很多,里面就多出了很多类,还是要看具体场景,建议如果if else比较复杂的时候可以这样处理。
策略模式的定义
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
定义一组算法,将每个算法都封装起来,并且使他们之间可以互换。策略模式使算法可以独立于客户端而变化。
上述示例代码就是将if else中各分支逻辑当成独立算法,然后再设置到客户度中。每个算法是独立维护的。
这样符合几个原则:
- SRP(职责单一):各算法各司其职,各自维护
- OCP(开闭原则):对修改关闭,对扩展开放。上述示例非常容易扩展
- DIP(依赖倒置原则):对于客户端来说,它是依赖抽象的,而不是依赖实现。
当然对于 if else 的解除还有很多争议,但是一切不要为了使用而使用,而不去考虑当前场景。比如if else 有一个很好的优势就是提高程序的效率,比如将命中率较高的算法放在if else的最前面。
策略模式也是一种依赖注入,达到依赖翻转的目的(可以自己画一个UML图就知道为何会依赖翻转了)。
常见用法举例
与工厂模式结合
上述的if else 解除后,在有些情况下也需要指定算法,有些事具体业务使用一个算法,有些确实需要用多个算法,这时就可以使用工厂模式将这些算法映射过去
rust
class LogicFactory {
let logics: [String: ILogic] = [
"logic1": Logic1(),
"logic2": Logic2(),
"logic3": Logic3(),
]
// 这里的type最好使用enum ,这里为了方便使用String,Client初始化方法自行补充
func createLogicClient(type: String) -> Client {
return Client(logic: logics[type])
}
}
其实我感觉这也是if else的一种变体,因为if else 本质上也是一种映射关系,只不过读取字典更快,当然还是要强调,保持代码的简单才是最佳的方式,因为你不知道后期会有什么改动,只要保持简单后期维护起来就简单,就更易于变更。
switch case 的拆解
先看实例:
kotlin
enum ButtonType {
case image
case video
case hashtag
case music
var title: String {
switch self {
case .image:
return "Image"
case .video:
return "Video"
case .hashtag:
return "Hashtag"
case .music:
return "Music"
}
}
var normalIcon: UIImage {
switch self {
case .image:
return Assets.image1
case .video:
return Assets.image2
case .hashtag:
return Assets.image3
case .music:
return Assets.image4
}
}
var selectedIcon: UIImage {
switch self {
case .image:
return Assets.simage1
case .video:
return Assets.simage2
case .hashtag:
return Assets.simage3
case .music:
return Assets.simage4
}
}
}
这是一段工具栏的事例代码,ButtonType作为一种类型载体,包含了很多类型数据,这种使我们用enum的常用方式。很显然,ButtonType的case很多的时候,每新增一个只读熟悉就爆炸了,需要写很多switch-case。再者,如果需要的只读属性很多,那么就是双重爆炸,这个enum会长的吓人,一旦要新增一个case,那改动的地方就超级多了。
注意:这里只是拿上述代码举例,并不是说上述写法一定有问题,一切看场景。
这里还有很明显的问题,每个ButtonType都有一组响应类型,比如正常点击响应,不可用状态的点击响应等等,在这些点击响应的地方也会需要switch case进行处理。
那么怎么解决这个问题呢?就是使用策略,看下列代码:
swift
portocol ButtonType {
var title: String { get }
var normalIcon: UIImage { get }
var selectedIcon: UIImage { get }
func normalAction()
func unableAction()
}
struct ImageButton: ButtonType {
var title: String { return "Image" }
var normalIcon: UIImage { return Asset.image1 }
var selectedIcon: UIImage { return Asset.simage1 }
func normalAction() {
... // do something when clicked in normal state
}
func unableAction() {
... // do something when clicked in unable state
}
}
struct MusicButton: ButtonType {
var title: String { return "Image" }
var normalIcon: UIImage { return Asset.image1 }
var selectedIcon: UIImage { return Asset.simage1 }
func normalAction() {
... // do something when clicked in normal state
}
func unableAction() {
... // do something when clicked in unable state
}
private weak var delegate: SomeDelegate?
init(delegate: SomeDelegate) { //#1
self.delegate = delegate
}
}
看上述将switch case 拆解为具体的"算法",每个算法独立维护,如果需要新增算法,直接继承ButtonType接口即可,然后修改一下客户端(使用新增算法的地方)即可。这里很明显符合OCP,使用这种方式后会发现代码中从上到下,几乎很少有enum的switch case,代码逻辑很变的很简洁。
当然这也会有一个问题,就是结构体会增加很多,在前期开发时确实不如直接使用switch case方便,但是后期会有巨大优势。还是那句话,具体场景具体措施,我们的观念要随上下文去变动。
这里有个小Tips。可能很多人会觉得,那如果这些算法需要调用其他逻辑怎么办?很简单,看#1处的初始化代码,需要什么依赖,直接传入即可。
小结
本节讲解了开发过程中策略模式的部分具体使用场景,代码场景千千万,是没法穷尽的,但是只要牢记设计模式6大原则,在开发的时候进行权衡取舍,你也可以写出自己的设计模式。