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应用程序。在设计接口时,始终考虑使用端的需求,避免强迫它们依赖于不需要的功能,是实现这一原则的关键。

相关推荐
smallcatlei16 小时前
ios 快捷指令扩展(Intents Extension)简单使用 swift语言
ios·swift
HH思️️无邪2 天前
iOS AVAudioSession 详解【音乐播放器的配置】
ios·音频·swift
今天也想MK代码2 天前
基于swiftui 实现3D loading 动画效果
ios·swiftui·swift
pk_xz1234562 天前
Swift 是一种由苹果公司开发的强大而直观的编程语言,主要用于开发 iOS、macOS、watchOS 和 tvOS 等苹果平台的应用程序。
macos·ios·swift
Adam.com2 天前
#Swift The difference between Parameter and Agrument
服务器·ssh·swift
zuguorui2 天前
XCode16中c++头文件找不到解决办法
开发语言·c++·xcode·swift
concisedistinct2 天前
在macOS的多任务处理环境中,如何平衡应用的性能与用户体验?这是否是一个复杂的优化问题?如何优化用户体验|多任务处理|用户体验|应用设计
macos·swift
淡暗云之遥2 天前
XCode16.0 Command PhaseScriptExecution failed with a nonzero exit code 的错误
ios·bug·xcode·swift
胖虎13 天前
SwiftUI(五)- ForEach循环创建视图&尺寸类&安全区域
ios·swiftui·swift·foreach·安全区域
HH思️️无邪3 天前
iOS MPNowPlayingInfoCenter 通知栏、锁屏 显示当前播放的媒体信息
ios·swift·媒体