协议仍然是Swift的一个组成部分 - 无论是语言本身的设计方式,还是标准库的结构方式。因此,Swift的每个版本都添加了与协议相关的新功能,这使得它们更加灵活和强大,这并不奇怪。
特别是在应用程序或系统中设计抽象时,协议提供了一种方法,可以清楚地将不同类型彼此分开,并设置更明确定义的API - 这通常使得从测试到重构的所有内容都变得更加简单。
本周,我们来看看如何使用协议创建多个抽象级别,并尝试一些不同的技术,让我们从一个更通用的协议开始,然后我们越来越专注于变得越来越具体用例。
同时小编这里有些书籍和面试资料哦(点击下载)

遗产
由于我们的每个用户类型都具有上述两个属性,因此我们可以User
使用一系列空扩展轻松地使它们符合我们的协议:
extension AnonymousUser: User {}
extension Member: User {}
extension Admin: User {}
执行上述操作已经非常有用,因为我们现在能够定义接受任何功能的功能User
,而无需了解有关存在的不同类型用户的任何具体信息。但是,使用协议继承,我们可以更进一步,并为特定功能创建协议的专用版本。
以身份验证为例 - 成员和管理员都经过身份验证,因此能够共享处理这两者之间身份验证的代码真的很不错 - 不需要我们的AnonymousUser
类型对此有任何了解。
为此,让我们创建另一个协议 - AuthenticatedUser
继承自我们的标准User
协议,然后添加与身份验证相关的新属性 - 例如accessToken
。我们将制作两者Member
并Admin
符合它,如下所示:
protocol AuthenticatedUser: User {
var accessToken: AccessToken { get }
}
extension Member: AuthenticatedUser {}
extension Admin: AuthenticatedUser {}
我们现在可以AuthenticatedUser
在需要登录用户的上下文中使用我们的新协议,例如在创建从需要身份验证的后端端点加载数据的请求时:
class DataLoader {
func load(from endpoint: ProtectedEndpoint,
onBehalfOf user: AuthenticatedUser,
then: @escaping (Result<Data>) -> Void) {
// Since 'AuthenticatedUser' inherits from 'User', we
// get full access to all properties from both protocols.
let request = makeRequest(for: endpoint,
userID: user.id,
accessToken: user.accessToken)
...
}
}
设置较小协议的层次结构,通过继承,变得越来越专业化也可以是避免类型转换和非可选选项的好方法 - 因为我们只需要将更具体的属性和方法添加到实际支持它们的类型中。不再需要空方法实现或属性nil
,只是为了符合协议。
专业化
接下来,让我们看一下当从使用相关类型的协议继承时,我们如何进一步专门化子协议。假设我们正在为应用程序构建一个组件驱动的UI系统,其中组件可以以不同的方式实现 - 例如使用a UIView
,a UIViewController
或a CALayer
。为了实现这种程度的灵活性,我们将从一个非常通用的协议开始,该协议Component
使每个组件能够决定可以添加到哪种容器:
protocol Component {
associatedtype Container
func add(to container: Container)
}
我们的大多数组件可能都是使用视图实现的 - 所以为方便起见,我们将为此创建一个专门版本Component
。就像之前使用我们的用户协议一样,我们将ViewComponent
继承我们的新协议Component
,但附加的扭曲我们将要求它的容器类型是某种类型UIView
。这可以通过使用where
子句的泛型约束来完成,如下所示:
protocol ViewComponent: Component where Container: UIView {
associatedtype View: UIView
var view: View { get }
}
执行上述操作的另一种方法是where
每次我们想要处理UIView
基于组件的组件时都使用该子句,但为它设置专用协议可以帮助我们删除大量样板并使事情更加简化。
既然我们有编译时保证所有ViewComponent
组件都具有视图,并且它们的容器类型也是视图,我们可以使用协议扩展来添加一些基本协议要求的默认实现 - 如下所示:
extension ViewComponent {
func add(to container: Container) {
container.addSubview(view)
}
}
这是Swift类型系统变得多么强大的另一个例子,以及它如何让我们既能够实现高度的灵活性,又能减少样板 - 使用通用约束和协议扩展等功能。
组成
最后,我们来看看如何通过组合来专门化协议。假设我们有一个Operation
协议用于实现各种异步操作。由于我们对所有操作使用单一协议,因此它目前要求我们为每个操作实现相当多的不同方法:
protocol Operation {
associatedtype Input
associatedtype Output
func prepare()
func cancel()
func perform(with input: Input,
then handler: @escaping (Output) -> Void)
}
像我们上面那样设置更大的协议并不是真正的错误,但是如果我们 - 例如 - 某些操作无法取消或者不需要任何特定的准备(因为我们仍然是需要实现这些协议方法)。
这是我们可以使用组合解决的问题。让我们再次从标准库中获取一些灵感 - 这次通过查看Codable
类型,这实际上只是一个组成两个协议的类型 - Decodable
和Encodable
:
typealias Codable = Decodable & Encodable
上述方法的优点在于类型可以自由地仅符合Decodable
或者Encodable
,并且我们可以编写仅处理解码或编码的函数,同时仍然可以使用单一类型来引用两者。
使用相同的技术,我们可以将我们的Operation
协议从之前分解为三个独立的协议,每个协议专用于一个任务:
protocol Preparable {
func prepare()
}
protocol Cancellable {
func cancel()
}
protocol Performable {
associatedtype Input
associatedtype Output
func perform(with input: Input,
then handler: @escaping (Output) -> Void)
}
然后,就像标准库定义的那样Codable
,我们可以添加一个typealias来将所有这三个独立的协议组合成一个Operation
类型 - 就像我们之前使用的更大的协议一样:
typealias Operation = Preparable & Cancellable & Performable
上述方法的好处是,我们现在可以Operation
根据每种类型的功能选择性地符合我们协议的不同方面。我们还能够在不同的上下文中重用我们较小的协议 - 例如Cancellable
现在可以被各种类型的可取消类型使用 - 不仅仅是操作,这使我们可以编写更多通用代码,比如这个扩展Sequence
允许我们轻松取消任何使用单个调用的可取消类型序列:
extension Sequence where Element == Cancellable {
func cancelAll() {
forEach { $0.cancel() }
}
}
太酷了!👍使用像这样的较小的构建块样式协议的另一大好处是,创建用于测试的模拟变得更加容易,因为我们将只能模拟我们正在测试的API实际使用的方法和属性。
结论
随着协议不断变得越来越强大,使用它们的更多方式也开放了。就像标准库如何大量使用协议来重用各种类型之间的算法和其他代码一样,我们也可以使用专门的协议来设置多个抽象级别 - 每个级别专门用于解决一组特定的问题。
然而,就像设计任何类型的抽象一样,同样重要的是不要过早地得出协议是正确选择的结论。尽管Swift通常被称为“面向协议的语言”,但协议有时会增加所需的开销和复杂性。有时只使用具体类型就足够了,也许是一个更好的起点 - 因为类型通常可以在以后通过协议进行改造。
你怎么看?您是否经常创建多个专用版本的协议,或者您将尝试使用它?通过加我们的交流群 点击此处进交流群 ,来一起交流或者发布您的问题,意见或反馈。
网友评论