1

As an overview, I'm trying to implement a data abstraction layer in Swift. I'm using two database SDKs but I'm trying to be able to isolate their specific APIs from the rest of the system.

I'm trying to implement a factory pattern that will return the correct object based on a protocol conformance of the supplied concrete type. But the compiler is giving me red flags that I can't wrap my head around.

The Thing class is a first class entity that moves freely through the logic and UI layers of the app, and certain objects persist on a Realm store (which shouldn't matter) and have a specific type that is a subclass of the Realm object. That type is returned by a static function that has to be there to conform with the protocol FromDataSourceA.

protocol FromDataSourceA {
    static func A_objectType () -> A_Object.Type
}

class MyObject {
    ...
}

class Thing: MyObject, FromDataSourceA {
    static func A_objectType () -> A_Thing.Type
    ...
}

// Example usage
let target = DataTarget<Thing>(url: "url")
let dataSource = target.dataSourceBuilder()

As you can see, the concrete type Thing conforms to FromDataSourceA and is passed into the DataTarget initializer. DataSourceBuilder needs to be able to check that Thing conforms to FromDataSourceA to be able to return the correct DataSource.

The DataSourceTypeA class looks like

class DataSourceTypeA<T: MyObject & FromDataSourceA>: DataSource<T> {
    let db: DatabaseTypeA // All db-specific APIs contained here

    override init (target: DataTarget<T>) {
        self.db = try! DatabaseTypeA()
        super.init(target: target)
    }

    override func create<T: MyObject & FromDataSourceA> (object: T) {
        db.beginWrite()
        db.create(T.A_objectType(), value: object.dict()) // <-- T.A_objectType() is what the conformance to FromDataSourceA is needed for
        try! db.commitWrite()
    }

    override func get<T: MyObject & FromDataSourceA>(id: Int) -> T {
        ...
    }
}

class DataSource<T>: AnyDataSource {
    let target: DataTarget<T>

    init (target: DataTarget<T>) {
        self.target = target
    }

    func create<T> (object: T) { }

    func get<T>(id: Int) -> T { fatalError("get(id:) has not been implemented") }
}

protocol AnyDataSource {
    func create<T> (object: T)
    func get<T> (id: Int) -> T
}

The problem I'm facing right now is when I check that the metatype conforms to FromDataSourceA, the compiler gives me this warning

class DataTarget<T: MyObject> {
    ...

    func dataSourceBuilder () -> DataSource<T> {
        if T.self is FromDataSourceA { // <-- Thing.self conforms to FromDataSourceA

            // The Problem:

            return DataSourceTypeA(target: self) // Generic class 'DataSourceTypeA' requires that 'MyObject' conform to 'FromDataSourceA'

        } else {
            ...
        }
    }
}

Why won't the compiler let me return the DataSourceTypeA instance with argument self if the generic T passes the conditional statement and is proven as being in conformance with FromDataSourceA?

elight
  • 562
  • 2
  • 17

1 Answers1

1

The problem is that the call

return DataSourceTypeA(target: self)

is resolved at compile time, therefore it does not help to check

if T.self is FromDataSourceA { }

at runtime. A possible solution is to make the conformance check a compile-time check by defining a restricted extension:

extension DataTarget where T: FromDataSourceA {
    func dataSourceBuilder() -> DataSource<T> {
        return DataSourceTypeA(target: self)
    }
}

If necessary, you can add more implementations for other restrictions:

extension DataTarget where T: FromDataSourceB {
    func dataSourceBuilder() -> DataSource<T> {
        // ...
    }
}

or add a default implementation:

extension DataTarget {
    func dataSourceBuilder() -> DataSource<T> {
        // ...
    }
}

For a call

let dataSource = target.dataSourceBuilder()

the compiler will pick the most specific implementation, depending on the static (compile-time) type information of target.

Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • 1
    I worry a little that the phrase "The compiler will pick the most specific implementation when resolving a call to that function" will give the impression that multiple extensions with where clauses will act as a kind of substitute for dynamic dispatch and/or function overloading. A lot of people _hope_ that that will happen and are then surprised at runtime when it doesn't (i.e. the "wrong" version of the function is called). – matt Feb 05 '20 at 19:08
  • Thanks for you answer @Martin. I added `MyObject` to the constraint on the restricted extension and it is working. @matt thank you for the comment - I'll be wary and try to spend some more time researching this.. – elight Feb 05 '20 at 19:11
  • @MartinR Probably. :) I may be worrying needlessly. After all, you did say "the compiler", which is the point. It's just that I see a lot of questions where people try to misuse a where clause, as here: https://stackoverflow.com/a/51881252/341994 and also my https://stackoverflow.com/a/57526133/341994 – matt Feb 05 '20 at 21:04