Swift SOLID 3. 接口隔离

SOLID 原则简介

SOLID 原则是五个面向对象设计的基本原则,旨在帮助开发者构建易于管理和扩展的系统。具体包括:

  1. 单一职责原则:一个类,一个职责。
  2. 开放封闭原则:对扩展开放,对修改封闭。
  3. 里氏替换原则:子类可替代基类。
  4. 接口隔离原则:最小接口,避免不必要依赖。
  5. 依赖倒置原则:依赖抽象,不依赖具体。

Swift 编程语言中也适用这些原则,遵循这些原则,Swift 开发者可以设计出更加灵活、易于维护和扩展的应用程序。

接口隔离原则

接口隔离原则强调 客户端不应该被迫依赖于它不使用的接口 。常用的术语 fat interface(即一个接口中包含太多的方法或属性)。Fat interface被认为是非内聚的接口,意味着接口提供了更多的方法、功能。Fat interface会带来与 单一职责 类似的问题,如不必要的重构,额外的测试。这是因为接口耦合了太多功能,修改一个接口,所有实现了该协议的类都需要重新构建和测试。

接口隔离原则的重要性

遵循接口隔离原则可以带来多个好处:

  • 增强模块的可维护性:当接口粒度适当时,维护和理解代码变得更加容易。
  • 提升代码的可重用性:更细粒度的接口更容易在不同的上下文中被重用。
  • 降低耦合度:细粒度接口降低了模块间的依赖关系,使得修改一个模块对其他模块的影响最小。

接口隔离原则的应用

1. 分离接口

假设我们有一个应用,它需要处理不同类型的支付方式。而不是定义一个包含所有支付方法的大接口,我们可以为每种支付方式定义一个细粒度的协议。

swift 复制代码
 // 信用卡支付
 protocol CreditCardPaymentProtocol {
    func processCreditCardPayment(amount: Double)
 }
 ​
 // 微信支付
 protocol WechatPaymentProtocol {
    func processWechatPayment(amount: Double)
 }
 ​
 class CreditCardPayment: CreditCardPaymentProtocol {
    func processCreditCardPayment(amount: Double) {
        // 实现 信用卡 支付逻辑
    }
 }
 ​
 class WechatPayment: WechatPaymentProtocol {
    func processWechatPayment(amount: Double) {
        // 实现 微信 支付逻辑
    }
 }

2. 功能分离

考虑一个多功能打印机的例子,它可以打印、扫描和复印。而不是定义一个包含所有功能的接口,我们可以为每项功能定义单独的接口。

swift 复制代码
 protocol Printable {
    func printDocument(document: Document)
 }
 ​
 protocol Scannable {
    func scanDocument() -> Document
 }
 ​
 protocol Copiable {
    func copyDocument(document: Document) -> Document
 }
 ​
 // 分别实现每个接口
 class Printer: Printable {
    func printDocument(document: Document) {
        // 打印文档
    }
 }
 ​
 class Scanner: Scannable {
    func scanDocument() -> Document {
        // 扫描文档并返回
    }
 }
 ​
 class Copier: Copiable {
    func copyDocument(document: Document) -> Document {
        // 复印文档并返回
    }
 }

3. 避免不必要的依赖

通过细分接口,可以确保客户端类只依赖它们真正需要的方法,从而避免不必要的依赖。

swift 复制代码
 // 数据加载
 protocol DataLoading {
    func loadData()
 }
 ​
 // 数据保存
 protocol DataSaving {
    func saveData()
 }
 ​
 class DataManager: DataLoading, DataSaving {
    func loadData() {
        // 加载数据
    }
 ​
    func saveData() {
        // 保存数据
    }
 }
 ​
 // 使用 DataManager 的类可以选择性地依赖于加载或保存功能
 class ReportingTool: DataLoading {
    let dataManager: DataLoading
 ​
    init(dataManager: DataLoading) {
        self.dataManager = dataManager
    }
 ​
    func generateReport() {
        dataManager.loadData()
        // 生成报告的逻辑
    }
 }

ReportingTool 类专注于生成报告的逻辑,而生成报告只需要加载数据的功能,不需要保存数据的功能。因此,它只依赖于 DataLoading 协议,而不是直接依赖于 DataManager 类。这种设计减少了耦合,提高了代码的灵活性和可维护性。

打破耦合,遵守接口隔离原则

有两个类DocumentPDF

  • Document类的namecontent存储文档信息。
  • PDF类接受document入参,创建pdf文件。

这里不会关注具体实现细节,只关注接口部分。

swift 复制代码
 public class Document {
    public var name: String
    public var content: String
     
    public init(name: String, content: String) {
        self.name = name
        self.content = content
    }
 }
 ​
 public class PDF {
    public var document: Document
     
    public init(document: Document) {
        self.document = document
    }
     
    public func create() -> Data {
        // do something
        return Data()
    }
 }

下面声明Machine协议:

swift 复制代码
 /// 机器协议
 public protocol Machine {
    /// 将文档转换为PDF
    func convert(document: Document) -> PDF?
    /// 传真文档
    func fax(document: Document)
    /// 复印文档
    func copy(document: Document) -> Document?
 }

FaxMachine 传真机

swift 复制代码
 public class FaxMachine: Machine {
    public func convert(document: Document) -> PDF? {
        return nil
    }
     
    public func fax(document: Document) {
        print("执行传真")
    }
     
    public func copy(document: Document) -> Document? {
        return nil
    }
 }

FaxMachine只有传真的功能,所以只需要实现fax(document: Document)方法,协议中的其它方法对于FaxMachine是无意义的,但因为遵守了Machine协议,其强制实现所有方法。

PhoneMachine 手机

swift 复制代码
 public class PhoneMachine: Machine {
    public func convert(document: Document) -> PDF? {
        return PDF(document: document)
    }
     
    public func fax(document: Document) { }
     
    public func copy(document: Document) -> Document? {
        return nil
    }
 }

PhoneMachine 可以复印 document, 或转换成 PDF。没有传真的能力,无法实现 fax(document: Document) 方法。

UltraMachine 超机

swift 复制代码
 public class UltraMachine: Machine {
    public func convert(document: Document) -> PDF? {
        return PDF(document: document)
    }
     
    public func fax(document: Document) {
        print("执行传真")
    }
     
    public func copy(document: Document) -> Document? {
        // do something
        return document
    }
 }

UltraMachine 是一台超强机器,可以实现协议中的三个方法。

违反接口隔离产生的问题

产生耦合

Machine 将不同职责耦合到了一起,违反了单一职责。

因为耦合了不同职责,修改任意方法后,其它遵守Machine协议的方法也需要重新构建和测试。

不易理解和测试

尽管上述示例没有暴露出 fat interface 的弊端:不易理解和测试,但方法变得越来越多时,这一问题会逐渐明显。

可选返回值

将所有接口放入到一个协议时,由于某些类只实现部分方法,方法返回值必须是可选类型。调用方法时,必须处理返回值为nil的场景:

javascript 复制代码
 let document = Document(name: "Document Name", content: "Document Content")
 let iPhone: Machine = NewIphone()
 if let pdf: PDF = iPhone.convert(document: document) {
    print(pdf)
 }

设计解决方案时,如果先考虑具体的实现,后设计接口,我们会倾向于将接口放到一个协议中。如果先考虑接口设计,则会将不同接口划分到不同协议中。

遵循接口隔离

将最初的 Machine 协议,拆分为两个协议 DocumentConverterFaxable

swift 复制代码
 /// 文档转换能力
 public protocol DocumentConverter {
    /// 将文档转换为PDF
    func convert(document: Document) -> PDF
    /// 复印文档
    func copy(document: Document) -> Document
 }
 ​
 /// 传真能力
 public protocol Faxable {
    /// 传真文档
    func fax(document: Document)
 }

避免过度设计

在接口拆分时需要找到合适状态,盲目的进行接口隔离会导致过度设计。

当拆分接口时,可以先回答以下问题:

  • 接口隔离是否会带来中期收益
  • 接口隔离是否会带来长期收益

接口拆分为上述两个协议后,类实现如下:

swift 复制代码
 public class FaxMachine: Faxable {
    public func fax(document: Document) {
        print("执行传真")
    }
 }
 ​
 ​
 public class PhoneMachine: DocumentConverter {
    public func convert(document: Document) -> PDF {
        return PDF(document: document)
    }
    
    public func copy(document: Document) -> Document {
        return document
    }
 }
 ​
 ​
 public class UltraMachine: Faxable, DocumentConverter {
    public func convert(document: Document) -> PDF {
        return PDF(document: document)
    }
     
    public func copy(document: Document) -> Document {
        return document
    }
     
    public func fax(document: Document) {
        print("执行传真")
    }
 }

使用两个协议分别实现不同职责,避免了:

  • 可选类型的问题
  • 职责耦合的问题

总结

遵循接口隔离原则可以带来多个好处:

  • 增强模块的可维护性:当接口粒度适当时,维护和理解代码变得更加容易。
  • 提升代码的可重用性:更细粒度的接口更容易在不同的上下文中被重用。
  • 降低耦合度:细粒度接口降低了模块间的依赖关系,使得修改一个模块对其他模块的影响最小。

接口隔离原则鼓励我们设计精简而专注的接口,避免了过度膨胀的接口导致的不必要耦合。通过遵循接口隔离,我们能够构建更灵活、易于测试和维护的Swift应用程序。在设计接口时,始终考虑使用端的需求,避免强迫它们依赖于不需要的功能,是实现这一原则的关键。

相关推荐
HarderCoder18 小时前
Swift 中的不透明类型与装箱协议类型:概念、区别与实践
swift
HarderCoder18 小时前
Swift 泛型深度指南 ——从“交换两个值”到“通用容器”的代码复用之路
swift
东坡肘子19 小时前
惊险但幸运,两次!| 肘子的 Swift 周报 #0109
人工智能·swiftui·swift
胖虎119 小时前
Swift项目生成Framework流程以及与OC的区别
framework·swift·1024程序员节·swift framework
songgeb1 天前
What Auto Layout Doesn’t Allow
swift
YGGP2 天前
【Swift】LeetCode 240.搜索二维矩阵 II
swift
YGGP2 天前
【Swift】LeetCode 73. 矩阵置零
swift
非专业程序员Ping3 天前
HarfBuzz 实战:五大核心API 实例详解【附iOS/Swift实战示例】
android·ios·swift
Swift社区4 天前
LeetCode 409 - 最长回文串 | Swift 实战题解
算法·leetcode·swift
YGGP7 天前
【Swift】LeetCode 54. 螺旋矩阵
swift