Reputation: 183
I have a Savable
protocol that has multiple versions of save()
method based on the conformance of C1
and C2
. And then there is another Downloadable
protocol that conforms to Savable
protocol that calls the Savable
's save()
method based on the conformance of C1
and C2
.
As can be seen from the sample code below, the download()
methods are all the same. Is there a way to simplify this code by removing the repetitive functions? Or should I use another totally different approach to get the same outcome?
PS: Both Savable
and Downloadable
need to be a protocol for some unmentioned technical reasons. Furthermore, C1
and C2
both have Self or associated type requirements, thus performing a type check is not possible.
// Constraint
protocol C1 { }
protocol C2 { }
// Savable
protocol Savable { }
extension Savable {
func save() {
print("Perform basic save")
}
}
extension Savable where Self: C1 {
func save() {
print("Perform save C1")
}
}
extension Savable where Self: C1 & C2 {
func save() {
print("Perform save C1 & C2")
}
}
// Downloadable
protocol Downloadable: Savable { }
extension Downloadable {
func download() {
print("Perform download")
save()
}
}
extension Downloadable where Self: C1 {
func download() {
print("Perform download")
save()
}
}
extension Downloadable where Self: C1 & C2 {
func download() {
print("Perform download")
save()
}
}
// Usage
class MyClassC1: Downloadable, C1 { }
let obj1 = MyClassC1()
obj1.download()
// Output:
// Perform download
// Perform save C1
class MyClassC1C2: Downloadable, C1, C2 { }
let obj2 = MyClassC1C2()
obj2.download()
// Output:
// Perform download
// Perform save C1 & C2
Thanks in advance!
Upvotes: 4
Views: 135
Reputation: 299345
This approach is very fragile. You're relying on compile-time checks to perform overloads, and that only works if the type is known at compile-time. This kind of thing should only be done when there are no functional differences between the versions of save()
(for example, if the only difference is performance). Protocols are not subclasses.
As an example of the problem, consider:
func getit(what: Downloadable) {
what.download()
}
getit(what: obj2) // => "Perform basic save", not "perform save C1 & C2"
Even if you add generics, it'll "fail" exactly the same way:
func getit<D: Downloadable>(what: D) { ... }
This isn't really a failure. All the compiler at compile time knows is that this is of type Downloadable, so it'll call the Downloadable implementation.
If you want runtime dispatch, you need to do the dispatch at runtime with as?
. Get rid of all the where
extensions and replace save
with:
protocol Savable { }
extension Savable {
func save() {
if self is C1 & C2 {
print("Perform save C1 & C2")
} else if self is C1 {
print("Perform save C1")
} else {
print("Perform basic save")
}
}
}
Note the order is important (testing C1 & C2
before testing C1
). Deciding how this dispatches is up to you.
This unfortunately requires you to statically know all the types at compile-time and put them in one place. It is possible to fix this and register "savers" centrally, but it's a bit tricky and I don't particularly recommend this unless you really need it. Here's how you would do it, however.
// A Saver object that knows all the "rules". It maps conformances to functions
class Saver {
static let shared = Saver()
private var savers: [(predicate: (Any) -> Bool, save: (Any) -> ())] = []
func addSaver<T>(of: T.Type, save: @escaping (T) -> ()) {
savers.append((predicate: { $0 is T }, save: { save($0 as! T) }))
}
func save(value: Any) {
for (predicate, save) in savers {
if predicate(value) {
save(value)
return
}
}
fatalError("Couldn't save") // Or some default behavior or whatever
}
}
// Now you configure the Saver. Order is important!
let saver = Saver.shared
saver.addSaver(of: (C1 & C2).self, save: { _ in print("Perform save C1 & C2") })
saver.addSaver(of: C1.self, save: { _ in print("Perform save C1") })
saver.addSaver(of: Savable.self, save: { _ in print("Perform basic save") })
// Savable
protocol Savable { }
extension Savable {
func save() {
// And use it here
Saver.shared.save(value: self)
}
}
Using this, you may discover many little corner cases. Like what happens if someone wants to add another saver at some random point in the program (maybe in another module?) so you may need to reorder the tests, or what if two savers are equally "specific" so you don't know which one to prefer. Those are exactly the corner cases the compiler faces when dealing with something like this.
Upvotes: 1