Skip to content

Instantly share code, notes, and snippets.

@rnapier
Last active April 6, 2019 19:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rnapier/03627315078286415214615399552ed7 to your computer and use it in GitHub Desktop.
Save rnapier/03627315078286415214615399552ed7 to your computer and use it in GitHub Desktop.
// Fully type-erased solution for
// https://stackoverflow.com/questions/55549318/avoiding-type-erasure-when-implementing-the-repository-pattern/55550454
// I don't like type-erasers like this; I believe it can be sliced a better way.
// Compare https://gist.github.com/rnapier/f7f0fa6202b0d6586af188635f54b28b, which I like, but relies on a common
// currency of serializing everything to Data and working with that instead of having storage that can deals with Element
// directly.
// FURTHER THOUGHTS:
//
// This example is interesting because, like so many of these toy projects, its design is completely broken and wouldn't
// actually be usable. CKRecord doesn't work this way (it's not like NSManagedObject, which is how I was treating it).
// CKRecords are more-or-less Dictionaries, except they have various restrictions in what they can hold (like a plist),
// but also certain special features like references to related records. In order to use those, we'd have to modify
// RepositoryStorage to cpature more features of CloudKit, or we'd have to dumb down CK to some least-common-denominator.
// But the one thing you *can't* do is just pretend that this is "general storage" without engaging with the specific
// backend implemenations and their limits/features.
//
// In short, given the brief "build a system that can serialize to CloudKit or Dictionary," this ain't the answer. So the
// fact that it creates a type mess doesn't actually tell us anything. And implementing to meet that brief would get rid
// of much of the confusion (because the common currency would become CKRecord, and a common currency fixes all of this).
//
// Going back to the "it's not like NSManagedObject," if the brief had been "work with Core Data and Dictionary," this
// fails even worse, and no little papering-over of protocols is going to fix it. You *have* to consider your actual use
// case. You can't escape that!
import CloudKit
// This isn't really important; it's just saying that items can have different ID types
protocol Identified {
associatedtype ID
var id: ID { get }
}
// And there's some kind of repository storage that can get and set "items". I feel this protocol is a mistake because
// it treats a PAT as a type, and that means there's a type eraser in your future.
protocol RepositoryStorage {
associatedtype Item: Identified
func get(identifier: Item.ID, completion: @escaping (Result<Item?, Error>) -> Void)
mutating func add(item: Item)
mutating func delete(identifier: Item.ID)
}
// OK, so I accept that I'm on the wrong road, but let's push on. One storage would be CloudKit.
// This is a sloppy implementation, but you could squint and imagine it's legitimate.
struct DatabaseTable<Item: Identified & CKRecord>: RepositoryStorage
where Item.ID == CKRecord.ID {
let database: CKDatabase
func get(identifier: Item.ID, completion: @escaping (Result<Item?, Error>) -> Void) {
database.fetch(withRecordID: identifier) { (record, error) in
if let error = error {
completion(.failure(error))
} else {
completion(.success(record as? Item))
}
}
}
mutating func add(item: Item) {
database.save(item, completionHandler: {(_,_) in })
}
mutating func delete(identifier: Item.ID) {
database.delete(withRecordID: identifier, completionHandler: {(_,_) in })
}
}
// And of course an in-memory Dictionary storage.
extension Dictionary: RepositoryStorage where Value: Identified, Key == Value.ID, Value.ID: Hashable {
func get(identifier: Key, completion: @escaping (Result<Value?, Error>) -> Void) {
completion(.success(self[identifier]))
}
mutating func add(item: Value) {
self[item.id] = item
}
mutating func delete(identifier: Key) {
self[identifier] = nil
}
}
// And then blam!, as predicted, we need a type eraser. I hate type erasers. This is the complicated version
// that stdlib uses, but allows both value and reference types.
struct Respository<Item: Identified>: RepositoryStorage {
private class AnyStorageBase<Item: Identified>: RepositoryStorage {
func get(identifier: Item.ID, completion: @escaping (Result<Item?, Error>) -> Void) { fatalError() }
func add(item: Item) { fatalError() }
func delete(identifier: Item.ID) { fatalError() }
}
private class AnyStorage<Concrete: RepositoryStorage>: AnyStorageBase<Concrete.Item> {
var concrete: Concrete
init(_ concrete: Concrete) { self.concrete = concrete }
override func get(identifier: Concrete.Item.ID, completion: @escaping (Result<Concrete.Item?, Error>) -> Void) {
concrete.get(identifier: identifier, completion: completion)
}
override func add(item: Concrete.Item) { concrete.add(item: item) }
override func delete(identifier: Concrete.Item.ID) { concrete.delete(identifier: identifier) }
}
init<Storage: RepositoryStorage>(storage: Storage) where Storage.Item == Item {
box = AnyStorage(storage)
}
func get(identifier: Item.ID, completion: @escaping (Result<Item?, Error>) -> Void) {
box.get(identifier: identifier, completion: completion)
}
mutating func add(item: Item) { box.add(item: item) }
mutating func delete(identifier: Item.ID) { box.delete(identifier: identifier) }
private let box: AnyStorageBase<Item>
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment