第3章:协议扩展(Protocol Extensions)
3.5 实战案例:通过协议扩展实现通用功能
协议扩展不仅是一个理论工具,更是实际开发中提升代码复用性和模块化的利器。在本节中,我们将通过一个实战案例展示如何利用协议扩展实现通用功能。具体来说,我们将设计一个简单的“通知系统”,通过协议和扩展为不同类型的对象提供通知发送和日志记录功能。这一案例将综合运用协议定义、默认实现和条件约束,体现协议化编程(POP)的实用价值。
案例背景
假设我们要开发一个应用,需要在某些事件发生时发送通知(例如打印到控制台或写入文件),并记录事件日志。通知的发送方式和日志格式可能因对象类型而异,但我们希望核心逻辑可以复用。我们将使用协议扩展来实现这一目标。
设计协议
首先,定义两个核心协议:一个用于通知,一个用于日志记录。
// 表示可发送通知的对象
protocol Notifiable {
var identifier: String { get }
func sendNotification(message: String)
}
// 表示可记录日志的对象
protocol Loggable {
var logPrefix: String { get }
func log(_ event: String)
}
Notifiable定义了通知的基本要求:标识符和发送方法。Loggable定义了日志记录的要求:前缀和记录方法。
添加默认实现
通过协议扩展为这两个协议提供默认实现:
extension Notifiable {
func sendNotification(message: String) {
print("Notification from \(identifier): \(message)")
}
}
extension Loggable {
func log(_ event: String) {
print("\(logPrefix) - \(event) at \(Date())")
}
}
sendNotification默认将通知打印到控制台。log默认记录事件和时间戳。
实现具体类型
现在,创建几个遵循这些协议的具体类型:
// 表示用户,遵循 Notifiable
struct User: Notifiable {
var identifier: String
}
// 表示设备,遵循 Notifiable 和 Loggable
struct Device: Notifiable, Loggable {
var identifier: String
var logPrefix: String {
return "[Device \(identifier)]"
}
// 自定义通知发送方式
func sendNotification(message: String) {
print("Device \(identifier) alert: \(message)")
}
}
let user = User(identifier: "alice")
let device = Device(identifier: "dev001")
user.sendNotification(message: "Welcome!")
// 输出: Notification from alice: Welcome!
device.sendNotification(message: "Low battery")
// 输出: Device dev001 alert: Low battery
device.log("Started")
// 输出: [Device dev001] - Started at [当前时间]
User使用Notifiable的默认实现。Device覆盖了sendNotification,但复用了Loggable的默认实现。
使用 where 子句增强功能
假设我们想为支持日志记录的通知对象添加额外功能,例如记录通知发送事件。可以使用 where 子句约束扩展:
extension Notifiable where Self: Loggable {
func sendAndLog(message: String) {
sendNotification(message: message)
log("Notification sent: \(message)")
}
}
// 只有 Device 能使用此方法,因为它同时遵循 Notifiable 和 Loggable
device.sendAndLog(message: "Update available")
// 输出:
// Device dev001 alert: Update available
// [Device dev001] - Notification sent: Update available at [当前时间]
// user.sendAndLog() // 编译错误:User 未遵循 Loggable
sendAndLog结合了通知和日志功能,仅对同时遵循Notifiable和Loggable的类型生效。
添加更复杂的通用功能
我们可以进一步扩展功能,例如为通知添加优先级支持:
protocol Prioritized {
var priority: Int { get }
}
extension Notifiable where Self: Prioritized {
func sendPriorityNotification(message: String) {
let prefix = priority > 5 ? "[HIGH]" : "[LOW]"
sendNotification(message: "\(prefix) \(message)")
}
}
struct AlertDevice: Notifiable, Loggable, Prioritized {
var identifier: String
var logPrefix: String { "[Alert \(identifier)]" }
var priority: Int
}
let alert = AlertDevice(identifier: "alert001", priority: 7)
alert.sendPriorityNotification(message: "System failure")
// 输出: Notification from alert001: [HIGH] System failure
alert.sendAndLog(message: "Maintenance scheduled")
// 输出:
// Notification from alert001: Maintenance scheduled
// [Alert alert001] - Notification sent: Maintenance scheduled at [当前时间]
Prioritized协议添加了优先级要求。sendPriorityNotification根据优先级动态调整通知格式,仅对支持优先级的类型生效。
案例分析:协议扩展的优势
- 复用性:默认实现(如
sendNotification和log)避免了重复代码。 - 灵活性:遵循者可以覆盖默认实现(
Device的通知)或使用扩展功能(sendAndLog)。 - 模块化:通过
where子句,功能被精确分配给符合条件的类型。 - 类型安全:Swift 的类型系统确保只有满足条件的类型才能访问特定扩展。
优化与分派考虑
- 静态分派:通过具体类型调用(如
device.sendNotification)使用静态分派,性能高效。 - 动态分派:通过协议类型调用(如
let n: Notifiable = device)会触发动态分派,可能有轻微开销。 - 建议:在性能敏感场景中,尽量使用具体类型调用扩展方法。
注意事项
- 依赖清晰:扩展中的默认实现应只依赖协议要求的数据(如
identifier和logPrefix)。 - 功能边界:避免在扩展中添加过于复杂的逻辑,以保持可维护性。
- 测试覆盖:为默认实现和覆盖实现编写单元测试,确保行为一致。
小结
通过这个通知系统案例,我们展示了如何利用协议扩展实现通用功能。默认实现提供了基础行为,where 子句增强了条件适配性,而不同类型的遵循者则展示了灵活性。这种设计体现了 POP 的核心思想:抽象行为、复用逻辑、松耦合。下一章将探讨协议与泛型的结合,进一步扩展这些概念的应用范围。
