Swift 中的面向协议编程

发布日期:2026-07-12 04:07:04   来源 : 杭州电子商务研究院    浏览量 :5
杭州电子商务研究院 发布日期:2026-07-12 04:07:04  
5

介绍

什么是 POP,为什么要使用它,它在现实中如何发挥作用?

Swift - 第一种 POP 语言

在 2015 年 WWDC 上,苹果宣布 Swift 是世界上第一个面向协议编程(POP)语言。

那么 POP 是什么?

面向协议编程是 Swift 2.0 引入的一种新编程范式。在面向协议的方法中,我们通过定义协议来开始设计我们的系统。我们依赖新概念:协议扩展、协议继承和协议组合。该范式还改变了我们看待语义的方式。在 Swift 中,值类型优于类。但是,面向对象的概念不适用于结构和枚举:结构不能从另一个结构继承,枚举也不能从另一个枚举继承。因此,继承 fa(面向对象的基本概念之一)不能应用于值类型。另一方面,值类型可以从协议甚至多个协议继承。因此,使用 POP,值类型已成为 Swift 中的一等公民。

从协议开始

在设计软件系统时,我们会尝试确定满足给定系统要求所需的元素。然后,我们为这些元素之间的关系建模。我们可以从超类开始,并通过继承为其关系建模。或者,我们可以从协议开始,并将关系建模为协议实现。Swift 为这两种解释提供了全面支持。但是,Apple 告诉我们:

“不要从课程开始,而要从协议开始。”

为何?因为协议比类更能起到抽象的作用。

如果使用类来建模抽象,则需要依赖继承。超类定义核心功能并将其公开给子类。子类可以完全覆盖该行为、添加特定行为或让超类完成所有工作。这很好用,直到您意识到需要来自其他类的更多功能。Swift 与许多其他编程语言一样,不支持多重继承。按照类优先的方法,您必须不断向超类添加新功能或以其他方式创建新的中间类,从而使问题复杂化。另一方面,协议充当蓝图而不是父类。协议通过描述实现类型应实现的内容来建模抽象。让我们以以下协议为例:

      protocol Entity {
    var name: String {get set}
    static func uid() -> String
}
    

它告诉我们,该协议的采用者将能够通过实现类型方法 uid() 来创建一个实体、为其分配一个名称并生成其唯一标识符。

一种类型可以建模多个抽象,因为任何类型(包括值类型)都可以实现多个协议。与类继承相比,这是一个巨大的优势。您可以根据需要创建尽可能多的协议和协议扩展来分离关注点。告别单片超类吧!唯一的警告是协议抽象地定义模板——没有实现。这就是协议扩展可以拯救的地方。

POP 的支柱

协议扩展

协议充当蓝图:它们告诉我们采用者应实现什么,但您无法在协议中提供实现。如果我们需要为符合类型定义默认行为怎么办?我们需要在基类中实现它,对吗?错!必须依赖基类来实现默认实现会削弱协议的优势。此外,这对值类型不起作用。幸运的是,还有另一种方法:协议扩展是可行的方法!在 Swift 中,您可以扩展协议并为方法、计算属性、下标和便利初始化器提供默认实现。在下面的示例中,我为类型方法 uid() 提供了默认实现。

      extension Entity {
    static func uid() -> String {
        return UUID().uuidString
    }
}
    

现在采用该协议的类型不再需要实现 uid() 方法。

      struct Order: Entity {
    var name: String
    let uid: String = Order.uid()
}
let order = Order(name: "My Order")
print(order.uid)
// 4812B485-3965-443B-A76D-72986B0A4FF4
    

协议继承

协议可以继承自其他协议,然后在继承的要求之上添加进一步的要求。在下面的示例中,Persistable 协议继承自我之前介绍的 Entity 协议。它添加了将实体保存到文件并根据其唯一标识符加载它的要求。

      protocol Persistable: Entity {
    func write(instance: Entity, to filePath: String)
    init?(by uid: String)
}
    

采用 Persistable 协议的类型必须满足 Entity 和 Persistable 协议中定义的要求。

如果您的类型需要持久性功能,它应该实现 Persistable 协议。

      struct PersistableEntity: Persistable {
    var name: String
    func write(instance: Entity, to filePath: String) { // ...
    }  
    init?(by uid: String) {
        // try to load from the filesystem based on id
    }
}
    

而不需要持久化的类型只需实现实体协议:

      struct InMemoryEntity: Entity {
    var name: String
}
    

协议继承是一项强大的功能,它允许更细粒度和更灵活的设计。

协议组成

Swift 不允许类多重继承。但是,Swift 类型可以采用多个协议。有时您可能会发现此功能很有用。

这里有一个例子:假设我们需要一个代表实体的类型。

我们还需要比较给定类型的实例。我们还想提供自定义描述。

我们有三个协议来定义所提到的要求:

  • 实体
  • 可等价
  • 自定义字符串可转换

如果这些是基类,我们必须将功能合并到一个超类中;但是,通过 POP 和协议组合,解决方案变成:

      struct MyEntity: Entity, Equatable, CustomStringConvertible {
    var name: String
    // Equatable
    public static func ==(lhs: MyEntity, rhs: MyEntity) -> Bool {
        return lhs.name == rhs.name
    }
    // CustomStringConvertible
    public var description: String {
        return "MyEntity: \(name)"
    }
}
let entity1 = MyEntity(name: "42")
print(entity1)
let entity2 = MyEntity(name: "42")
assert(entity1 == entity2, "Entities shall be equal")
    

这种设计不仅比将所有必需的功能压缩到一个单一的基类中更灵活,而且适用于值类型。

使用 POP 实现更简洁的设计

我将通过一个示例向您展示面向协议编程相对于传统方法的优势。

我们的目标是创建满足以下要求的类型:

  • 创建给定名称和图像数据的图像
  • 图像应该保存到文件系统并从文件系统加载
  • 创建图像的有损压缩版本
  • 对图像进行 Base64 编码,以便通过互联网传输

超类方式

让我们从基类方法开始。我将所有功能都压缩到 Image 类中。我们得到的是一个完整的类型:我们可以直接创建 Image 类的实例,并且满足所有要求。

      class Image {
    fileprivate var imageName: String
    fileprivate var imageData: Data

    var name: String {
        return imageName
    }

    init(name: String, data: Data) {
        imageName = name
        imageData = data
    }

    // persistence
    func save(to url: URL) throws {
        try self.imageData.write(to: url)
    }

    convenience init(name: String, contentsOf url: URL) throws {
        let data = try Data(contentsOf: url)
        self.init(name: name, data: data)
    }

    // compression
    convenience init?(named name: String, data: Data, compressionQuality: Double) {
        guard let image = UIImage.init(data: data) else { return nil }
        guard let jpegData = UIImageJPEGRepresentation(image, CGFloat(compressionQuality)) else { return nil }
        self.init(name: name, data: jpegData)
    }

    // BASE64 encoding
    var base64Encoded: String {
        return imageData.base64EncodedString()
    }
}

// Test
var image = Image(name: "Pic", data: Data(repeating: 0, count: 100))
print(image.base64Encoded)

do {
    // persist image
    let documentDirectory = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor:nil, create:false)
    let imageURL = documentDirectory.appendingPathComponent("MyImage")
    try image.save(to: imageURL)
    print("Image saved successfully to path \(imageURL)")

    // load image from persistence
    let storedImage = try Image.init(name: "MyRestoredImage", contentsOf: imageURL)
    print("Image loaded successfully from path \(imageURL)")
} catch {
    print(error)
}
    

现在,如果我们不需要所有这些功能怎么办?假​​设我并不总是需要 Base64 编码功能。如果我将 Image 类子类化,我将获得所有功能 - 即使我不需要它们。

如果我们需要创建子类来专门化某些方法,则无法摆脱那些我们不需要的公共方法和属性。我们只能在继承时获得一切。

此外,我们仅限于课程。现在,让我们使用 POP 来改造这个设计。

使用 POP 进行重新设计

我将为每个主要功能创建协议,即持久性、创建压缩​​、有损版本和 Base64 编码。

      protocol NamedImageData {
    var name: String { get }
    var data: Data { get }
    init(name: String, data: Data)
}

protocol ImageDataPersisting: NamedImageData {
    init(name: String, contentsOf url: URL) throws
    func save(to url: URL) throws
}

extension ImageDataPersisting {
    init(name: String, contentsOf url: URL) throws {
        let data = try Data(contentsOf: url)
        self.init(name: name, data: data)
    }

    func save(to url: URL) throws {
        try self.data.write(to: url)
    }
}

protocol ImageDataCompressing: NamedImageData {
    func compress(withQuality compressionQuality: Double) -> Self?
}

extension ImageDataCompressing {
    func compress(withQuality compressionQuality: Double) -> Self? {
        guard let uiImage = UIImage.init(data: self.data) else {
            return nil
        }
        guard let jpegData = UIImageJPEGRepresentation(uiImage, CGFloat(compressionQuality)) else {
            return nil
        }
        return Self(name: self.name, data: jpegData)
    }
}

protocol ImageDataEncoding: NamedImageData {
    var base64Encoded: String { get }
}

extension ImageDataEncoding {
    var base64Encoded: String {
        return self.data.base64EncodedString()
    }
}
    

通过这种方法,我们可以创建更细粒度的设计。您可以创建一个采用所有协议的类型:

      struct MyImage: ImageDataPersisting, ImageDataCompressing, ImageDataEncoding {
    var name: String
    var data: Data
}
    

或者您可能决定跳过对ImageDataPersisting的符合性:

      struct InMemoryImage: NamedImageData, ImageDataCompressing, ImageDataEncoding {
    var name: String
    var data: Data
}
    

最重要的是,您可以选择在类型中采用哪种协议。并且您的类型可以是引用或值类型。如果使用超类实现,则不存在这种灵活性。

另一个好处是我们可以通过协议扩展提供默认实现。实际上,我们甚至可以添加新功能 - 最好的部分是:我们甚至不需要原始代码。我们可以扩展任何 Foundation 或 UIKit 协议并根据需要对其进行装饰,而无需深入研究类结构或其他细节。

最后的想法

Swift 支持多种范式:面向对象编程、面向协议编程和函数式编程。这对于我们软件开发人员来说意味着什么?答案是自由。

选择哪种范式取决于你。如果你愿意,你仍然可以走 OOP 路线。你可以混合搭配。然而,一旦你理解了面向协议编程,你可能就再也不会回头了。

查看我在 Pluralsight 上的 Swift 课程

谢谢!祝您编码愉快!

致谢

我要感谢 Apple 的 Swift 和开发者工具推广者 Marshall Elfstrand 提出的宝贵建议。

以上内容来自杭州电子商务研究院推送
关注
关于我们
热门推荐
合作伙伴
免责声明:本站部分资讯来源于网络,如有侵权请及时联系客服,我们将尽快处理
Copyright © 2025-2027 ToB产业网址导航 公安备案 浙公网安备33010602013138号 浙ICP备16025413号-9
支持 反馈 关注 数据