一、先搞懂什么是 Swift 协议导向编程

很多人刚接触 iOS 开发时,会习惯用“类继承”来复用代码,比如写一个基础的网络请求类,所有需要发网络请求的页面都继承它。但这种方式有个大问题:如果一个页面既要发网络请求,又要处理本地缓存,还得支持扫码,那它就得同时继承多个类,可 Swift 本身不支持多继承,这时候就会陷入两难。

而协议导向编程(以下简称 POP),就是专门解决这类问题的核心思路。它不是让类“继承”别人的能力,而是让类“声明”自己拥有某种能力——就像你去面试,不用说“我继承了某个前辈的技能”,只要说“我会写代码、会沟通”,把具体的能力一条条列出来就行。

1.1 POP 和类继承的核心区别

举个最直观的例子:你要做一个“能说话、能跑”的动物系统。如果用类继承,你得先写一个 Animal 基类,再让 Dog、Cat 继承它,然后在基类里写 speak() 和 run() 方法。但如果后续要加一个“能飞”的能力,比如 Bird,你只能修改基类加 fly(),可 Dog、Cat 并不需要这个能力,就会导致基类变得臃肿。

用 POP 的话,你只要写三个独立的协议:Speakable(能说话)、Runnable(能跑)、Flyable(能飞)。Dog 只要声明自己遵循 Speakable 和 Runnable,Cat 同理,Bird 只要遵循 Speakable 和 Flyable,完全不会有多余的代码,每个类只需要自己的能力。

二、用 POP 优化 iOS 架构的核心思路

iOS 应用的架构,本质就是把“用户交互、数据处理、页面展示”这些模块拆解开,让它们之间的耦合度尽可能低——也就是改一个模块的时候,不会影响到其他模块。POP 刚好能帮我们做到这一点,核心思路就是“面向协议编程,而非面向实现编程”。

2.1 用协议定义能力,解耦模块

我们以 iOS 开发中最常见的“用户信息管理”和“页面跳转”为例,很多新手会把这两个功能写在同一个 ViewController 里,导致页面代码臃肿,后续改起来很麻烦。用 POP 的话,我们可以把这两个功能拆成独立的协议,每个协议只定义一个能力,然后让 ViewController 按需遵循。

首先,我们定义用户信息管理的协议:

// 技术栈:Swift 5.9 + iOS 15.0
// 定义用户信息管理协议:所有需要获取用户信息的类都可以遵循
protocol UserInfoManageable {
    // 获取当前登录用户的ID
    func getCurrentUserId() -> String?
    // 保存用户的昵称
    func saveUserNickname(_ nickname: String)
}

然后定义页面跳转的协议:

// 定义页面跳转协议:所有需要跳转的ViewController都可以遵循
protocol PageNavigatable {
    // 跳转到指定页面(用枚举区分页面类型,避免硬编码)
    func navigate(to pageType: PageType)
}

// 页面类型枚举,用来统一管理所有可跳转的页面
enum PageType {
    case userProfile
    case setting
    case login
}

接下来,我们可以写一个专门的工具类来实现这两个协议的具体逻辑,而不是把逻辑写在 ViewController 里:

// 工具类:专门实现用户信息管理和页面跳转的具体逻辑
class AppUtility: UserInfoManageable, PageNavigatable {
    // 实现用户信息获取逻辑:这里用UserDefaults模拟存储
    func getCurrentUserId() -> String? {
        return UserDefaults.standard.string(forKey: "currentUserId")
    }
    
    // 实现用户昵称保存逻辑
    func saveUserNickname(_ nickname: String) {
        UserDefaults.standard.set(nickname, forKey: "userNickname")
    }
    
    // 实现页面跳转逻辑:这里用UINavigationController的push方法模拟
    func navigate(to pageType: PageType) {
        // 先获取当前的导航控制器(实际开发中可以用路由来解耦)
        guard let nav = UIApplication.shared.keyWindow?.rootViewController as? UINavigationController else {
            return
        }
        
        // 根据页面类型创建对应的ViewController
        switch pageType {
        case .userProfile:
            let profileVC = UserProfileViewController()
            nav.pushViewController(profileVC, animated: true)
        case .setting:
            let settingVC = SettingViewController()
            nav.pushViewController(settingVC, animated: true)
        case .login:
            let loginVC = LoginViewController()
            nav.pushViewController(loginVC, animated: true)
        }
    }
}

最后,我们的 ViewController 只需要声明自己需要什么能力,然后调用工具类的方法就行,完全不用关心具体的实现:

// 首页ViewController,只需要声明自己需要用户信息管理和页面跳转的能力
class HomeViewController: UIViewController {
    // 持有工具类的实例,用来调用协议定义的方法
    private let utility = AppUtility()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // 获取当前用户ID,用来加载首页数据
        if let userId = utility.getCurrentUserId() {
            print("当前用户ID:\(userId)")
            // 加载首页数据的逻辑
        } else {
            // 未登录,跳转到登录页
            utility.navigate(to: .login)
        }
    }
    
    // 点击个人中心按钮的事件
    @objc func onProfileButtonTapped() {
        utility.navigate(to: .userProfile)
    }
    
    // 点击保存昵称的事件
    @objc func onSaveNicknameTapped() {
        utility.saveUserNickname("小明")
    }
}

这样做的好处很明显:如果后续要修改用户信息的存储方式(比如从 UserDefaults 改成 CoreData),我们只需要修改 AppUtility 里的实现,所有遵循 UserInfoManageable 协议的 ViewController 都不用改一行代码;如果要修改页面跳转的方式(比如从 push 改成 present),也只需要修改 AppUtility 里的 navigate 方法,完全不影响其他模块。

2.2 用协议扩展实现默认实现,复用代码

上面的例子里,我们的协议只是定义了方法的签名,具体的实现都写在了 AppUtility 里。但如果多个类需要同一个方法的默认实现,我们可以用协议扩展来实现,这样就不用每个类都写一遍重复的代码。

比如,我们要给所有遵循 UserInfoManageable 协议的类加一个“获取用户昵称”的默认方法,就可以这样写:

// 给UserInfoManageable协议加扩展,实现默认的获取用户昵称的方法
extension UserInfoManageable {
    // 默认实现:从UserDefaults获取用户昵称
    func getUserNickname() -> String? {
        return UserDefaults.standard.string(forKey: "userNickname")
    }
}

这样一来,所有遵循 UserInfoManageable 协议的类(包括 HomeViewController、UserProfileViewController 等),都可以直接调用 getUserNickname() 方法,不用自己写实现。如果某个类需要特殊的实现,也可以重写这个方法,完全不影响其他类。

2.3 用协议组合实现多能力复用

如果一个类需要同时拥有多个能力,我们可以用协议组合来简化声明。比如,我们要做一个“既能管理用户信息,又能跳转页面”的工具类,就可以这样定义一个组合协议:

// 协议组合:把UserInfoManageable和PageNavigatable组合成一个新的协议
typealias AppServiceable = UserInfoManageable & PageNavigatable

然后,我们的 AppUtility 只需要声明自己遵循 AppServiceable 就行,不用再写两个协议:

class AppUtility: AppServiceable {
    // 原来的实现不变
}

这样做的好处是,后续如果要加新的能力,只要修改组合协议就行,所有遵循这个组合协议的类都能自动获得新的能力。

三、POP 优化架构的应用场景

POP 不是万能的,但在很多 iOS 开发的场景下,它能极大地提升架构的合理性,以下是几个最常见的应用场景:

  1. 模块解耦:比如把网络请求、本地存储、页面跳转这些功能拆成独立的协议,让各个模块之间只通过协议交互,不用关心对方的具体实现,方便后续替换或修改。
  2. 单元测试:因为我们可以用“模拟协议实现”来代替真实的业务逻辑,比如测试 HomeViewController 的时候,我们可以写一个 MockAppUtility 来模拟网络请求和页面跳转,不用依赖真实的网络环境或导航控制器。
  3. 多端复用:如果你的项目有多个 iOS 应用(比如主应用和配套的工具应用),可以把公共的能力(比如用户信息管理、支付逻辑)写成协议,然后在多个应用中复用,不用重复写代码。
  4. 功能迭代:比如要给应用加一个“深色模式适配”的功能,只要定义一个 DarkModeAdaptable 协议,让需要适配的页面遵循这个协议,然后在协议扩展里实现适配逻辑,所有页面就能快速获得适配能力。

四、POP 优化架构的优缺点

4.1 优点

  1. 解耦彻底:模块之间只通过协议交互,没有强依赖,改一个模块不会影响其他模块,方便维护和升级。
  2. 复用性强:通过协议扩展和协议组合,同一个能力可以被多个类复用,不用重复写代码。
  3. 测试方便:可以轻松地模拟协议的实现,不用依赖真实的业务逻辑,单元测试的覆盖率和效率都能提升。
  4. 灵活度高:一个类可以同时遵循多个协议,拥有多个能力,完全解决了 Swift 不支持多继承的问题。

4.2 缺点

  1. 学习成本高:对于新手来说,理解“面向协议而非面向实现”的思路需要一定的时间,刚开始可能会觉得协议的定义和扩展很复杂。
  2. 协议过多会导致混乱:如果协议的定义太细,或者没有统一的管理,会导致项目里出现大量的协议,后续维护起来反而麻烦。
  3. 性能损耗:协议的方法调用是通过“协议见证表”来实现的,比直接调用类的方法会有一点点性能损耗,不过在大部分场景下,这种损耗可以忽略不计。

五、使用 POP 优化架构的注意事项

  1. 协议定义要单一职责:一个协议最好只定义一个能力,比如 UserInfoManageable 只负责用户信息的管理,PageNavigatable 只负责页面跳转,不要把多个能力混在一个协议里。
  2. 不要过度使用协议扩展:协议扩展适合实现通用的默认逻辑,如果某个逻辑只适合少数几个类,最好还是写在类的内部,不要为了用协议扩展而用协议扩展。
  3. 用枚举代替硬编码:比如页面类型、网络请求的类型,最好用枚举来定义,不要直接写字符串或数字,方便后续修改和维护。
  4. 统一管理协议:可以把所有的协议放在一个专门的文件夹里,比如叫 Protocols,方便后续查找和维护。
  5. 结合其他架构模式使用:POP 不是独立的架构模式,它可以和 MVC、MVVM、Clean Architecture 等架构模式结合使用,比如在 MVVM 里,ViewModel 可以通过协议来定义自己的能力,View 只需要遵循协议就能和 ViewModel 交互。

六、文章总结

POP 是 Swift 语言的核心特性之一,它能帮我们解决 iOS 开发中常见的“类继承限制、模块耦合、代码复用难”等问题,优化应用的架构设计。通过定义协议来解耦模块、用协议扩展来复用代码、用协议组合来实现多能力复用,我们可以写出更灵活、更易维护、更易测试的代码。

当然,POP 不是万能的,我们需要根据项目的实际情况来选择是否使用,以及如何使用。在实际开发中,我们可以从简单的场景入手,比如先把页面跳转、用户信息管理这些功能拆成协议,逐步熟悉 POP 的思路,再应用到更复杂的场景中。