1

We are using Abstract Factory design pattern in our project, as the project became complex, most of the time the concrete class functionality need to separate to multiple class.

As the following code snippet, the render are supported by Renderer and Canvas, so there need static_cast in the concrete implementation as following code snippet.

So is it bad design smell for typecasting in Abstract Factory design pattern? If yes, how to improve the design in following snippet?

#include <iostream>

class Renderer
{
public:
    virtual ~Renderer() {};
    virtual void RenderIt(Canvas* canvas) = 0;
};

class OpenGLRenderer : public Renderer 
{
    void RenderIt(Canvas* canvas) {
        // <----How can we avoid to cast here.
        OpenGLCanvas* canvas = static_cast<OpenGLRenderer>(canvas);
        // Do something with opengl.
    }
};

class DirectXRenderer : public Renderer 
{
    void RenderIt(Canvas* canvas) {
        // <---How can we avoid to cast here.
        DirectXCanvas* canvas = static_cast<DirectXCanvas>(canvas);
        // do something with directx
    }
};

#include <string>

class RendererFactory
{
public:
    Canvas * createCanvas(const std::string& type) {
        if(type == "opengl") 
            return new OpenGLCanvas();
        else if(type == "directx") 
            return new DirectXCanvas();
        else return NULL;
    }

    Renderer *createRenderer(const std::string& type) 
    {
        if(type == "opengl") 
            return new OpenGLRenderer();
        else if(type == "directx") 
            return new DirectXRenderer();
        else return NULL;
    }
};
  • 3
    Basically there is a hiccup in your class design. The interface of Renderer takes a generic Canvas, implying that it can work with any sort of canvas, but the implementations put the lie to this statement. But to come up with a better design, a bit more information is needed. Foremost for me is the question, what is the relationship between canvas and renderer? Can any number of renderers work on the same canvas? Can a renderer render to different canvases in different calls? – Sebastian Redl Sep 25 '17 at 8:46
  • @SebastianRedl, DirectX render can only render on DirectX canvas. The canvas store the environment, and the render just hold the current video frame. The multiple render can work on the same canvas. And render can render to different canvas in different call. – ZijingWu Sep 25 '17 at 8:51
  • This design seems problematic to me. Why do you have a base class for Canvas if the users of Canvas need specialized subclasses that have different interfaces? Why even bother with the superclass in that case? – user1118321 Sep 26 '17 at 4:47
  • We want the client code to be the same, so we can switch the implementation easily in an single place. – ZijingWu Sep 27 '17 at 4:05
  • But if that were the case, the subclasses would have identical interfaces and you could swap one for the other. Since a GLRenderer needs a GLCanvas and not just a Canvas, you can't switch the implementation easily in a single place. You now have at least 2 places where you have to switch it. – user1118321 Sep 27 '17 at 5:02
4

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

  • Precise, accurate, comprehensive, and with a broad set of variants making use of language dependent concepts. Impressive contribution ! – Christophe Sep 25 '17 at 16:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.