实施存储库模式时避免类型擦除

时间:2019-04-06 13:02:17

标签: swift generics type-erasure

我正在尝试以通用方式在Swift中实现存储库模式。我当前面临的问题是,好像我必须为所有存储库编写类型擦除包装器。我在这里想念什么吗?此时是否有更好的方法或使编译器满意?

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

2 个答案:

答案 0 :(得分:0)

您不需要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
   }
}

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

答案 1 :(得分:0)

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

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

因此,我们将从存储的物品开始。它们可能需要具有某种标识符,并且可以对许多常见后端进行哈希处理(但也许您不需要哈希;如果不需要,则将其删除)。

protocol Identified {
    associatedtype ID
    var id: ID { get }
}

typealias Storable = Identified & Hashable

然后有些东西可以充当存储。没有“ RepositoryStorage”之类的东西。这只是说“如果您遵守这些要求,那么存储库就可以使用您。”

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。您可能需要查询和联接,但这还不够。 (在一个方向上做一些灵活的事情通常会使您在另一个方向上受挫。没有通用的“泛型”。)

那么当它实际上只有几个项目时,我们可以简化它吗?这是我经常使用的方法:

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 }
}

这对于许多项目而言非常快速且高效。我可能会重新考虑是否有一百多个项目,而对于数百个或更多项目则不合适。但是对于小型设备,这非常非常快。

一个非常常见的数据存储是UserDefaults:

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

关键的教训是,通过创建较低层专用的通用货币(数据),这消除了所有类型擦除循环。只要您能够做到,只要将上层通用接口与下层非通用接口分开,就可以节省很多麻烦。

这可能会或可能不会适合您的情况。它是针对键/值存储而非数据库而设计的,其读取频率要比书面读取频率高得多。但是对于这种用法,它比存储库模式简单得多,并且通常要快得多。当我说知道您的用例很重要时,这就是我要进行的权衡。