【问题标题】:Avoiding Type Erasure when implementing the Repository Pattern实现存储库模式时避免类型擦除
【发布时间】:2019-04-06 23:13:31
【问题描述】:

我正在尝试以通用方式在 Swift 中实现存储库模式。我目前面临的问题是,我似乎必须为我的所有存储库编写类型擦除包装器。我在这里错过了什么吗?有没有更好的方法来做到这一点或让编译器在这一点上满意?

// 1
class Item {}

// 2
protocol Repository {
    associatedtype T
}

// 3
protocol AnyItemRepository: Repository where T == Item {}

// 4
class ItemRepository: AnyItemRepository {
    static let shared = ItemRepository()

    private init() {}
}

// 5
class ViewController {

    // 6
    var itemRepository: AnyItemRepository? = ItemRepository.shared

}
  1. 众多实体之一
  2. 必要时可扩展或直接实现的基础存储库接口
  3. 一个特殊的项目存储库接口,可确保基础存储库之上的附加功能
  4. 特定实体类型的具体存储库实现
  5. 一些需要访问数据的类
  6. 对任何项目存储库的依赖性。这一行的编译器错误:Protocol 'AnyItemRepository' can only be used as a generic constraint because it has Self or associated type requirements

【问题讨论】:

    标签: swift generics type-erasure


    【解决方案1】:

    您不需要AnyItemRepository 类型。只需在Repository 上编写扩展方法,如下所示:

    public extension Repository where T == Item {
       func doSomethingSpecial(with items: [Item]) {
          // blah blah
       }
    }
    

    在您的视图控制器中,您不能以这种方式使用RepositoryAnyItemRepository,因为它们是泛型类型约束。您必须使用具体类型或泛型参数化 ViewController

    class RepositoryViewController<R>: UIViewController where R: Repository, R.T == Item {
        var itemRepository: R { get }
    }
    
    class ViewController: RepositoryViewController<ItemRepository> {
       override var itemRepository: ItemRepository {
          return ItemRepository.shared
       }
    }
    

    (以上是未经测试的伪代码,旨在为您提供要点。它从未被任何人运行过,甚至可能无法编译。)

    【讨论】:

    • 感谢您的回答格雷戈里。我已经尝试过一个非常相似的解决方案,就像你建议的那样,我非常喜欢这个想法。然而事实证明,故事板不支持一般参数化的类。
    【解决方案2】:

    尚不清楚您打算将此存储库用于什么目的。 Repository 专为具有某些特征(例如与行式数据库的强链接)的环境而设计。我将讨论在常见 iOS 应用(甚至更小的 Mac 应用)中通常更有效的其他模式。

    我通常非常不喜欢类型擦除,这通常表明存在设计问题。但在这种情况下,我认为类型擦除可能是一个合理的答案。

    因此,我们将从可以存储的物品种类开始。它们可能需要某种标识符,并且对于许多常见的后端都是可散列的(但也许您不需要散列;如果不需要,请将其取出)。

    protocol Identified {
        associatedtype ID
        var id: ID { get }
    }
    
    typealias Storable = Identified & Hashable
    

    还有一些可以作为存储的东西。没有“RepositoryStorage”这样的东西。这只是说“如果您遵守这些要求,那么 Repository 可以使用您。”

    protocol RepositoryStorage {
        associatedtype Item: Storable
    
        func get(identifier: Item.ID) -> Item?
        func add(item: Item)
        func delete(identifier: Item.ID)
        func allItems() -> [Item]
    }
    

    然后是标准的、有点乏味的类型橡皮擦模式(stdlib 使用的另一种模式更乏味,但这个模式在大多数情况下已经足够好了)。

    // I'm making it a class because I assume it'll have reference semantics.
    final class Respository<Item: Storable>: RepositoryStorage {
    
        init<Storage: RepositoryStorage>(storage: Storage) where Storage.Item == Item {
            self._get = storage.get
            self._add = storage.add
            self._delete = storage.delete
            self._allItems = storage.allItems
        }
    
        let _get: (Item.ID) -> Item?
        func get(identifier: Item.ID) -> Item? { return _get(identifier) }
    
        let _add: (Item) -> Void
        func add(item: Item) { _add(item) }
    
        let _delete: (Item.ID) -> Void
        func delete(identifier: Item.ID) { _delete(identifier) }
    
        let _allItems: () -> [Item]
        func allItems() -> [Item] { return _allItems() }
    }
    

    没关系,它是一个通用存储库。如果您正在处理可能将存储在 SQLite 数据库中的大量项目,这是合理的。但根据我的经验,它往往既太多又太少。如果它只是几个项目,那就太多了,如果你有很多项目,那就太少了,所以可能需要做的不仅仅是 CRUD。您可能需要 Query 和 Join,然后这还不够。 (使某些东西在一个方向上变得灵活通常会在其他方向上使您不适应。没有通用的“通用”。)

    那么,当它实际上只有几件物品时,我们是否可以让它变得更简单?这是我经常使用的一种方法:

    class DataStore<Key: Hashable & Codable, Value: Codable> {
        let identifier: String
        private(set) var storage: DataStorage
    
        var dictionary: [Key: Value] {
            didSet {
                storage[identifier] = try? PropertyListEncoder().encode(dictionary)
            }
        }
    
        init(identifier: String, storage: DataStorage = UserDefaults.standard) {
            self.identifier = identifier
            self.storage = storage
    
            let data = storage[identifier] ?? Data()
            self.dictionary = (try? PropertyListDecoder().decode([Key: Value].self,
                                                                 from: data)) ?? [:]
        }
    
        subscript(key: Key) -> Value? {
            get { return dictionary[key] }
            set { dictionary[key] = newValue }
        }
    }
    

    DataStore 充当字典,您可以在其中存储键/值对:

    let ds = DataStore<String: Item>(identifier: "Item")
    ds["first"] = item
    

    它可以存储任何可编码的东西。稍加修改,您就可以将其从类字典接口切换为类数组或类集接口;我通常只是想要一本字典。

    当它被更新时,它会将整个数据存储编码到它的存储中作为数据:

    protocol DataStorage {
        subscript(identifier: String) -> Data? { get set }
    }
    

    这对于几十个项目来说非常快速和高效。我可能会重新考虑如果有超过一百个项目,那么对于数百个或更多项目是不合适的。但对于小型集,这非常非常快。

    一个很常见的DataStorage是UserDefaults:

    extension UserDefaults: DataStorage {
        subscript(identifier: String) -> Data? {
            get { return data(forKey: identifier) }
            set { set(newValue, forKey: identifier) }
        }
    }
    

    关键的教训是,通过创建低层专用的通用货币(数据),这消除了所有类型擦除跳跃。只要你能做到这一点,将上层的泛型接口与下层的非泛型接口分开,你就会省去很多乏味的事情。

    这可能适用于您的情况,也可能不适用。它是为键/值存储而不是数据库量身定制的,而且它的设计目的是阅读频率高于写入频率。但是对于这种用法,它比存储库模式更简单且通常更快。当我说了解你的用例很重要时,我的意思就是这种权衡。

    【讨论】:

    • 感谢您的回答 Rob。我非常感谢您为此付出的努力。对于我的用例来说,这种模式可能完全是多余的,但我想花尽可能多的时间来真正理解它。我的代码库看起来已经非常像您最初建议的那样。我也觉得为通用存储库编写类型擦除包装器很好。但是,必须为每个专门的存储库编写类型擦除包装器似乎我做错了什么。你会说无论如何我都必须这样做,这只是我不得不接受的不便吗?
    • 我会说你应该从你的专业存储库的实际情况开始,然后从那里开始构建。如果您正在构建大量的类型橡皮擦,那么您几乎可以肯定做错了什么。正如您所说,“对于我的用例来说可能有点矫枉过正”。专注于您的用例。不要制造你没有的问题。对于无限问题,没有“通用”解决方案。当您将问题联系起来时,我们就可以制定解决方案。
    • 那么,你目前有多少 Repository 的实现,它们是什么? (如果答案是“一”,那就停下来。在这种情况下,使用泛型是非常有害的。它会让你陷入困境,以至于当你发现你需要灵活性的实际轴时,将来很难让系统变得灵活。 )
    • 感谢您的建议罗。我将为这个应用程序提供 6 个存储库。我构建这个应用程序的主要原因是提高我的 Swift 知识。这就是为什么我不太担心它是矫枉过正的。这基本上是一个学习项目。
    • 不过,矫枉过正并不等同于更深入的知识。如果你真的想扩展你对 Swift 的理解,构建一个真正需要灵活性的用例,然后探索不同的方法来解决它。我今天大部分时间都在从事这个项目,因为我认为它可以比类型橡皮擦更明智地切割。我的用例是一个可以支持 CloudKit 后端和 Dictionary 后端的引擎。这是我当前的完全类型擦除解决方案,我正在努力从中删除类型橡皮擦。 gist.github.com/rnapier/03627315078286415214615399552ed7
    猜你喜欢
    • 2012-01-17
    • 1970-01-01
    • 2018-06-18
    • 2017-08-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-11-07
    • 2016-04-20
    相关资源
    最近更新 更多