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…

相關文章
相關標籤/搜索