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

毫无疑问,协议是SWIFT整体设计的主要部分-而且能够提供一种很好的方法来建立抽象、分离关注点和提升系统或功能的总体灵活性。经过不强烈地将类型绑定在一块儿,而是经过更抽象的接口链接代码库的各个部分,咱们一般会获得一个更加解耦的体系结构,它容许咱们孤立地迭代每一个单独的特性。编程

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

使用闭包的单个需求

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

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

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直接。相反,咱们必须首先执行类型擦除将咱们的协议引用转换成一个具体的类型,这两种类型都会使咱们的代码混乱,并要求咱们实现其余类型,以便可以使用咱们的协议。bash

由于咱们所处理的协议只有一个要求,因此问题是-咱们真的须要吗?毕竟,咱们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()
        ...
    }
    
    ...
}
复制代码

经过直接注入咱们须要的功能,而不是要求类型符合协议,咱们还大大提升了代码的灵活性-由于咱们如今能够自由地注入任何东西,从空闲函数到内联定义的闭包,再到实例方法。咱们也再也不须要执行任何类型删除,留给咱们的代码要简单得多。app

使用泛型类型

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

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能够产生更简单、更健壮的代码-特别是当咱们想要定义的协议要求咱们使用关联类型的时候。

你认为如何?除了协议,您最喜欢的在SWIFT中建立抽象的方法是什么?请经过加咱们的交流群 点击此处进交流群 ,来一块儿交流或者发布您的问题,意见或反馈。

原文地址 :www.swiftbysundell.com/articles/al…

相关文章
相关标签/搜索