MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Swift协议与抽象设计模式

2021-02-182.9k 阅读

Swift 协议基础

协议定义与基础使用

在 Swift 编程中,协议是一种定义方法、属性和其他需求的蓝图,它并不提供这些需求的实现,而是规定了遵循该协议的类型必须满足的条件。定义协议使用 protocol 关键字。

以下是一个简单的协议定义示例:

protocol Identifiable {
    var id: String { get }
}

在上述示例中,Identifiable 协议定义了一个只读属性 id,类型为 String。任何遵循 Identifiable 协议的类型都必须提供 id 属性的实现。

结构体和类都可以遵循协议,示例如下:

struct User: Identifiable {
    let id: String
}

class Product: Identifiable {
    let id: String
    init(id: String) {
        self.id = id
    }
}

User 结构体和 Product 类中,都实现了 Identifiable 协议要求的 id 属性,因此它们都遵循了 Identifiable 协议。

协议中的方法要求

协议不仅可以定义属性,还能定义方法。方法可以是实例方法,也可以是类方法(对于类类型而言)。

  1. 实例方法
protocol Runnable {
    func run()
}

struct Runner: Runnable {
    func run() {
        print("Running...")
    }
}

在这个例子中,Runnable 协议定义了一个 run 实例方法。Runner 结构体通过实现 run 方法来遵循 Runnable 协议。

  1. 类方法
protocol Loggable {
    static func log(message: String)
}

class Logger: Loggable {
    static func log(message: String) {
        print("Log: \(message)")
    }
}

Loggable 协议定义了一个静态(类)方法 logLogger 类通过实现该静态方法来遵循 Loggable 协议。

协议中的可选项

在 Swift 协议中,可以定义可选的属性和方法。要使用可选成员,需要在协议定义前添加 @objc 关键字,并且该协议必须继承自 NSObjectProtocol。这是因为可选成员是基于 Objective - C 运行时机制实现的。

@objc protocol OptionalProtocol: NSObjectProtocol {
    @objc optional func optionalMethod()
    @objc optional var optionalProperty: Int { get }
}

class OptionalClass: OptionalProtocol {
    // 不需要实现可选方法和属性
}

let optionalObject = OptionalClass()
if let method = optionalObject.optionalMethod {
    method()
}

在上述代码中,OptionalProtocol 定义了一个可选方法 optionalMethod 和一个可选属性 optionalPropertyOptionalClass 类遵循该协议,但不需要实现可选成员。通过可选链的方式可以安全地调用可选方法。

协议与多态

基于协议的多态实现

多态是面向对象编程的重要特性之一,在 Swift 中,协议为实现多态提供了强大的支持。当多个类型遵循同一个协议时,可以将它们视为同一类型(即协议类型),从而实现多态行为。

protocol Shape {
    func area() -> Double
}

struct Circle: Shape {
    let radius: Double
    func area() -> Double {
        return .pi * radius * radius
    }
}

struct Rectangle: Shape {
    let width: Double
    let height: Double
    func area() -> Double {
        return width * height
    }
}

let shapes: [Shape] = [Circle(radius: 5), Rectangle(width: 4, height: 6)]
for shape in shapes {
    print("Area: \(shape.area())")
}

在这段代码中,CircleRectangle 结构体都遵循 Shape 协议,并且实现了 area 方法。通过将 CircleRectangle 的实例放入 Shape 类型的数组中,可以以统一的方式调用它们的 area 方法,体现了多态性。

协议类型的参数与返回值

协议类型可以作为函数的参数类型和返回值类型,这进一步增强了基于协议的多态性。

func totalArea(shapes: [Shape]) -> Double {
    var total = 0.0
    for shape in shapes {
        total += shape.area()
    }
    return total
}

let total = totalArea(shapes: shapes)
print("Total Area: \(total)")

totalArea 函数中,接受一个 Shape 类型数组作为参数,这意味着可以传入任何遵循 Shape 协议的类型数组。函数内部通过调用每个形状的 area 方法来计算总面积,展示了协议类型作为参数在多态实现中的应用。

协议扩展实现多态行为

协议扩展允许为协议提供默认实现。这对于为遵循协议的所有类型提供通用行为非常有用,同时也有助于实现多态。

protocol Printable {
    func printDescription()
}

extension Printable {
    func printDescription() {
        print("Default description")
    }
}

struct Animal: Printable {}

struct Dog: Printable {
    override func printDescription() {
        print("I'm a dog")
    }
}

let animal = Animal()
animal.printDescription() // 输出 "Default description"

let dog = Dog()
dog.printDescription() // 输出 "I'm a dog"

在这个例子中,Printable 协议通过扩展提供了 printDescription 方法的默认实现。Animal 结构体遵循 Printable 协议但没有重写 printDescription 方法,因此使用默认实现。而 Dog 结构体重写了 printDescription 方法,提供了特定的行为,展示了协议扩展在多态实现中的作用。

协议组合与类型约束

协议组合

有时候,一个类型可能需要遵循多个协议。Swift 提供了协议组合(Protocol Composition)的方式来满足这种需求。协议组合使用 & 符号将多个协议连接起来。

protocol Readable {
    func read()
}

protocol Writable {
    func write(data: String)
}

class File: Readable, Writable {
    func read() {
        print("Reading file...")
    }
    func write(data: String) {
        print("Writing \(data) to file...")
    }
}

func operateOnFile(file: Readable & Writable) {
    file.read()
    file.write(data: "Some data")
}

let file = File()
operateOnFile(file: file)

在上述代码中,File 类同时遵循 ReadableWritable 协议。operateOnFile 函数接受一个同时遵循这两个协议的参数,通过协议组合 Readable & Writable 来指定参数类型,这样函数内部可以调用 readwrite 方法。

类型约束

类型约束用于限制泛型类型必须遵循特定的协议。在泛型函数或泛型类型定义中,可以使用 where 子句来添加类型约束。

func find<T: Equatable>(array: [T], value: T) -> Int? {
    for (index, element) in array.enumerated() {
        if element == value {
            return index
        }
    }
    return nil
}

let numbers = [1, 2, 3, 4]
if let index = find(array: numbers, value: 3) {
    print("Value found at index \(index)")
}

find 函数中,T 是泛型类型,通过 T: Equatable 约束 T 必须遵循 Equatable 协议,这样才能在函数内部使用 == 操作符进行比较。

关联类型

关联类型为协议中的类型提供了一个占位符,实际的类型在遵循协议时确定。使用 associatedtype 关键字来定义关联类型。

protocol Container {
    associatedtype Item
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}

struct Stack<Element>: Container {
    // Element 成为 Container 协议中 Item 的实际类型
    var items = [Element]()
    mutating func append(_ item: Element) {
        items.append(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> Element {
        return items[i]
    }
}

let stack = Stack<Int>()
stack.append(10)
print(stack.count)
print(stack[0])

Container 协议中,Item 是关联类型。Stack 结构体遵循 Container 协议时,Element 类型被用作 Item 的实际类型,展示了关联类型在协议定义中的灵活性。

抽象设计模式与协议

策略模式

策略模式(Strategy Pattern)是一种行为型设计模式,它定义了一系列算法,并将每个算法封装起来,使它们可以相互替换。在 Swift 中,可以通过协议来实现策略模式。

protocol PaymentStrategy {
    func pay(amount: Double)
}

class CreditCardPayment: PaymentStrategy {
    let cardNumber: String
    init(cardNumber: String) {
        self.cardNumber = cardNumber
    }
    func pay(amount: Double) {
        print("Paying \(amount) with credit card \(cardNumber)")
    }
}

class PayPalPayment: PaymentStrategy {
    let email: String
    init(email: String) {
        self.email = email
    }
    func pay(amount: Double) {
        print("Paying \(amount) with PayPal account \(email)")
    }
}

class ShoppingCart {
    var paymentStrategy: PaymentStrategy?
    func setPaymentStrategy(_ strategy: PaymentStrategy) {
        self.paymentStrategy = strategy
    }
    func checkout(amount: Double) {
        guard let strategy = paymentStrategy else {
            print("No payment strategy set")
            return
        }
        strategy.pay(amount: amount)
    }
}

let cart = ShoppingCart()
let creditCard = CreditCardPayment(cardNumber: "1234567890123456")
cart.setPaymentStrategy(creditCard)
cart.checkout(amount: 50.0)

let paypal = PayPalPayment(email: "user@example.com")
cart.setPaymentStrategy(paypal)
cart.checkout(amount: 30.0)

在上述代码中,PaymentStrategy 协议定义了支付行为的抽象。CreditCardPaymentPayPalPayment 类遵循该协议,实现了不同的支付方式。ShoppingCart 类持有一个 PaymentStrategy 类型的属性,通过设置不同的策略对象,可以在运行时选择不同的支付方式,这正是策略模式的核心思想。

代理模式

代理模式(Proxy Pattern)是一种结构型设计模式,它为其他对象提供一种代理以控制对这个对象的访问。在 Swift 中,协议是实现代理模式的常用手段。

protocol WebServiceDelegate {
    func didReceiveData(data: Data)
    func didFailWithError(error: Error)
}

class WebService {
    var delegate: WebServiceDelegate?
    func fetchData() {
        // 模拟网络请求
        let success = Bool.random()
        if success {
            let data = Data("Sample data".utf8)
            delegate?.didReceiveData(data: data)
        } else {
            let error = NSError(domain: "WebServiceError", code: 404, userInfo: nil)
            delegate?.didFailWithError(error: error)
        }
    }
}

class ViewController: WebServiceDelegate {
    func didReceiveData(data: Data) {
        if let text = String(data: data, encoding: .utf8) {
            print("Received data: \(text)")
        }
    }
    func didFailWithError(error: Error) {
        print("Error: \(error.localizedDescription)")
    }
}

let service = WebService()
let viewController = ViewController()
service.delegate = viewController
service.fetchData()

在这个例子中,WebServiceDelegate 协议定义了处理网络请求结果的方法。WebService 类持有一个 WebServiceDelegate 类型的代理属性,并在网络请求完成后调用代理的方法。ViewController 类遵循 WebServiceDelegate 协议,实现了处理数据和错误的方法,作为 WebService 的代理来处理网络请求的结果。

适配器模式

适配器模式(Adapter Pattern)是一种结构型设计模式,它允许将一个类的接口转换成客户希望的另一个接口。在 Swift 中,可以通过协议和类的组合来实现适配器模式。

protocol Target {
    func request()
}

class Adaptee {
    func specificRequest() {
        print("Specific request from Adaptee")
    }
}

class Adapter: Target {
    let adaptee: Adaptee
    init(adaptee: Adaptee) {
        self.adaptee = adaptee
    }
    func request() {
        adaptee.specificRequest()
    }
}

let adaptee = Adaptee()
let adapter = Adapter(adaptee: adaptee)
adapter.request()

在上述代码中,Target 协议定义了目标接口。Adaptee 类有一个不同的方法 specificRequestAdapter 类遵循 Target 协议,并持有一个 Adaptee 实例,通过在 request 方法中调用 adaptee.specificRequest,将 Adaptee 的接口适配成 Target 的接口。

协议的高级特性与最佳实践

协议继承

协议可以继承自其他协议,从而扩展其功能。一个协议可以继承多个协议,通过逗号分隔。

protocol BasicShape {
    var name: String { get }
}

protocol ShapeWithArea: BasicShape {
    func area() -> Double
}

struct Square: ShapeWithArea {
    let name = "Square"
    let sideLength: Double
    func area() -> Double {
        return sideLength * sideLength
    }
}

在这个例子中,ShapeWithArea 协议继承自 BasicShape 协议,它不仅拥有 BasicShape 协议定义的 name 属性要求,还添加了 area 方法要求。Square 结构体遵循 ShapeWithArea 协议,需要实现这两个要求。

协议与泛型的高级结合

在 Swift 中,协议和泛型可以结合使用,创造出非常强大和灵活的代码结构。

protocol Transformable {
    associatedtype Output
    func transform() -> Output
}

struct NumberTransformer: Transformable {
    let number: Int
    typealias Output = Double
    func transform() -> Double {
        return Double(number) * 2.0
    }
}

func transformAll<T: Transformable>(items: [T]) -> [T.Output] {
    var results = [T.Output]()
    for item in items {
        results.append(item.transform())
    }
    return results
}

let transformers = [NumberTransformer(number: 1), NumberTransformer(number: 2)]
let transformedValues = transformAll(items: transformers)
print(transformedValues)

在上述代码中,Transformable 协议使用关联类型 Output 来表示转换后的结果类型。NumberTransformer 结构体遵循 Transformable 协议并指定 OutputDoubletransformAll 函数是一个泛型函数,接受一个遵循 Transformable 协议的类型数组,并返回转换后的结果数组,展示了协议与泛型的高级结合应用。

最佳实践建议

  1. 协议命名:协议名称应使用驼峰命名法,并且通常以 “able” 或 “ible” 结尾,以表示遵循该协议的类型具有某种能力,如 IdentifiableRunnable 等。
  2. 避免过度设计:虽然协议提供了强大的抽象能力,但不应过度使用,导致代码变得复杂难以理解。在设计协议时,要确保协议的职责清晰,粒度适中。
  3. 文档化协议:为协议添加文档注释,说明协议的用途、要求以及遵循协议的类型应注意的事项,这有助于其他开发者理解和使用你的代码。
  4. 使用协议扩展提供默认实现:当多个遵循协议的类型可能具有相同的行为时,通过协议扩展提供默认实现可以减少代码重复,同时也方便遵循协议的类型根据需要重写默认行为。

通过深入理解和灵活运用 Swift 协议与抽象设计模式,可以编写出更加灵活、可维护和可扩展的代码,提升程序的质量和开发效率。无论是构建小型应用还是大型框架,协议和抽象设计模式都能发挥重要作用。在实际开发中,应根据具体的需求和场景,合理选择和应用这些技术,以实现最佳的编程效果。