Last active
April 6, 2019 19:44
-
-
Save rnapier/03627315078286415214615399552ed7 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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