一,系统中的SOLID 原则,指的是开发者设计软件系统的时候,需遵循:
高内聚、低耦合、可维护、可扩展、健壮五个原则,现有一个文件处理模块,通常的写法如下:
// 低层模块 - 具体的数据存储实现
class FileDataStore {
func save(data: String, to filename: String) {
system.manager.save(path.filename, data);
// 将数据保存到文件
print("Saving data to file: \(filename)")
}
}
// 高层模块 - 业务逻辑
class UserManager {
private let fileStore = FileDataStore() // 直接依赖于具体实现
func saveUser(_ user: String) {
fileStore.save(data: user, to: "user_data.txt")
}
}
在这个设计中,UserManager是业务逻辑模块,直接依赖于 FileDataStore实现模块,将数据保存到文件中。
如果业务层的保存方式修改为保存到数据库中,那么必须去修改UserManager中的代码,例如修改FileDataStore为DataBaseStore,然后执行save方法,这样的实现耦合性高,也就是说代码不够健壮,如果遵循SOLID 原则,写法如下:
// 抽象层 - 定义协议
protocol DataStoreProtocol {
func save(data: String, to location: String)
}
// 低层模块 - 具体实现
class FileDataStore: DataStoreProtocol {
func save(data: String, to location: String) {
system.manager.save(path.filename, data);
print("Saving data to file: \(location)")
}
}
// 低层模块 - 另一种具体实现
class DatabaseDataStore: DataStoreProtocol {
func save(data: String, to location: String) {
database.table.insert(("filename","fileData"),(path.filename, data));
print("Saving data to database: \(location)")
}
}
// 高层模块 - 业务逻辑
class UserManager {
private let dataStore: DataStoreProtocol // 依赖于抽象而非具体实现
init(dataStore: DataStoreProtocol) {
self.dataStore = dataStore
}
func saveUser(_ user: String) {
dataStore.save(data: user, to: "users")
}
}
// 使用文件
let fileStore = FileDataStore()
let userManager = UserManager(dataStore: fileStore)
userManager.saveUser("John Doe")
// 或者数据库
let dbStore = DatabaseDataStore()
let anotherUserManager = UserManager(dataStore: dbStore)
anotherUserManager.saveUser("Jane Doe")
以上代码,底层具体实现模块遵循了数据存储协议,协议中有一个保存接口,然后分别写了文件保存、数据库保存两个具体实现类,在UserManager业务模块中进需要传递一个集成DataStoreProtocol协议的实现类,就可以实现保存,只要实现类遵循了DataStoreProtocol协议,任意的保存方式都可以实现,遵循了SOLID 原则,增加了扩展性。具体好处如下:
1. 解耦:高层模块不再依赖于低层模块的具体实现,依赖的是底层接口(UserManager.init(dataStore: DataStoreProtocol) );
2,可测试性:可以使用模拟对象进行测试(下面会进行测试用例);
3,灵活性:可以轻松更换不同的实现(文件或数据库亦或其他保存方式);
4,可维护性:修改实现不会影响依赖于协议的高层模块,例如DatabaseDataStore内部修改,不影响UserManager业务层,因为UserManager中只是调用了DataStoreProtocol的接口。
还有,就是通过查看DataStoreProtocol,立马就能知道其职责是什么。
可测试性,如果需要对业务进行测试,那么测试用例:
// 测试时可以创建模拟对象
class MockStoreManager: DataStoreProtocol {
// 用于记录方法调用的属性
var saveCallCount = 0
var lastSavedData: String?
var lastSaveLocation: String?
var shouldThrowError = false
func save(data: String, to location: String) {
saveCallCount += 1
lastSavedData = data
lastSaveLocation = location
if lastSavedData != nil || lastSaveLocation != nil {
print("保存成功");
return;
} else {
shouldThrowError = true
print("保存失败");
return;
}
}
// ... 其他协议方法的模拟实现
}
在测试类中,遵循了DataStoreProtocol协议,只是打印了是否保存成功,可以直接方便进行测试,不需要具体实现如何保存。
遵循了倒置原则中的可测试性。
倒置原则中的接口隔离原则的设计:
如果以上的DataStoreProtocol协议中,增加了一个数据查询方法,然后将查询出来的数据进行打印:
protocol DataStoreProtocol {
func save(data: String, to location: String)
func get(location: String) -> String(Data);
func print(data: String);
}
当一个某一个具体实现模块(DatabaseDataStore)要对数据进行增删改查,但并不想进行打印,那么DatabaseDataStore遵循DataStoreProtocol协议,我们就强迫DatabaseDataStore实现它们不需要的方法print,如果不去实现则会报错,这样就违反了接口隔离的原则。而且整个DataStoreProtocol协议中的接口范围会更大,对其具体的职责不够清晰,范围太广导致继承类需要实现不需要的方法。接口隔离的原则具体指的是:
1,类只需要实现与其职责相关的接口;
2,修改接口时影响范围更小;
3,接口职责更加明确;
4,更容易为特定功能创建测试;
5,减少不必要的依赖,避免实现和使用不需要的方法
正确的实现方式如下:
// 数据增删改查接口
protocol DataStoreProtocol {
func save(data: String, to location: String)
func get(location: String) -> String(Data);
func update(location: String, data: String) -> bool;
func delete(location: String, data: String) -> bool;
}
// 数据打印接口
protocol DataPrinterProtocol {
func print(data: String)
}
// 业务层
class dataDealManager: DataStoreProtocol, DataPrinterProtocol {
private let dataStore: DataStoreProtocol
private let printer: DataPrinterProtocol
func saveUser(_ user: String) {
dataStore.save(data: user, to: "users")
}
func printData(_ data: String) {
printer.print(data)
}
}
在实际的设计中,应当:
1,设计小而专注的接口;
2,让类遵循多个专门的接口,而不是一个庞大的接口;
3,遵循单一职责原则,确保每个接口只负责一个方面的职责;
4,在你的分析会话管理器中,可以根据实际使用场景来决定是否将协议拆分得更细;
针对以上这个软件的设计思想,不仅仅是为了能够设计出更健全的软件架构,最重要的其实是可以通过这个思维,延伸到各个工种,更甚者可以延伸到生活中。
例如产品经理:对于一些设计思想的低层记录,最终实现到延展;
一个小Demo:
import Foundation
// MARK: - 高内聚:只负责温度转换
struct TemperatureConverter {
func celsiusToFahrenheit(_ c: Double) -> Double {
c * 9 / 5 + 32
}
func fahrenheitToCelsius(_ f: Double) -> Double {
(f - 32) * 5 / 9
}
}
// MARK: - 低耦合 & 可扩展:支付方式通过协议解耦
protocol PaymentService {
func pay(amount: Double)
var name: String { get }
}
final class ApplePayService: PaymentService {
let name = "Apple Pay"
func pay(amount: Double) {
print("[Apple Pay] Pay \(amount)")
}
}
final class WechatPayService: PaymentService {
let name = "WeChat Pay"
func pay(amount: Double) {
print("[WeChat Pay] Pay \(amount)")
}
}
// 新增一种支付方式,只需实现协议,不需要改旧代码(可扩展)
final class CreditCardPayService: PaymentService {
let name = "Credit Card"
func pay(amount: Double) {
print("[Credit Card] Pay \(amount)")
}
}
// MARK: - 可维护:把校验逻辑抽离成独立类型
enum ValidationError: Error, CustomStringConvertible {
case empty(field: String)
case tooShort(field: String, min: Int)
var description: String {
switch self {
case .empty(let field):
return "\(field) 不能为空"
case .tooShort(let field, let min):
return "\(field) 长度不能少于 \(min) 个字符"
}
}
}
struct UsernameValidator {
let minLength: Int
func validate(_ username: String) throws {
guard !username.isEmpty else {
throw ValidationError.empty(field: "用户名")
}
guard username.count >= minLength else {
throw ValidationError.tooShort(field: "用户名", min: minLength)
}
}
}
// MARK: - 健壮性:用 Result 和错误处理保护关键流程
enum NetworkError: Error {
case invalidURL
case requestFailed
case decodeFailed
}
struct User: Decodable {
let id: Int
let name: String
}
final class UserService {
func fetchUser(id: Int, completion: @escaping (Result<User, NetworkError>) -> Void) {
guard let url = URL(string: "https://api.example.com/users/\(id)") else {
completion(.failure(.invalidURL))
return
}
URLSession.shared.dataTask(with: url) { data, _, error in
guard error == nil, let data = data else {
completion(.failure(.requestFailed))
return
}
do {
let user = try JSONDecoder().decode(User.self, from: data)
completion(.success(user))
} catch {
completion(.failure(.decodeFailed))
}
}.resume()
}
}
// MARK: - ViewModel:组合使用上述能力
final class CheckoutViewModel {
private let paymentService: PaymentService // 低耦合:依赖抽象
private let usernameValidator: UsernameValidator // 可维护:校验独立
private let userService: UserService // 健壮性:内部做错误处理
init(paymentService: PaymentService,
usernameValidator: UsernameValidator = UsernameValidator(minLength: 3),
userService: UserService = UserService()) {
self.paymentService = paymentService
self.usernameValidator = usernameValidator
self.userService = userService
}
func checkout(username: String, userId: Int, amount: Double) {
// 1. 校验用户名(可维护)
do {
try usernameValidator.validate(username)
} catch let error as ValidationError {
print("注册失败:\(error)")
return
} catch {
print("注册失败:未知错误")
return
}
// 2. 获取用户信息(健壮性:使用 Result)
userService.fetchUser(id: userId) { [weak self] result in
switch result {
case .success(let user):
print("获取用户成功:\(user.name)")
// 3. 发起支付(低耦合 + 可扩展)
self?.paymentService.pay(amount: amount)
case .failure(let error):
print("获取用户失败:\(error)")
}
}
}
}
// MARK: - 示例入口(方便在命令行或 Playgrounds 中运行)
func runDemo() {
// 高内聚示例:温度转换
let converter = TemperatureConverter()
let celsius = 25.0
let fahrenheit = converter.celsiusToFahrenheit(celsius)
print("温度转换示例:\(celsius)℃ = \(fahrenheit)℉")
// 低耦合 & 可扩展示例:切换不同支付方式,只改注入
let payment: PaymentService = CreditCardPayService()
let viewModel = CheckoutViewModel(paymentService: payment)
// 假设 username / userId / amount 来自 UI 输入
viewModel.checkout(username: "Sky", userId: 1, amount: 199.0)
}
// 在命令行 Swift 或 Playground 中可以直接调用
runDemo()

