(First a general note: that is not an abstract factory. The pattern gets its name from having an abstract class or interface for the factory. You're then supposed to have a different factory class for each family of types, instead of choosing the type from looking at a string. However, you might choose a concrete factory class depending on a string. There is such an example at the end of this answer.)
This problem is in fact discussed in the Design Patterns book:
But even when no coercion is needed, an inherent problem remains: All products are returned to the client with the same abstract interface as given by the return type. The client will not be able to differentiate or make safe assumptions about the class of a product. If clients need to perform subclass-specific operations, they won't be accessible through the abstract interface. Although the client could perform a downcast (e.g., with dynamic_cast
in C++), that's not always feasible or safe, because the downcast can fail. This is the classic trade-off for a highly flexible and extensible interface.
— Gamma, Helm, Johnson, Vlissides: Design Patterns. Elements of Reusable Object-Oriented Software. 1994. See p. 91 in the chapter Abstract Factory.
So if the decision between OpenGL and DirectX needs to be a runtime decision, then C++ can only give you that with a loss of type safety. Many design patterns feel much more comfortable in more dynamic languages, but those don't guarantee comparable type safety in the first place. E.g. you probably wouldn't think about this problem at all in Python, but the necessary cast is very visible in C++. So the dynamic_cast
isn't really such a bad thing.
(Note that you should use a dynamic_cast
here to assert that the runtime type of the object really matches. A static_cast
would perform an unchecked downcast and is not type-safe.)
What you can do is use templates instead of inheritance. All code using the renderers/canvasses would then be templated over the API type and would effectively be compiled twice. For example:
struct DirectXCanvas { ... };
struct DirextXRenderer {
void render(DirectXCanvas& canvas);
};
struct DirectXAPI {
using Canvas = DirectXCanvas;
using Renderer = DirectXRenderer;
};
struct OpenGLCanvas { ... };
struct OpenGLRenderer {
void render(OpenGLCanvas& canvas);
};
struct OpenGPLAPI {
using Canvas = OpenGLCanvas;
using Renderer = OpenGLRenderer;
};
template<typename API>
void client_code() {
API::Renderer r;
API::Canvas c1;
API::Canvas c2;
r.render(c1);
r.render(c2);
}
int main() {
string type = ...;
if (type == "directx") {
client_code<DirectXAPI>();
} else if (type == "opengl") {
client_code<OpenGLAPI>();
} else {
return 1;
}
return 0;
}
In the above example, the API struct takes over the role of your factory. Here using type aliases is sufficient, in a more complex example these API types could also provide methods.
Depending on how the client code is structured, it is sometimes possible to introduce a higher-level interface that abstracts over all concrete types. The concrete subclasses of the interface then internally now about the specific Canvas and Renderer types. This is known as type erasure.
Note that in languages with generics (as opposed to templates), it is possible to write this kind of code without casts. The fundamental difference is that template arguments are placeholders that must be filled in with concrete types, whereas generics have type variables that are sufficient for checking type safety, but don't specify a concrete run-time type. We'd then have (using Java):
interface Renderer<Canvas> {
void render(Canvas c);
}
interface AbstractFactory<Canvas> {
Canvas createCanvas();
Renderer<Canvas> createRenderer();
}
class OpenGLCanvas { ... }
class OpenGLRenderer implements Renderer<OpenGLCanvas> {
@Override void render(OpenGLCanvas c) { ... }
}
class OpenGLFactory implements AbstractFactory<OpenGLCanvas> {
@Override OpenGLCanvas createCanvas() {
return new OpenGLCanvas();
}
@Override OpenGLRenderer createRenderer() {
return new OpenGLRenderer();
}
}
class DirectXCanvas { ... }
class DirectXRenderer implements Renderer<DirectXCanvas> {
@Override void render(DirectXCanvas c) { ... }
}
class DirectXFactory implements AbstractFactory<DirectXCanvas> {
@Override DirectXCanvas createCanvas() {
return new DirectXCanvas();
}
@Override DirectXRenderer createRenderer() {
return new DirectXRenderer();
}
}
void <Canvas> clientCode(AbstractFactory<Canvas> factory) {
Renderer<Canvas> r = factory.createRenderer();
Canvas c1 = factory.createCanvas();
Canvas c2 = factory.createCanvas();
r.render(c1);
r.render(c2);
}
static void main(String... argv) {
String type = ...;
if ("directx".equals(type)) {
clientCode(new DirectXFactory());
} else if ("opengl".equals(type)) {
clientCode(new OpenGLFactory());
} else {
System.exit(1);
}
}
The difference to C++ here is that we do in fact use Renderer and Factory interfaces, and that the client code only needs to be compiled once, not again for each API. However, we still have to carry all necessary type variables (here: the Canvas) around. In Java that is especially cumbersome since the language does not have type aliases.