Swift SOLID 2. 开闭原则

1,220 阅读4分钟

SOLID 原则简介

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

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

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

开放封闭原则

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

示例1: 继承示例

 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类即可,无需修改现有代码。

示例2: 协议抽象

问题代码

 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)。当我们需要同时管理篮球时,就需要修改 EquipmentRoom 类。这违反了开放封闭原则。

通过引入协议,我们可以解决这个问题。

优化后的代码

 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)
    }
 }

通过这种方式,我们可以扩展 Ball 的功能,而无需修改 EquipmentsRoom 类。

想要扩展 Ball的功能,只需要新创建一个实体,遵循 EquipmentType 即可。

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

示例3: 枚举

问题代码

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

 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)
    }
 }
 ​
 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)
        }
    }
 }

在这个例子中,我们使用枚举来定义不同的日志类型,并且在 LogManager 类中使用 switch-case 语句来处理不同类型的日志,这违反了开放封闭原则。

添加新的日志类型

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

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

 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,需要修改多处代码。甚至会出现忘记其中一处,进入未知逻辑中。

优化后的代码

为了遵守开放封闭原则,我们可以移除枚举,并让每个日志类实现自己的日志方法。

 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

class RemoteLogger: Logger {
    func log(message: String) {
        // do something
    }
}

遵循开闭原则的建议

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

通过遵循这些建议,我们可以确保我们的代码更加灵活、易于维护和扩展。