Protocol with associated type in Action with factory pattern

Hi All,

I should write a protocol with associated type with factory pattern.

I write this code but ...

struct DB_Dev_Type {...}
struct DB_Production_Type {...}

protocol Environment {
    associatedtype DBElement

    func attachDB(_ db: DBElement)
    
    var identifier: String { get }
}

class DevEnvironment: Environment {
    typealias DBElement = DB_Dev_Type

    var identifier: String { return "dev" }
    func attachDB(_ db: DBElement) {
        print("db is \(db)")
    }

}

class LiveEnvironment: Environment {
    typealias DBElement = DB_Production_Type

    var identifier: String { return "live" }
    func attachDB(_ db: DBElement) {
        print("db is \(db)")
    }
}

class EnvironmentFactory {

    enum EnvType {
        case dev
        case live
    }

    func create(_ type: EnvType) -> Environment {
        switch type {
        case .dev:
            return DevEnvironment()
        case .live:
            return LiveEnvironment()
        }
    }
}

let factory = EnvironmentFactory()
let dev = factory.create(.dev)
print(dev.identifier)

on this line

func create(_ type: EnvType) -> Environment {

I have this error

error: TestPlayground.playground:59:43: error: protocol 'Environment' can only be used as a generic constraint because it has Self or associated type requirements
    func create(_ type: EnvType) -> Environment {
                                      ^

In this method i should return the protocol type to have generic type not a specialised. Do you have idea?

Thanks

Gotta "erase" the associated type.

Will need to create an erased AnyEnvironment type and then you can conform Environment to it and return AnyEnvironment from func create.

protocol AnyEnvironment {
    var identifier: String { get }
}

protocol Environment: AnyEnvironment {
    associatedtype DBElement
    func attachDB(_ db: DBElement)
}

...

func create(_ type EnvType) -> AnyEnvironment {
    switch type {
        case .dev:
            return DevEnvironment()
        case .live:
            return LiveEnvironment()
    }
}

Alternatively, since it doesn't seem like the associated type does much right now, you could remove attach & the associated type from Environment and then return Environment directly from create.

Hi Josh,

thanks for your answer. I thought to split, but I interested to resolve all this var and func in this protocols

protocol Environment {
    associatedtype DBElement
    func attachDB(_ db: DBElement)
    var identifier: String { get }
}

Unfortunately I don't think it's possible without the split. Though like I mentioned maybe you could consider removing associatedtype DBElement entirely since it doesn't appear to be used.

You could create a AnyEnvironment class to erase the type:

struct DB_Dev_Type {}
struct DB_Production_Type {}

protocol Environment {
    associatedtype DBElement
    var identifier: String { get }
    func attachDB(_ db: DBElement)
}

class DevEnvironment: Environment {
    typealias DBElement = DB_Dev_Type

    var identifier: String { "dev" }

    func attachDB(_ db: DBElement) {
        print("db is \(db)")
    }
}

class LiveEnvironment: Environment {
    typealias DBElement = DB_Production_Type

    var identifier: String { "live" }

    func attachDB(_ db: DBElement) {
        print("db is \(db)")
    }
}

final class AnyEnvironment: Environment {
    typealias DBElement = Any

    func attachDB(_ db: Any) {
        _attachDB(db)
    }

    var identifier: String {
        _identifier
    }

    init<E: Environment>(_ env: E) {
        self._identifier = env.identifier
        self._attachDB = {
            env.attachDB($0 as! E.DBElement)
        }
    }

    private let _identifier: String
    private let _attachDB: (Any) -> Void
}

extension Environment {
    func eraseToAnyEnvironment() -> AnyEnvironment {
        AnyEnvironment(self)
    }
}

class EnvironmentFactory {
    enum EnvType {
        case dev
        case live
    }

    func create(_ type: EnvType) -> AnyEnvironment {
        switch type {
        case .dev:
            return DevEnvironment().eraseToAnyEnvironment()
        case .live:
            return LiveEnvironment().eraseToAnyEnvironment()
        }
    }
}

let factory = EnvironmentFactory()
let dev = factory.create(.dev)
print(dev.identifier)

dev.attachDB(DB_Dev_Type()) // OK
dev.attachDB(DB_Production_Type()) // CRASH

This would compile, but it doesn't make much sense. As shown in the last 2 lines, because you erased the type, the compiler has lost knowledge of the original type and the original associated type, thus you could pass in any DB_ (or any type at all, really) but only a specific one would work: the others would cause a fatal error.

A way to keep around the type information in the erased class would be the following:

final class AnyEnvironment: Environment {
    typealias DBElement = Any

    func attachDB(_ db: Any) {
        _attachDB(db)
    }

    var identifier: String {
        _identifier
    }

    let originalEnvironment: Any

    init<E: Environment>(_ env: E) {
        self._identifier = env.identifier
        self._attachDB = {
            env.attachDB($0 as! E.DBElement)
        }
        self.originalEnvironment = env
    }

    private let _identifier: String
    private let _attachDB: (Any) -> Void
}

/// same code as previous example...

if dev.originalEnvironment is DevEnvironment {
    dev.attachDB(DB_Dev_Type())
} else if dev.originalEnvironment is LiveEnvironment {
    dev.attachDB(DB_Production_Type())
} else {
    fatalError()
}

But this is ugly and makes this kind of polymorphism meaningless. To me, the problem here is "what's the client of create(_: EnvType) supposed to do?". If the client gets an environment without knowing the associated type, it cannot attach a DB without this type of casting, that's inevitably subject to the possibility of runtime crashes.

A way to close the system, and make the environment communicate the DB_ that it wants with a value that has finite states, is to use an enum:

struct DB_Dev_Type {}
struct DB_Production_Type {}

enum AttachDB {
    case dev((DB_Dev_Type) -> Void)
    case production((DB_Production_Type) -> Void)
}

protocol Environment {
    var identifier: String { get }
    var attachDB: AttachDB { get }
}

class DevEnvironment: Environment {
    var identifier: String { "dev" }

    var attachDB: AttachDB {
        .dev { db in
            print("db is \(db)")
        }
    }
}

class LiveEnvironment: Environment {
    var identifier: String { "live" }

    var attachDB: AttachDB {
        .production { db in
            print("db is \(db)")
        }
    }
}

class EnvironmentFactory {
    enum EnvType {
        case dev
        case live
    }

    func create(_ type: EnvType) -> Environment {
        switch type {
        case .dev:
            return DevEnvironment()
        case .live:
            return LiveEnvironment()
        }
    }
}

let factory = EnvironmentFactory()
let dev = factory.create(.dev)
print(dev.identifier)

switch dev.attachDB {
case .dev(let attachDB):
    attachDB(DB_Dev_Type())

case .production(let attachDB):
    attachDB(DB_Production_Type())
}

This doesn't need erasure, checks everything at compile time and is not at risk of runtime crashes.

Thank for the you point of view. So I think it's the last suggestions it's the best that I found, but the point is that any time that I will use dev.attachDB I will use the switch or if let . So also in other post I see that there isn't dynamically detect the protocol type before defined.

I tell this because if you return

func create(_ type: EnvType) -> -> Any? {

when you want use it

let factory = EnvironmentFactory()
let dev = factory.create(.dev)

(dev as? DevEnvironment)?.attachDB(DB_Production_Type())

So in the end we can check or cast the factory created to use the method in protocol.

What's the point of the protocol then?

Hi @ExFalsoQuodlibet ,

so I should use the protocol type without cast type or check type. So but the issue is the associatedtype , I do the better with what that I can use in swift5.4.

Thank you

What I'm asking is: given that you're going to return Any from create(_ type: EnvType), there's no need to define the Environment protocol, right?