【问题标题】:How to use generics in an arbitrary abstraction layer?如何在任意抽象层中使用泛型?
【发布时间】:2016-01-13 21:43:59
【问题描述】:

Sample Project Here

摘要

我正在尝试了解如何重构代码和使用 Swift 泛型。我能够跨几个概念上相似的类型确定一个通用实现,但我遇到了困难,因为代码构建的层不是通用的。

背景

这是我对一些我想利用的概念的理解。

协议

对于声明一个接口并拥有该接口的许多“具体”实现很有用。这允许您在运行时换出实现。

泛型

当实现相同或相似但类型不同时很有用。似乎典型的例子是交换两个值或map

问题

我有一个负责创建 URL 请求的客户端类 (APIClient)。客户端只知道主要数据类型(整数、字符串、数组、JSON 等)。在客户端类之上有几个抽象领域对象的类(FruitDownloaderTrafficDownloaderWildlifeDownloader)。此类知道如何从域类型中解压缩主要数据类型并调用正确的客户端方法。该类使用映射器类(FruitMapperTrafficMapperWildlifeMapper)从 JSON 构造域对象。

...Dowloader 类的实现非常相似:

class FruitDownloader {

    init(client: APIClient, mapper: FruitMapper)
    {
        self.client = client
        self.mapper = mapper
    }

    func downloadFruit(location: Location, season: Season, successHandler: (([ Fruit ]) -> Void)?, failureHandler: ((NSError) -> Void)?)
    {
        client.getFruitsForRegionIdentifier(location.identifier,
            seasonIdentifier: season.identifier,
            successHandler: { objectNotation in
                if let instances = objectNotation["fruit_data"] as? [ ObjectNotation ] {
                    do {
                        let fruits = try instances.map(self.mapper.fruit)
                        successHandler?(fruits)
                    }
                    catch let error as NSError {
                        failureHandler?(error)
                    }
                }
                else {
                    failureHandler?(NSError(domain: "com.fruit.downloader", code: 1000, userInfo: nil))
                }
            },
            failureHandler: failureHandler)
    }

    private let client: APIClient
    private let mapper: FruitMapper
}

class TrafficDownloader {

    init(client: APIClient, mapper: TrafficMapper)
    {
        self.client = client
        self.mapper = mapper
    }

    func downloadTraffic(location: Location, season: Season, successHandler: (([ Traffic ]) -> Void)?, failureHandler: ((NSError) -> Void)?)
    {
        client.getHistoricalTrafficReportForRegionIdentifier(location.identifier,
            seasonIdentifier: season.identifier,
            successHandler: { objectNotation in
                if let instances = objectNotation["data"] as? [ ObjectNotation ] {
                    do {
                        let trafficReport = try instances.map(self.mapper.traffic)
                        successHandler?(trafficReport)
                    }
                    catch let error as NSError {
                        failureHandler?(error)
                    }
                }
                else {
                    failureHandler?(NSError(domain: "com.traffic.downloader", code: 1000, userInfo: nil))
                }
            },
            failureHandler: failureHandler)
    }

    private let client: APIClient
    private let mapper: TrafficMapper
}

class WildlifeDownloader {

    init(client: APIClient, mapper: WildlifeMapper)
    {
        self.client = client
        self.mapper = mapper
    }

    func downloadWildlife(location: Location, successHandler: (([ Wildlife ]) -> Void)?, failureHandler: ((NSError) -> Void)?)
    {
        client.getWildlifeForRegionIdentifier(location.identifier,
            successHandler: { objectNotation in
                if let instances = objectNotation["content"] as? [ ObjectNotation ] {
                    do {
                        let wildlife = try instances.map(self.mapper.wildlife)
                        successHandler?(wildlife)
                    }
                    catch let error as NSError {
                        failureHandler?(error)
                    }
                }
                else {
                    failureHandler?(NSError(domain: "com.wildlife.downloader", code: 1000, userInfo: nil))
                }
            },
            failureHandler: failureHandler)
    }

    private let client: APIClient
    private let mapper: WildlifeMapper
}

好的,这对于泛型于TDownloader 类来说已经成熟了。好的,现在有一些紧迫的问题:

  1. 每个下载器类都使用一个特定于域的映射器。
  2. 每个下载者都必须通过ObjectNotation 摸索一个特殊的密钥。
  3. 客户端方法的参数略有不同。
  4. 每个下载器调用一个特定的客户端方法。

让我们依次处理:

1:具有关联类型的协议

让我们让它使用Mappable 类型的东西,而不是使用特定映射器的下载器。可映射协议需要对许多不同类型是通用的,因此我们可以通过使用关联类型来实现这一点。

protocol MapperType {
    typealias ObjectType
    func object(objectNotation: ObjectNotation) throws -> ObjectType
}

2:对象符号操作

嗯,我不太确定有什么好的方法来处理这个问题。但是,我可以声明另一个协议 Mappable 并让我的模型对象符合。

protocol Mappable {
    static func objectNotationRoot() -> String
}

3:下载器参数

一些下载者使用LocationSeason,另一些下载者只使用Location。通用下载器可以同时获取并在不需要 Season 实例时丢弃它,但这感觉就像是泄漏抽象。

4:根据类型调用不同的方法

好的,现在我真的被困住了。我如何调用客户端?检查T 的类型并以此为基础做出决定感觉真的很恶心。类型信息在下载器中,客户端不获取或使用此信息。我是否应该认为我需要在这些层之间插入一些东西?

这是我半生不熟的通用下载器的样子:

class Downloader<T: Mappable, U: MapperType where U.ObjectType == T> {

    init(client: APIClient, mapper: U)
    {
        self.client = client
        self.mapper = mapper
    }

    func download(location: Location, season: Season, successHandler: (([ T ]) -> Void)?, failureHandler: ((NSError) -> Void)?)
    {
        client._________(location.identifier,
            seasonIdentifier: season.identifier,
            successHandler: { objectNotation in
                if let instances = objectNotation[T.objectNotationRoot()] as? [ ObjectNotation ] {
                    do {
                        let objects = try instances.map(self.mapper.object)
                        successHandler?(objects)
                    }
                    catch let error as NSError {
                        failureHandler?(error)
                    }
                }
                else {
                    failureHandler?(NSError(domain: "com.fruit.downloader", code: 1000, userInfo: nil))
                }
            },
            failureHandler: failureHandler)
    }

    private let client: APIClient
    private let mapper: U
}

【问题讨论】:

    标签: swift generics protocols


    【解决方案1】:

    你可以这样做,但你真的不需要所有的协议。一个结构体就足够了,还有一些函数和“一个奇怪的技巧”。

    “一个奇怪的技巧”是方法是真正的柯里化函数,它们将对象作为第一个参数。因此,鉴于此:

    struct Foo {
        func bar() {}
    }
    

    有一个函数Foo.bar(self: Foo),它返回一个函数() -&gt; Void。这可能还不完全有意义,但我们稍后会使用它。

    首先,我们的Downloader(按照目前的设计)需要四样东西:客户端、获取内容的方法、查找树顶部的方法以及将找到的内容映射到对象的方法。所以我们做到了:

    struct Downloader<ObjectType> {
        let client: APIClient
        let fetcher: Fetcher
        let rootKey: String
        let mapper: (ObjectNotation) throws -> ObjectType
    }
    

    为方便起见,我创建了以下类型别名,因为它非常庞大:

    typealias Fetcher = (APIClient) -> (locationIdentifier: String, seasonIdentifier: Int, successHandler: ((ObjectNotation) -> Void)?, failureHandler: ((NSError) -> Void)?) -> NSURL
    

    这种类型的函数是什么样的?好吧,APIClient.getFruitsForRegionIdentifier 有这种类型(我就是从那里复制它的)。所以我们可以将它作为 fetcher 传递。

    有了这些download 就很简单了:

    func download(location: Location, season: Season, successHandler: (([ ObjectType ]) -> Void)?, failureHandler: ((NSError) -> Void)?) {
        fetcher(client)(
            locationIdentifier: location.identifier,
            seasonIdentifier: season.identifier,
            successHandler: self.successWrapper(successHandler: successHandler, failureHandler: failureHandler),
            failureHandler: failureHandler)
    }
    

    为了让它更容易阅读,我把成功的那块拖了出来。它接受一个 successHandler 和一个 failureHandler 并返回一个新的成功处理程序。

    func successWrapper(successHandler successHandler: (([ ObjectType ]) -> Void)?, failureHandler: ((NSError) -> Void)?)
        -> ((objectNotation: ObjectNotation) -> Void) {
            return { objectNotation in
                if let instances = objectNotation[self.rootKey] as? [ ObjectNotation ] {
                    do {
                        let objects = try instances.map(self.mapper)
                        successHandler?(objects)
                    }
                    catch let error as NSError {
                        failureHandler?(error)
                    }
                }
                else {
                    failureHandler?(NSError(domain: "com.fruit.downloader", code: 1000, userInfo: nil))
                }
            }
    }
    

    好的,那里有很多东西。我们怎么用它?好吧,我们可以像这样创建一个 Fruit 下载器:

    let fruitDownloader = Downloader<Fruit>(
        client: APIClient(baseURL: NSURL()),
        fetcher: APIClient.getFruitsForRegionIdentifier, // <- That curried function we talked about
        rootKey: "fruit_data",
        mapper: simpleMapper("fruit")
    )
    

    simppleMapper 只是一个助手:

    extension String: ErrorType {} // For sloppy errors
    
    func simpleMapper<T>(key: String) -> (ObjectNotation) throws -> T {
        return { objectNotation in
            guard let value = objectNotation[key] as? T else {
                throw "Could not!" // FIXME
            }
            return value
        }
    }
    

    我们已经完成了。这里的关键,到处都是高阶函数。接受函数并返回新函数的函数。

    使用高阶函数,您可能可以简化更多。查看APIClient,不清楚为什么我们真的需要所有这些不同的方法。看起来唯一不同的是 URL 模板。也许我们只需要一个函数并将其附加到下载器。

    您可能会注意到我们甚至不需要Downloader 结构。它只是包装了一个方法,甚至没有任何状态(这就是为什么它当然不应该是一个类)。我们可以将它构建为一个返回下载函数的函数(就像simpleMapper 所做的那样)。结构有一些好处(例如,您可以在调试器中打印出它们的属性),所以我并不是说您应该摆脱所有这些,但最好记住您有选择。

    (我注意到,Wildlife 有一个不同的函数签名,缺少 Season。目前尚不清楚这是否是故意的。如果是,那么您可能需要创建另一层函数来丢弃已经过去的季节。但它不应该很复杂。想想返回函数的函数。)

    All the code together as a gist.

    【讨论】:

      猜你喜欢
      • 2011-01-22
      • 1970-01-01
      • 2019-05-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-05-14
      • 2012-12-10
      相关资源
      最近更新 更多