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?
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.
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.
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.