我正在尝试以通用方式在Swift中实现存储库模式。我当前面临的问题是,好像我必须为所有存储库编写类型擦除包装器。我在这里想念什么吗?此时是否有更好的方法或使编译器满意?
Protocol 'AnyItemRepository' can only be used as a generic constraint because it has Self or associated type requirements
答案 0 :(得分:0)
您不需要AnyItemRepository
类型。只需像这样在Repository
上编写扩展方法:
public extension Repository where T == Item {
func doSomethingSpecial(with items: [Item]) {
// blah blah
}
}
在视图控制器中,您不能以这种方式使用Repository
或AnyItemRepository
,因为它们是通用类型约束。您必须使用具体类型或一般性地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) }
}
}
关键的教训是,通过创建较低层专用的通用货币(数据),这消除了所有类型擦除循环。只要您能够做到,只要将上层通用接口与下层非通用接口分开,就可以节省很多麻烦。
这可能会或可能不会适合您的情况。它是针对键/值存储而非数据库而设计的,其读取频率要比书面读取频率高得多。但是对于这种用法,它比存储库模式简单得多,并且通常要快得多。当我说知道您的用例很重要时,这就是我要进行的权衡。