策略模式实小战

策略模式实小战

"我更喜欢朝着正在编写的代码需要的方向去演化代码,当我去重构代码以解决耦合性、简单性以及表达性的问题时,可能会发现代码已经接近于一个特定的模式了。此时,我把类和变量的名字改成使用模式的名字,并且把代码的结构更改为以更正规的模式的形式。这样,代码就回归为模式"

-- 《敏捷软件开发》

不要拘泥于形式,遇到什么情况,就用最适合的方式去写代码。以下很多事例代码没有对错之分,只是用来举例,提供一些基本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大原则,在开发的时候进行权衡取舍,你也可以写出自己的设计模式。

拓展

相关推荐
pf_data10 小时前
手机换新,怎么把旧iPhone手机数据传输至新iPhone16手机
ios·智能手机·iphone
键盘敲没电1 天前
【iOS】KVC
ios·objective-c·xcode
吾吾伊伊,野鸭惊啼1 天前
2024最新!!!iOS高级面试题,全!(二)
ios
吾吾伊伊,野鸭惊啼1 天前
2024最新!!!iOS高级面试题,全!(一)
ios
不会敲代码的VanGogh1 天前
【iOS】——应用启动流程
macos·ios·objective-c·cocoa
Swift社区1 天前
Apple 新品发布会亮点有哪些 | Swift 周报 issue 61
ios·swiftui·swift
逻辑克1 天前
使用 MultipeerConnectivity 在 iOS 中实现近场无线数据传输
ios
dnekmihfbnmv1 天前
好用的电容笔有哪些推荐一下?年度最值得推荐五款电容笔分享!
ios·电脑·ipad·平板
Magnetic_h2 天前
【iOS】单例模式
笔记·学习·ui·ios·单例模式·objective-c
归辞...2 天前
「iOS」——单例模式
ios·单例模式·cocoa