Files
knowledge-kit/Chapter1 - iOS/1.126.md
2025-06-23 01:18:55 +08:00

7.4 KiB
Raw Blame History

Swift 面向协议编程

概念

面向协议编程Protocol Oriented Programming简称 POP

  • 是 Swift 的一种编程范式, Apple 于2015年 WWDC 提出
  • 在 Swift 的标准库中,能见到大量 POP 的影子

同时Swift 也是一门面向对象的编程语言Object Oriented Programming简称OOP 在 Swift 开发中OOP 和 POP 是相辅相成的任何一方并不能取代另一方POP 能弥补 OOP 一些设计上的不足

优势

OOP 三大特性:继承、封装、多态

继承的经典使用场合:当多个类(比如 A、B、C 类)具有很多共性时,可以将这些共性抽取到一个父类中(比如 D 类最后A、B、C类继承 D 类。

但一些情况下,继承并不能解决问题

比如 AVC 继承自 UIViewController 有 run 方法BVC 继承自 UITableViewController有 run 方法。AVC、BVC 有重复代码,如何消除?继承吗?可能会存在菱形继承问题。

菱形继承,也称为钻石继承或多头继承,发生在当一个类从两个或多个具有共同父类的类继承时。这种结构形成了一个菱形的继承图,因此得名。菱形继承可能导致一些问题,主要是二义性和数据冗余。

  1. 二义性:在菱形继承中,子类可能会从多个路径继承同一个基类的方法或属性。这会导致在子类中调用该方法或属性时存在不确定性,编译器或解释器不知道应该使用哪个版本的方法或属性。这种不确定性可能导致难以调试的错误和不可预测的行为。
  2. 数据冗余:菱形继承也可能导致数据冗余。如果子类从多个父类继承了相同的数据成员,那么这些数据成员在内存中会有多个拷贝。这不仅浪费了存储空间,而且可能导致数据不一致的问题,因为对其中一个拷贝的修改不会影响到其他拷贝。

为了解决这些问题一些编程语言提供了虚继承virtual inheritance的机制。虚继承允许子类只继承共享基类的一个拷贝从而避免了二义性和数据冗余的问题。在虚继承中共享基类被视为虚基类子类只通过其中一个父类继承该基类的实现。

然而,虚继承并不是没有代价的。它可能会引入一些性能开销,因为编译器需要处理额外的间接引用和内存布局问题。此外,虚继承也可能使代码更加复杂和难以理解,特别是对于不熟悉该概念的开发者来说。

因此,在使用多继承时,需要谨慎考虑其潜在的问题和代价。在可能的情况下,尽量避免菱形继承结构,并通过其他方式(如接口、组合或委托)来实现所需的功能。如果必须使用菱形继承,那么应该仔细规划类的设计和继承关系,并充分利用虚继承等机制来减少潜在的问题。

采用 POP 实现。

protocol Runable {
	func run()
}
extension Runable {
	func run() {
		print("running")
	}
}

class AVC: UIViewController, Runable {
	// ...
}
class BVC: UITableViewController, Runable {
	// ...
} 

思想转换

  • 优先考虑创建协议,而不是基类
  • 优先考虑值类型structenum而不是引用类型class
  • 巧用协议的拓展功能 extension Runable { ... }
  • 不要为了面向协议而使用协议

应用

统计字符串中数字个数

// 方法1
var testString: String = "ab1783893cs"
extension String {
    var countOfNumber: Int {
        var count: Int = 0
        for c in self where ("0"..."9").contains(c) {
            count += 1
        }
        return count
    }
}
print(testString.countOfNumber)	// 7

// 方法2.优雅的 Swift 风格
struct Counter {
    var originalString: String
    init(originalString: String) {
        self.originalString = originalString
    }
    var countOfNumber: Int {
        var count: Int = 0
        for c in originalString where ("0"..."9").contains(c) {
            count += 1
        }
        return count
    }
}
extension String {
    var counter: Counter {
        Counter(originalString: self)
    }
}
print(testString.counter.countOfNumber) // 7

上述2个方法虽然实现了统计功能但是不够优雅没有那么的 Swift 化。改写如下

struct MY<Base> {
    let base: Base
    init(base: Base) {
        self.base = base
    }
}

protocol MyCompitable {}
extension MyCompitable {
    var my: MY<Self> {
        set{}
        get{ MY(base: self)}
    }
    static var my: MY<Self>.Type {
        set{}
        get{ MY<Self>.self }
    }
}

具体使用的地方

// 第一步:让 String 拥有 my 前缀属性
extension String: MyCompitable { }
// 第二步:给 String.my、String().my 前缀拓展功能(因为协议的 extension 中有 my 计算属性,也有个 my 静态计算属性,也就是一个方法)
extension MY where Base == String {
    func countOfNumber() -> Int {
        var count: Int = 0
        for c in base where ("0"..."9").contains(c) {
            count += 1
        }
        return count
    }
    
    static func test() {
        print("I am a static method")
    }
    
    mutating func modify() {
        print("I am a mutating method")
    }
}
print(testString.my.countOfNumber())
String.my.test()
testString.my.modify()
// console
7
I am a static method
I am a mutating method

要拓展系统提供的类型,可以按照上述模版进行修改。

  • 加前缀 my 的目的是防止重复,系统实现是黑盒,如果自己直接提供类似 testString.countOfNumber 怕后续系统也提供 countOfNumber 方法。所以加前缀 testString.my.countOfNumber

QA如果是 NSString、NSMutableString 可以满足需求吗? 答案是不行的。但是可以按照上述方式进行修改调整。怎么修改?按照 extension MY where Base == Stringextension MY where Base == NSString 的方式写2遍

当然不,找共性,String、NSString、NSMutableString 都遵循 ExpressibleByStringLiteral 协议。所以实现 extension MY where Base: ExpressibleByStringLiteral 即可。

// 第一步:让 String 拥有 my 前缀属性
extension String: MyCompitable { }
extension NSString: MyCompitable { }

// 第二步:给 String.my、String().my 前缀拓展功能(因为协议的 extension 中有 my 计算属性,也有个 my 静态计算属性,也就是一个方法)
extension MY where Base: ExpressibleByStringLiteral {
    func countOfNumber() -> Int {
        var count: Int = 0
        for c in (base as! String) where ("0"..."9").contains(c) {
            count += 1
        }
        return count
    }
    
    static func test() {
        print("I am a static method")
    }
    
    mutating func modify() {
        print("I am a mutating method")
    }
}


var str:String = "123"
print(str.my.countOfNumber())
String.my.test()
str.my.modify()
print("")

var ocStr: NSString = "456" as NSString
print(ocStr.my.countOfNumber())
String.my.test()
ocStr.my.modify()
print("")

var ocMutableStr: NSString = "456" as NSMutableString
print(ocMutableStr.my.countOfNumber())
String.my.test()
ocMutableStr.my.modify()

这种设计在 SnapKit 中也存在:

// 实例属性用法
view.my.makeConstraints { ... }
// 静态属性用法
UIView.my.registerDefaultConfig()

在 RxSwift 中也存在:

let observable = Observable<Int>.timer(.second(2), period: .second(1), scheduler: MainScheduler.instance) 
observable.map{ "\($0)" }.bind(to: priceLabel.rx.text)