簡體   English   中英

具有關聯類型的兩個協議:如何以類型安全的方式鏈接它們的實現?

[英]Two protocols with associated types: How to link their implementations in a type-safe way?

目標

在為我的應用程序創建網絡層時,我通常使用通用方法從服務器獲取數據,例如

func fetch<R: Resource>(_ resource: R, completion: ((Result<R.Model, Error>) -> Void))

我發現這非常優雅,因為我只需要實現一次我的 fetch 方法,但仍然使用此協議獲得類型安全:

protocol Resource {
    associatedType Model
    var endPointPath: String { get }
}

這樣,我可以輕松地在任何地方添加新資源(端點),而無需修改現有代碼,並且包含 API 端點的資源始終通過關聯類型直接與結果的Model類型掛鈎——我總是知道在獲取給定Resource

例子

我可以簡單地獲取這個資源:

struct ArticlesResource: Resource {
    typealias Model = [Article]
    let endPointPath = "/articles" 
}

通過此調用並返回類型安全的 model :

myAPI.fetch(ArticleResource()) { result in
    switch result {
        case let .success(model):
            // model is of type [Article]
        case .failure:
            // Request failed...
    }
}

這種方法一直很適合我——直到現在。

問題

現在我正在處理一個給定的網絡框架,它使用與我相同的方法(驚訝:)。 它還使用協議來定義請求,並且該協議還具有網絡調用結果的關聯類型

我需要包裝這個具體的網絡框架,這樣它就不會暴露在我自己的網絡層之外(因為它是我的網絡層的使用者不應該關心的實現細節)。

所以讓我們假設,這個第 3 方框架具有與我自己的網絡層相同的 API(只是具有不同的 model 和資源類型):兩者都有一個在與各自關聯的(特定於域的)資源類型中通用的fetch方法model 類型(通過關聯類型,見圖)。

我現在如何實現我的 fetch 方法?

struct MyAPI {

    private let networkingClient: ThirdPartyNetworkingClient

    func fetch<R: Resource>(_ resource: R, completion: ((Result<R.Model, Error>) -> Void)) {
        // 1. Map my own Resource to ThirdPartyResource
        let networkResource = ? 💥
        // 2. Fetch ThirdPartyResource
        networkingClient.fetch(networkResource) { result in
            // 3. Map ThirdPartyModel to my own Model
            let model = R.Model(...) 💥
            // 4. Return my own Model to the app via completion handler
            completion(.success(model))
        }
    }

}

(在上面的示例中,為了簡潔起見,我假設了愉快的路徑並忽略了任何錯誤。)

在第 1 步中,我遇到了一個問題:我在一個通用的 function ThirdPartyResourceType中,所以我需要一個方法來將我自己的資源類型R以通用方式 ResourceType 映射到第三部分。

func mapToThirdPartyResource<R: Resource>(_ myResource: R) -> ThirdPartyResource {
    // ... map ...
}

但是, ThirdPartyResourceType具有關聯的類型要求,因此任何具有此類簽名的 function 都不會編譯。 你們都知道這個臭名昭著的 Swift 編譯器錯誤:

 Protocol 'ThirdPartyResourceType' can only be used as a generic constraint because it has Self or associated type requirements

如果不指定協議的關聯類型,則類型信息不完整,因此編譯器無法使用它(僅作為通用約束)。 因此,不可能有通用的 function 將任何給定的 Resource 映射到其各自的ThirdPartyResourceType

同樣,在第 3 步中,出於同樣的原因,我無法將 map 從第 3 方庫的 model 類型返回到我自己的 Model 類型。

問題

雖然在我看來這是 Swift 的語言限制,它只是阻止我這樣做,但我並不完全確定我是否足夠清楚地看到所有替代方案。 有沒有辦法在不失去類型安全的情況下實現所描述的網絡層抽象?

從邏輯上講,整個鏈條是緊密相連的:

Resource → ThirdPartyResource
                  ↓
Model    ← ThirdPartyModel

我正在尋找一種在ThirdPartyResorce中完全像這樣連接它的方法,以通用方式連接所有這些類型 - 而不會將第三方資源和-Model暴露給外界(消費者應用程序)。 那么有沒有辦法繞過這個限制呢?

建築素描


[編輯] 協議聲明和映射示例

為了讓事情更具體一點,這里有兩個示例協議:

由我自己的網絡層使用(暴露給客戶端應用程序)

protocol Resource {
    associatedtype Model
}

由我的第 3 方庫使用(對客戶端應用程序隱藏)

protocol ThirdPartyResource {
    associatedtype Model
    var query: String { get }
}

獲取方法

第 3 方網絡庫可能有一個網絡客戶端,它公開以下通用fetch function:

struct ThirdPartyNetworkingClient {
    func fetch<R: ThirdPartyResource>(_ resouce: R, completion: (Result<R.Model, Error>) -> ()) {
        // ...
    }
}

function 是一個實現細節,不應向客戶端應用程序公開。 相反,我的網絡層公開了它自己的通用fetch function,它在我的Resource協議而不是ThirdPartyResource上運行:

func fetch<R: Resource>(_ resource: R, completion: (Result<R.Model, Error>) -> ()) {
    let thirdPartyResource = thirdPartyResource(for: resource) // 1️⃣ Mapping
    thridPartyNetworkingClient.fetch(thirdPartyResource) { result in
        switch result {
        case .success(let thirdPartyModel):
            let model = // 2️⃣ map ThirdPartyResource.Model to our Resource.Model and return to client
            completion(.success(model))
            break
        case .failure:
            // handle error
            break
        }
    }
}

我正在尋找的通用映射函數(這個問題的全部內容)分別用 1️⃣ 和 2️⃣ 標記。 I need a point to convert a Resource to the respective ThirdPartyResource in order to perform the request with the 3rd party networking client, and when it returns a result, I need a way to map the ThirdPartyResource.Model back to my own layer's model ( Resource.Model )。

希望這能讓事情更清楚。

我不確定我是否遺漏了什么,但我認為步驟 1 中的問題可能可以這樣解決:

struct AnyThirdPartyResource<M>: ThirdPartyResource {
  typealias Model = M
  var query: String
  
  init(_ query: String) {
    self.query = query
  }
}

func transform<R: Resource>(_ resource: R) -> AnyThirdPartyResource<R.Model> {
  return AnyThirdPartyResource<R.Model>(resource.query)
}

AnyThirdPartyResource類型不必暴露給客戶端,僅用於獲取 function。

In regards to problem 2, I think if the two Model types are not the same, you'd have to specify a transform function from ThirdPartyResource.Model -> Resource.Model. 也許這可能是您框架的相應資源上的內部 function ?

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM