SOLID 原则简介
面向对象编程(OOP)采用"对象"作为软件设计的核心,其中对象可以包含数据(属性或字段)和操作数据的代码(方法)。SOLID 原则是五个面向对象设计的基本原则,旨在帮助开发者构建易于管理和扩展的系统。Swift 编程语言中也适用这些原则,具体包括:
- 单一职责原则(SRP) :确保一个类只负责一项职责。
- 开放封闭原则(OCP) :允许类的功能扩展,但禁止修改现有代码。
- 里氏替换原则(LSP) :子类应能无缝替换其基类。
- 接口隔离原则(ISP) :避免强迫客户端依赖它们不需要的接口。
- 依赖倒置原则(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
}
}
这样就可以实现对于扩展是开放的,对于修改是封闭的。
示例三 枚举
下面示例中的 FileLogger
和 ConsoleLogger
代表两种日志类。其遵循了 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-case
,if-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
}
}
遵循开闭原则的建议
- 尽量使用协议来定义抽象,减少与具体类型的直接依赖。
- 避免使用枚举定义多种类型的行为,而是采用协议和继承机制。
- 将类的属性设为私有,减少外部对类内部状态的直接访问。
- 避免使用全局变量,以降低代码耦合度。
将类所有属性设为私有有助于遵守开闭原则,因为它可以确保其它类不会访问这些属性,进而确保该类的改变不会间接引起依赖该属性的类产生变化。在面向对象设计中,称之为封装。
全局变量与属性类似,使用了全局变量的模块不能被认为是封闭的。