iOS — Swift高级分享:SWIFT协议的替代方案

毫无疑问,协议是SWIFT总体设计的主要部分-并且可以提供一种很好的方法来创建抽象、分离关注点和提高系统或功能的整体灵活性。通过不强烈地将类型绑定在一起,而是通过更抽象的接口连接代码库的各个部分,我们通常会得到一个更加解耦的体系结构,它允许我们孤立地迭代每个单独的特性。

然而,虽然协议在许多不同的情况下都是一个很好的工具,但它们也有各自的缺点和权衡。本周,让我们来看看其中的一些特性,并探索几种在SWIFT中抽象代码的替代方法-看看它们与使用协议相比如何。

使用闭包的单个需求

使用协议抽象代码的优点之一是它允许我们对多个代码进行分组。所需在一起。例如,PersistedValue协议可能需要两个save和一个load方法-这两种方法都使我们能够在所有这些值之间强制执行一定程度的一致性,并编写用于保存和加载数据的共享实用程序。

然而,并不是所有的抽象都涉及多个需求,并且非常常见的协议只有一个方法或属性-比如这个:

protocol ModelProvider {
associatedtype Model: ModelProtocol
func provideModel() -> Model
}

假设上面的ModelProvider协议用于抽象我们在代码库中加载和提供模型的方式。它使用关联类型,以便让每个实现以非常类型安全的方式声明它提供的模型类型,这是很棒的,因为它使我们能够编写通用代码来执行常见任务,例如为给定模型呈现详细视图:

class DetailViewController<Model: ModelProtocol>: UIViewController {
private let modelProvider: AnyModelProvider<Model>

init<T: ModelProvider>(modelProvider: T) where T.Model == Model {
// We wrap the injected provider in an AnyModelProvider
// instance to be able to store a reference to it.
self.modelProvider = AnyModelProvider(modelProvider)
super.init(nibName: nil, bundle: nil)
}

override func viewDidLoad() {
super.viewDidLoad()

let model = modelProvider.provideModel()
...
}

...
}

虽然上面的代码可以工作,但它说明了使用具有关联类型的协议的缺点之一-我们不能将引用存储到ModelProvider直接。相反,我们必须首先执行类型擦除将我们的协议引用转换成一个具体的类型,这两种类型都会使我们的代码混乱,并要求我们实现其他类型,以便能够使用我们的协议。

因为我们所处理的协议只有一个要求,所以问题是-我们真的需要吗?毕竟,我们ModelProvider协议没有添加任何额外的分组或结构,因此让我们取消它的唯一要求,将其转化为闭包-然后可以直接注入,如下所示:

class DetailViewController<Model: ModelProtocol>: UIViewController {
private let modelProvider: () -> Model

init(modelProvider: @escaping () -> Model) {
self.modelProvider = modelProvider
super.init(nibName: nil, bundle: nil)
}

override func viewDidLoad() {
super.viewDidLoad()

let model = modelProvider()
...
}

...
}

通过直接注入我们需要的功能,而不是要求类型符合协议,我们还大大提高了代码的灵活性-因为我们现在可以自由地注入任何东西,从空闲函数到内联定义的闭包,再到实例方法。我们也不再需要执行任何类型删除,留给我们的代码要简单得多。

使用泛型类型

虽然闭包和函数是建模单个需求抽象的好方法,但是如果我们开始添加额外的需求,那么使用它们可能会变得有点混乱。例如,假设我们希望扩展上面的内容DetailViewController也支持书签和删除模型。如果我们坚持基于闭包的方法,我们最终会得到这样的结果:

class DetailViewController<Model: ModelProtocol>: UIViewController {
private let modelProvider: () -> Model
private let modelBookmarker: (Model) -> Void
private let modelDeleter: (Model) -> Void

init(modelProvider: @escaping () -> Model,
modelBookmarker: @escaping (Model) -> Void,
modelDeleter: @escaping (Model) -> Void) {
self.modelProvider = modelProvider
self.modelBookmarker = modelBookmarker
self.modelDeleter = modelDeleter

super.init(nibName: nil, bundle: nil)
}

...
}

上述设置不仅要求我们跟踪多个独立闭包,而且还会出现大量重复的闭包。“模型”前缀-(使用“三人规则”)告诉我们,我们这里有一些结构性问题。而我们能回到将上述所有闭包封装到一个协议中去,这再次要求我们执行类型擦除,并失去我们在开始使用闭包时获得的一些灵活性。

相反,让我们使用泛型类型将我们的需求组合在一起-这两种类型都允许我们保留使用闭包的灵活性,同时在代码中添加一些额外的结构:

struct ModelHandling<Model: ModelProtocol> {
var provide: () -> Model
var bookmark: (Model) -> Void
var delete: (Model) -> Void
}

因为上面是一个具体的类型,所以它不需要任何形式的类型擦除(实际上,它看起来非常类似于我们在使用带关联类型的协议时经常被迫编写的类型擦除包装)。因此,就像闭包一样,它可以直接使用和存储-如下所示:

class DetailViewController<Model: ModelProtocol>: UIViewController {
private let modelHandler: ModelHandling<Model>
private lazy var model = modelHandler.provide()

init(modelHandler: ModelHandling<Model>) {
self.modelHandler = modelHandler
super.init(nibName: nil, bundle: nil)
}

@objc private func bookmarkButtonTapped() {
modelHandler.bookmark(model)
}

@objc private func deleteButtonTapped() {
modelHandler.delete(model)
dismiss(animated: true)
}

...
}

而具有关联类型的协议在定义更高级别的需求时非常有用(就像标准库的Equatable和Collection),当这样的协议需要直接使用时,使用独立闭包或泛型类型通常可以给我们相同的封装级别,但通过一个简单得多的抽象。

使用枚举分离要求

在设计任何类型的抽象时,一个常见的挑战是不要。“过于抽象”通过添加太多的需求。例如,现在假设我们正在开发一个应用程序,它允许用户使用多种媒体-比如文章、播客、视频等等-我们希望为所有这些不同的格式创建一个共享的抽象。如果我们再次从面向协议的方法开始,我们可能会得到这样的结果:

protocol Media {
var id: UUID { get }
var title: String { get }
var description: String { get }
var text: String? { get }
var url: URL? { get }
var duration: TimeInterval? { get }
var resolution: Resolution? { get }
}

由于上面的协议需要与所有不同类型的媒体一起工作,我们最终得到了多个仅与某些格式相关的属性。例如,Article类型没有任何概念持续时间或分辨力-留给我们一些我们必须实现的属性,因为我们的协议要求我们:

struct Article: Media {
let id: UUID
var title: String
var description: String
var text: String?
var url: URL? { return nil }
var duration: TimeInterval? { return nil }
var resolution: Resolution? { return nil }
}

上面的设置不仅要求我们在符合标准的类型中添加不必要的样板,还可能是歧义的来源-因为我们无法强制规定一篇文章实际上包含文本,或者应该支持URL、持续时间或解析的类型实际上携带了该数据-因为所有这些属性都是选项。

我们可以通过多种方法解决上述问题,从将协议拆分为多个协议开始,每个方法都具有提高专业化程度-像这样:

protocol Media {
var id: UUID { get }
var title: String { get }
var description: String { get }
}

protocol ReadableMedia: Media {
var text: String { get }
}

protocol PlayableMedia: Media {
var url: URL { get }
var duration: TimeInterval { get }
var resolution: Resolution? { get }
}

以上所述无疑是一种改进,因为它将使我们能够拥有以下类型Article符合ReadableMedia,和可玩类型(如Audio和Video)符合PlayableMedia-减少歧义和样板,因为每种类型都可以选择哪一种专门版本的Media它想要遵守的。

但是,由于上述协议都是关于数据的,因此使用实际数据类型相反,这既可以减少重复实现的需要,也可以让我们通过单一的具体类型来处理任何媒体格式:

struct Media {
let id: UUID
var title: String
var description: String
var content: Content
}

上面的结构现在只包含我们所有媒体格式之间共享的数据,除了content属性-这就是我们将用于专门化的内容。但这一次,而不是Content一个协议,让我们使用枚举-它将使我们能够通过关联的值为每种格式定义一组量身定做的属性:

extension Media {
enum Content {
case article(text: String)
case audio(Playable)
case video(Playable, resolution: Resolution)
}

struct Playable {
var url: URL
var duration: TimeInterval
}
}

选项已经消失,我们现在已经在共享抽象和启用特定于格式的专门化之间取得了很好的平衡。枚举的美妙之处还在于,它使我们能够表达数据变化,而不必使用泛型或协议-只要我们预先知道变体的数量,一切都可以封装在相同的具体类型中。

类和继承

另一种方法在SWIFT中可能不像在其他语言中那么流行,但仍然值得考虑,那就是使用通过继承专门化的类来创建抽象。例如,而不是使用Content为了实现上述媒体格式,我们可以使用Media基类,然后将其子类化,以添加特定于格式的属性,如下所示:

class Media {
let id: UUID
var title: String
var description: String

init(id: UUID, title: String, description: String) {
self.id = id
self.title = title
self.description = description
}
}

class PlayableMedia: Media {
var url: URL
var duration: TimeInterval

init(id: UUID,
title: String,
description: String,
url: URL,
duration: TimeInterval) {
self.url = url
self.duration = duration
super.init(id: id, title: title, description: description)
}
}

然而,尽管从结构的角度来看,上述方法是完全有意义的-但它也有一些不利之处。首先,由于类还不支持按成员划分的初始化器,所以我们必须自己定义所有初始化器-我们还必须通过调用super.init..但也许更重要的是,课程是参考类型,这意味着在共享时,我们必须小心避免执行任何意外的突变。Media跨代码库的实例。

但这并不意味着SWIFT中没有有效的继承用例。例如,在“在未来的引擎盖下&斯威夫特的承诺”,继承提供了一种公开只读的好方法。Future类型到api用户-同时仍然允许通过Promise子类:

class Future<Value> {
fileprivate var result: Result<Value, Error>? {
didSet { result.map(report) }
}

...
}

class Promise<Value>: Future<Value> {
func resolve(with value: Value) {
result = .success(value)
}

func reject(with error: Error) {
result = .failure(error)
}
}

func loadCachedData() -> Future<Data> {
let promise = Promise<Data>()
cache.load { promise.resolve(with: $0) }
return promise
}

使用上面的设置,我们可以让同一个实例在不同的上下文中公开不同的API集,当我们只允许其中一个上下文对给定的对象进行变异时,这是非常有用的。在使用泛型代码时尤其如此,因为如果我们尝试使用一个协议来实现相同的目标,我们将再次遇到关联类型问题。

结语

在可预见的将来,协议是很棒的,并且很可能仍然是在SWIFT中定义抽象的最常用的方式。然而,这并不意味着使用协议永远是最好的解决方案-有时会超越流行的范围“面向协议的编程”MARRA可以产生更简单、更健壮的代码-特别是当我们想要定义的协议要求我们使用关联类型的时候。

链接:https://www.jianshu.com/p/74d511140089

0 个评论

要回复文章请先登录注册