Swift SOLID 2. 开闭原则

SOLID 原则简介

面向对象编程(OOP)采用"对象"作为软件设计的核心,其中对象可以包含数据(属性或字段)和操作数据的代码(方法)。SOLID 原则是五个面向对象设计的基本原则,旨在帮助开发者构建易于管理和扩展的系统。Swift 编程语言中也适用这些原则,具体包括:

  1. 单一职责原则(SRP) :确保一个类只负责一项职责。
  2. 开放封闭原则(OCP) :允许类的功能扩展,但禁止修改现有代码。
  3. 里氏替换原则(LSP) :子类应能无缝替换其基类。
  4. 接口隔离原则(ISP) :避免强迫客户端依赖它们不需要的接口。
  5. 依赖倒置原则(DIP) :高层模块不应依赖低层模块,二者都应依赖于抽象。

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

开闭原则

开闭原则指出,软件实体(如类、模块、函数等)应该对扩展开放,对修改封闭。这意味着应在不改变现有代码的前提下,扩展类的功能。在 Swift 中,这通常通过使用协议和继承来实现。

示例一 继承示例

swift 复制代码
 class Payment {
    func processPayment(amount: Double) {}
 }
 ​
 class CreditCardPayment: Payment {
    override func processPayment(amount: Double) {
        print("Processing credit card payment")
    }
 }
 ​
 class PayPalPayment: Payment {
    override func processPayment(amount: Double) {
        print("Processing PayPal payment")
    }
 }

若要添加新的支付方式(如ApplePayPayment),只需继承Payment类即可,无需修改现有代码。

示例二 协议抽象

swift 复制代码
 struct Ball {
    let name: String
    let age: Int
     
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
 }
 ​
 struct EquipmentRoom {
    var balls: [Ball]
    init(balls: [Ball]) {
        self.balls = balls
    }
     
    mutating func add(ball: Ball) {
        balls.append(ball)
    }
 }

球类器材房(EquipmentRoom),管理的是球(Ball)。当需要同时管理篮球的时候,就需要修改 BallsRoom 类。因此,没有遵守开闭原则。

通过修改 Ball 类为新的 Equipment ,来满足需求功能,这样 EquipmentRoom 就不需要修改了。但是 Ball 类有可能在多处使用,修改可能导致其产生新的问题。

在一个大型工程中,对确定模块的修改不应影响或强制其他模块进行更改。

遵循开闭原则

通过协议解决问题。

swift 复制代码
 protocol EquipmentType {
    init(name: String, age: Int)
 }
 ​
 ​
 struct Ball: EquipmentType {
     
    let name: String
    let age: Int
     
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
 }
 ​
 ​
 struct EquipmentsRoom {
    var equipments: [EquipmentType]
    init(equipments: [EquipmentType]) {
        self.equipments = equipments
    }
     
    mutating func add(equipment: EquipmentType) {
        equipments.append(equipment)
    }
 }

创建EquipmentType 协议,Ball 类遵循该协议。EquipmentsRoom 不再依赖Ball ,转而依赖 EquipmentType 。想要扩展 Ball的功能,只需要新创建一个实体,遵循 EquipmentType 即可。

swift 复制代码
 struct Basketball: EquipmentType {
    let name: String
    let age: Int
     
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
     
    func otherMethod() {
        // something
    }
 }

这样就可以实现对于扩展是开放的,对于修改是封闭的。

示例三 枚举

下面示例中的 FileLoggerConsoleLogger 代表两种日志类。其遵循了 Logger 协议。每个类包含 LoggerType 属性。

swift 复制代码
 enum LoggerType {
    case file
    case console
 }
 ​
 ​
 protocol Logger {
    var type: LoggerType { get }
 }
 ​
 class FileLogger: Logger {
    let type: LoggerType = .file
    func fileLog(_ message: String) {
        print(message)
    }
 }
 ​
 class ConsoleLogger: Logger {
    let type: LoggerType = .console
    func consoleLog(_ message: String) {
        print(message)
    }
 }
 ​

LogManager 类负责接口并执行,以便输出对应的日志信息。

swift 复制代码
 // 日志管理器类负责接收任意Logger实例并调用它的log方法
 class LogManager {
    func log(message: String, using logger: Logger) {
        switch logger.type {
        case .file:
            (logger as? FileLogger)?.fileLog(message)
        case .console:
            (logger as? ConsoleLogger)?.consoleLog(message)
    }
 }

如果要添加一种新的类型 RemoteLogger

swift 复制代码
 class RemoteLogger: Logger {
    let type: LoggerType = .remote   
    func remoteLogPrint(_ message: String) {
        print(message)
    }
 }

还需要修改 LogManager 类的 func log(message: String, using logger: Logger) 方法处理相应的类型。

swift 复制代码
 class LogManager {
    func log(message: String, using logger: Logger) {
        switch logger.type {
        case .file:
            (logger as? FileLogger)?.fileLogPrint(message)
        case .console:
            (logger as? ConsoleLogger)?.consoleLogPrint(message)
        case .remote:
            (logger as? RemoteLogger)?.remoteLogPrint(message)
        }
    }
 }

这样没有满足 对扩展开放,对修改关闭.

枚举(enum)的使用可能导致代码违背开闭原则,因为添加新的枚举值通常需要修改现有的switch-case逻辑。为避免这一问题,可以将具体行为封装在遵循共同协议的类中,而非直接依赖于枚举类型。

此外还需要注意的是:使用 enum 后代代码需要判断多种情况,会存在很多 switch-caseif-else 代码的情况。意味着创建一个枚举case,需要修改多处代码。甚至会出现忘记其中一处,进入未知逻辑中。

遵循开闭原则

移除enum,每个实体实现自身的func log(message: String)方法,这样遵守开闭原则。如下所示:

swift 复制代码
 protocol Logger {
    func log(message: String)
 }
 ​
 class FileLogger: Logger {
    func log(message: String) {
        // do something
    }
 }
 ​
 class ConsoleLogger: Logger {
    func log(message: String) {
        // do something
    }
 }
 ​
 class LogManager {
    func log(message: String, using logger: Logger) {
        logger.log(message: message)
    }
 }

如果需要新增类型,只需要创建一个新的实体,无需修改 LogManager

javascript 复制代码
 class RemoteLogger: Logger {
    func log(message: String) {
        // do something
    }
 }

遵循开闭原则的建议

  • 尽量使用协议来定义抽象,减少与具体类型的直接依赖。
  • 避免使用枚举定义多种类型的行为,而是采用协议和继承机制。
  • 将类的属性设为私有,减少外部对类内部状态的直接访问。
  • 避免使用全局变量,以降低代码耦合度。

将类所有属性设为私有有助于遵守开闭原则,因为它可以确保其它类不会访问这些属性,进而确保该类的改变不会间接引起依赖该属性的类产生变化。在面向对象设计中,称之为封装。

全局变量与属性类似,使用了全局变量的模块不能被认为是封闭的。

相关推荐
Swift社区18 小时前
在 Swift 中实现字符串分割问题:以字典中的单词构造句子
开发语言·ios·swift
#摩斯先生18 小时前
Swift从0开始学习 对象和类 day3
ios·xcode·swift
没头脑的ht18 小时前
Swift内存访问冲突
开发语言·ios·swift
#摩斯先生18 小时前
Swift从0开始学习 并发性 day4
ios·xcode·swift
_黎明18 小时前
【Swift】类型标注、类型安全和类型推断
swift
没头脑的ht18 小时前
Swift闭包的本质
开发语言·ios·swift
今天啥也没干2 天前
使用 Sparkle 实现 macOS 应用自定义更新弹窗
前端·javascript·swift
yngsqq2 天前
037集——JoinEntities连接多段线polyline和圆弧arc(CAD—C#二次开发入门)
开发语言·c#·swift
_黎明2 天前
【Swift】字符串和字符
开发语言·ios·swift
RickeyBoy3 天前
基于 Swift 从零到一开发贪吃蛇游戏(四)
swift