阅读 1136

Swift 利器 - Protocols 和 Extensions

今天要聊的是 Swift 中我比较喜欢的特性之一:Protocols and Protocol Extensions。它能帮助我们在 无需继承 的前提下,创建高复用性、可随意组合的代码库。

我们都知道面向对象编程的三大特性:封装、继承、多态,有了 SwiftProtocol,我们就可以把 「面向对象」 改为 「面向协议」

常用的继承场景:

下面是大家都会用到的一种类,这种类被称作 「基类」

class BaseClass {
    init() {
        createSelf()
    }
    
    private func createSelf() {
    }
    
    // Some methods that subclasses will used
    func subclassUsedMethod() {
    }
    
    func subclassDeinitUsedMethod() {
    }
}
复制代码

这看起来是一个再正常不过的基类,子类继承它即可。但是我们不止于此,由于业务的迭代和功能的完善,我们可能会在基类中添加更多功能,所以就会面临的问题:基类开始变得臃肿。我们至少会遇到以下两个问题:

  1. 基类里放太多的东西,会一点点打破 「单一职能原则」。时间长了它会变成一个 Massive Class(巨类),并且功能繁多不利于理解和测试。
  2. 我们应用中继承基类的子类会使用基类的所有方法和功能。如果基类出错误了,即使子类没有使用基类的错误功能,但该子类仍然是不安全的。

Protocols

Protocol Extensions 随着 Swift 2.0 一起发布,为 protocol 类型带来了强大的功能,并且宣布了新的编程范式:面向协议编程。可以回顾一下 WWDC 中对该编程范式的介绍:视频链接

所以我们该怎么来使用它?我来拿切换深色模式 ThemeProvider 来举例子:

protocol ThemeProvider {
    func applyDarkMode()
    func removeDarkMode()
}

extension ThemeProvider where Self: UIViewController {
    func applyDarkMode() {
        view.backgroundColor = .black
    }
       
    func removeDarkMode() {
        view.backgroundColor = .white
    }
    
    func isCurrentDark() -> Bool {
        return view.backgroundColor == .black
    }
}
复制代码

我把 applyDarkTheme()removeDarkMode() 方法放到了 ThemeProvider 协议中,对于特殊类型为 UIViewController 的情况下,我通过扩展为其添加了默认实现。

同样的方法,再加一个错误处理的 ErrorPresentable

protocol ErrorPresentable {
    func showAlert(error: Error)
}

extension ErrorPresentable where Self: UIViewController {
    func showAlert(error: Error) {
        let alert: UIAlertController = UIAlertController(title: error.localizedDescription,
                                                         message: nil,
                                                         preferredStyle: .alert)
        alert.addAction(.init(title: "Dismiss", style: .cancel, handler: nil))
        present(alert, animated: true, completion: nil)
    }
}
复制代码

现在,我有两个可重用的 Protocol,它们遵循 「单一职责原则」。现在任何需要此功能的 ViewController 都可以添加对应 extension 来实现。而且我是可以给具体 ViewController 添加唯一扩展,而不是继承自基类。

下面是示例:

class ThemeViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        applyDarkMode()
    }
    
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        removeDarkMode()
    }
}

extension ThemeViewController: ThemeProvider, ErrorPresentable { }
复制代码

我还可以忽略默认实现,定制化自己所需要的实现方法,下面是例子:

class CustomViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        applyDarkMode()
    }
}
extension CustomViewController: ThemeProvider, ErrorPresentable {
    func applyDarkMode() {
        // Custom apply dark mode logic
    }
    
    func removeDarkMode() {
        // Custom remove dark mode logic
    }
}
复制代码

使用 protocols 和 extensions 隐藏三方依赖

下面我将举两个例子来展示如何隐藏第三方依赖并且使代码易于替换和重构。

Extensions

我们都知道的图片加载库 Kingfisher,包含将图片下载缓存到磁盘/内存中,它有 UIImageView 的扩展,引入了 setImage: 方法。所以我们可以通过导入 Kingfisher 再调用 setImage: 方法来使用。假设我们需要用另一个库来替换 Kingfisher,这种情况下我们需要遍历我们的所有文件,然后将 importsetImage: 全部进行替换。这将会是一件很大工作量的事儿,所以我们用 extension 来解决这个问题:

import UIKit
import Kingfisher
import CustomPod

extension UIImageView {
    func setImage(from url: URL) {
        // Custom set image
    }
}
复制代码

我写了 UIImageView 的扩展,并且调用了 KingfishersetImage: 方法。这样在整个项目中,我只需要使用这个扩展,就可以满足我们的需求。

Protocols

三方库 KeychainSwiftKeychain 访问提供了良好的 key-value API,这是 KeychainSwift 的用法示例:

let keychain: KeychainSwift = KeychainSwift()
keychain.set("Apach3's value", forKey: "Apach3's key")
keychain.get("Apach3's key") // Returns "Apach3's value"
复制代码

但是我如果不想公开这个库的用法,这个时候我们需要一个 protocol 来定义敏感数据的存储并隐藏库的使用:

protocol TokenStore {
    var accessToken: String { get set }
    var refreshToken: String { get set }
}

extension KeychainSwift: TokenStore {
    private enum Keys {
        static let accessToken = "accessToken"
        static let refreshToken = "refreshToken"
    }

    var accessToken: String {
        get { return get(Keys.accessToken) }
        set { set(newValue, forKey: Keys.accessToken) }
    }

    var refreshToken: String {
        get { return get(Keys.refreshToken) }
        set { set(newValue, forKey: Keys.refreshToken)}
    }
}
复制代码

使用的时候,需要这样:

class AuthenticationService {
    private let tokenStore: TokenStore

    init(tokenStore: TokenStore) {
        self.tokenStore = tokenStore
    }

    func fetchToken(for credentials: Credentials) {
//        Save tokens here
//        tokenStore.accessToken =
//        tokenStore.refreshToken =
    }
}
复制代码

Swift 中的 Protocol 真的非常强大,需要结合项目慢慢深入体会!

看都看了,点个赞再走吧~~