SOLID 原则简介
SOLID 原则是五个面向对象设计的基本原则,旨在帮助开发者构建易于管理和扩展的系统。具体包括:
- 单一职责原则:一个类,一个职责。
- 开放封闭原则:对扩展开放,对修改封闭。
- 里氏替换原则:子类可替代基类。
- 接口隔离原则:最小接口,避免不必要依赖。
- 依赖倒置原则:依赖抽象,不依赖具体。
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
类。这种设计减少了耦合,提高了代码的灵活性和可维护性。
打破耦合,遵守接口隔离原则
有两个类Document
和PDF
。
Document
类的name
和content
存储文档信息。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
协议,拆分为两个协议 DocumentConverter
和 Faxable
。
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应用程序。在设计接口时,始终考虑使用端的需求,避免强迫它们依赖于不需要的功能,是实现这一原则的关键。