第5章:协议的依赖注入与解耦
5.4 实战案例:重构一个耦合严重的模块
在实际开发中,耦合严重的代码往往难以维护和测试。通过协议化编程(POP)和依赖注入(DI),我们可以将紧耦合的模块重构为松耦合、可测试的结构。本节将通过一个实战案例,展示如何识别耦合问题,并利用协议重构一个耦合严重的模块,逐步将其转化为模块化、可扩展的设计。
案例背景
假设我们有一个电商应用的支付模块,初始实现如下:
class CheckoutManager {
private let paymentProcessor = StripeProcessor()
private let logger = FileLogger()
func checkout(amount: Double, userId: String) -> Bool {
logger.log("Starting checkout for user \(userId) with amount \(amount)")
let success = paymentProcessor.process(amount: amount)
if success {
logger.log("Checkout succeeded for user \(userId)")
} else {
logger.log("Checkout failed for user \(userId)")
}
return success
}
}
class StripeProcessor {
func process(amount: Double) -> Bool {
print("Processing \(amount) via Stripe")
return true // 模拟支付成功
}
}
class FileLogger {
func log(_ message: String) {
print("File log: \(message)") // 模拟写入文件
}
}
let checkout = CheckoutManager()
checkout.checkout(amount: 99.99, userId: "u123")
// 输出:
// File log: Starting checkout for user u123 with amount 99.99
// Processing 99.99 via Stripe
// File log: Checkout succeeded for user u123
问题分析
- 紧耦合:
CheckoutManager直接依赖具体类StripeProcessor和FileLogger。- 无法切换支付方式或日志记录方式。
- 不可测试:
- 测试时无法隔离支付和日志逻辑,依赖真实实现。
- 扩展性差:
- 添加新支付方式(如 PayPal)或日志方式(如控制台)需要修改
CheckoutManager。
- 添加新支付方式(如 PayPal)或日志方式(如控制台)需要修改
重构目标
- 使用协议抽象依赖。
- 通过依赖注入解耦。
- 提高测试性和扩展性。
第一步:定义协议
提取支付和日志的接口:
protocol PaymentProcessor {
func process(amount: Double) -> Bool
}
protocol Logger {
func log(_ message: String)
}
将现有实现改为遵循协议:
class StripeProcessor: PaymentProcessor {
func process(amount: Double) -> Bool {
print("Processing \(amount) via Stripe")
return true
}
}
class FileLogger: Logger {
func log(_ message: String) {
print("File log: \(message)")
}
}
第二步:注入依赖
修改 CheckoutManager,通过构造函数注入依赖:
class CheckoutManager {
private let paymentProcessor: PaymentProcessor
private let logger: Logger
init(paymentProcessor: PaymentProcessor, logger: Logger) {
self.paymentProcessor = paymentProcessor
self.logger = logger
}
func checkout(amount: Double, userId: String) -> Bool {
logger.log("Starting checkout for user \(userId) with amount \(amount)")
let success = paymentProcessor.process(amount: amount)
if success {
logger.log("Checkout succeeded for user \(userId)")
} else {
logger.log("Checkout failed for user \(userId)")
}
return success
}
}
let checkout = CheckoutManager(paymentProcessor: StripeProcessor(), logger: FileLogger())
checkout.checkout(amount: 99.99, userId: "u123")
// 输出与之前相同
- 依赖从硬编码变为外部注入,解耦完成。
第三步:扩展功能
添加新的支付和日志实现,验证扩展性:
class PayPalProcessor: PaymentProcessor {
func process(amount: Double) -> Bool {
print("Processing \(amount) via PayPal")
return true
}
}
struct ConsoleLogger: Logger {
func log(_ message: String) {
print("Console log: \(message)")
}
}
let paypalCheckout = CheckoutManager(paymentProcessor: PayPalProcessor(), logger: ConsoleLogger())
paypalCheckout.checkout(amount: 50.0, userId: "u456")
// 输出:
// Console log: Starting checkout for user u456 with amount 50.0
// Processing 50.0 via PayPal
// Console log: Checkout succeeded for user u456
- 无需修改
CheckoutManager,即可支持新实现。
第四步:添加单元测试
为测试创建 Mock 对象:
struct MockPaymentProcessor: PaymentProcessor {
let shouldSucceed: Bool
func process(amount: Double) -> Bool {
print("Mock payment of \(amount)")
return shouldSucceed
}
}
struct MockLogger: Logger {
var messages: [String] = []
func log(_ message: String) {
messages.append(message)
}
}
编写测试用例:
import XCTest
@testable import YourModule
class CheckoutManagerTests: XCTestCase {
func testCheckoutSuccess() {
let mockPayment = MockPaymentProcessor(shouldSucceed: true)
let mockLogger = MockLogger()
let checkout = CheckoutManager(paymentProcessor: mockPayment, logger: mockLogger)
let result = checkout.checkout(amount: 25.0, userId: "u789")
XCTAssertTrue(result)
XCTAssertEqual(mockLogger.messages, [
"Starting checkout for user u789 with amount 25.0",
"Checkout succeeded for user u789"
])
}
func testCheckoutFailure() {
let mockPayment = MockPaymentProcessor(shouldSucceed: false)
let mockLogger = MockLogger()
let checkout = CheckoutManager(paymentProcessor: mockPayment, logger: mockLogger)
let result = checkout.checkout(amount: 10.0, userId: "u999")
XCTAssertFalse(result)
XCTAssertEqual(mockLogger.messages, [
"Starting checkout for user u999 with amount 10.0",
"Checkout failed for user u999"
])
}
}
- 测试验证了成功和失败场景,检查逻辑和日志交互。
重构后的优势
- 松耦合:
CheckoutManager只依赖协议,不关心具体实现。
- 可测试性:
- Mock 对象隔离了支付和日志依赖,测试简单高效。
- 可扩展性:
- 添加新支付方式或日志方式只需实现协议并注入。
- 可维护性:
- 模块职责清晰,修改某一实现不影响其他部分。
进一步优化
- 协议扩展:为
Logger添加默认实现:extension Logger { func logError(_ message: String) { log("ERROR: \(message)") } } checkout.logger.logError("Payment issue") // 可直接使用 - 依赖容器:在大型项目中,可使用 DI 容器管理依赖:
struct Dependencies { let paymentProcessor: PaymentProcessor let logger: Logger } let deps = Dependencies(paymentProcessor: StripeProcessor(), logger: FileLogger()) let checkout = CheckoutManager(paymentProcessor: deps.paymentProcessor, logger: deps.logger)
注意事项
- 重构成本:初始重构需投入时间,但长期收益显著。
- 协议粒度:避免过于细化,保持接口简洁。
- 测试覆盖:确保测试涵盖所有关键路径。
小结
通过重构一个耦合严重的支付模块,我们展示了协议如何将紧耦合代码转化为松耦合、可测试的设计。这一过程体现了 POP 和 DI 的核心价值,为开发者提供了一个实用的重构模板。下一节将探讨协议在模块化架构中的应用,进一步扩展这些实践。
