5

This is a simplified version of a problem I'm facing in my current project.

Let's say we want to display shapes: rectangles, circles, etc. I'll have a Shape class which is subclassed by Rectangle, Circle and so on. I'll then have a World class which represents a 2d plane containing shapes at various coordinates. I'll add shapes to the world via world.add(shape, x, y). The World object and its children should be totally ignorant about the fact that I'm planning on displaying them onscreen.

Now I need to display this. Since I might change my mind about what graphics library I'm using, I'll have a separate Visualizer class. I create one with new Visualizer(world) and then call visualizer.show(). If I change graphics library, I'll just create a different Visualizer class.

Now, somewhere in this code is a function that displays a rectangle, and another that displays a circle. Where should they go?

  1. In the Rectangle and Circle classes? But now the Shape classes not only have to know they're being plotted, they have to know what plotting library I'm using. If I change libraries alot, the Rectangle class will accumulate lots of Rectangle.library1Plot(), Rectangle.library2Plot() methods.

  2. In the Visualizer? But now the Visualizer has to know about all the different subclasses of Shape, it can't just treat a Shape as a Shape and encapsulate what kind of shape it is. I'd have to switch on Shape subclass and then have .plotRectangle, .plotCircle methods, adding more and more of them as I create more shape types.

Neither of these solutions feel right to me.

Somewhere in the code, somebody is going to have to have simultaneous access to two pieces of information: what subclass a Shape is, and what graphics library I'm using. Where's the best place for that?

4
  • 1
    It sounds like you're talking about a viewmodel. The shapes would be the model, the visualizer would belong in the view. How the two interact would seem to be a viewmodel concern.
    – Robbie Dee
    May 24, 2018 at 10:39
  • @RobbieDee When I look up Viewmodel, I mostly get stuff about UIs and insulating the data model from user input. But in my case this is just display, there's no interactivity. I'm not sure how to relate my problem to the Viewmodel concept.
    – Jack M
    May 24, 2018 at 12:31
  • While it mainly used for "proper" user interfaces it is simply to do with separation of concerns. There are a set of objects and something that does the displaying. The viewmodel is just the mechanism for customising the model for whatever is used to display it. You could (and I in fact do) use it for simulations with no user input.
    – Robbie Dee
    May 24, 2018 at 12:35
  • Usually when I get friction when trying to decompose a design like this it's because I'm making the cut at the wrong granularity. If I have to force a square into a round hole with a sledge hammer then maybe I need to reexamine my need for squares when I should be working with the holes =) Dec 19, 2018 at 20:51

7 Answers 7

5

If it is acceptable that the Shape classes know how to draw themselves onto something (without knowing which output device or graphics library is used underneath), then you could introduce a VisualizerInterface interface that provides functions for drawing simple lines and arcs.

The Visializer classes would implement the VisualizerInterface and translate the functions to draw a line or an arc into the correct calls for that particular graphics library.
The Shape classes would use the VisualizerInterface to draw themselves onto any possible Vizualizer without knowing the underlying graphics library.

The Visualizer::show() method could look something like

Visualizer::show() {
  foreach(shape in world.get_shapes()) {
    shape.draw(this)
  }
}

where each class derived from Shape overrides the draw method.

5
  • This is the same basic idea as in DavidArno's answer, although I like this a bit better because we don't have the problem of the ever-growing switch statement in the Visualizer. We still have the problem that no part of the code is ever aware of both the Shape type and the plotting library at the same time, so you can't guarantee that you're plotting every shape in the most efficient way in every library, but maybe that's unavoidable without having a big switch statement somewhere.
    – Jack M
    May 24, 2018 at 13:17
  • @JackM: Have a look into the Visitor design pattern for how you can implement your option 2 without a big switch on the shape type. The Visualizer classes would still have to have a function for each type of shape. May 24, 2018 at 13:31
  • @JackM, you have misunderstood my answer is if think you'd need "[an] ever-growing switch statement in the Visualizer".
    – David Arno
    May 24, 2018 at 14:08
  • The interface doesn't seem to add any value. If targeting multiple display architectures, a common API would be helpful, but base classes are usually more useful in this circumstance, and can take the place of the interface. May 24, 2018 at 18:03
  • @FrankHileman: Although I mention it as an interface, this interface could just as well be provided by a base class. May 25, 2018 at 6:41
3

I would suggest a third option: separate the process of describing a shape from drawing it.

Have a system of describing the drawing of a shape, in terms of a list of straight line and arc definitions. Each shape then has responsibility of defining itself in terms of those line definitions.

The visualizer then has responsibility for translating a set of line descriptions into a rendered shape.

That way, the shapes do not need to know anything about how to render themselves and the visualizer doesn't need to know anything about individual shapes. You completely avoid the situation of "somewhere in the code, somebody is going to have to have simultaneous access to two pieces of information: what subclass a Shape is, and what graphics library I'm using.". The visualizer just calls shape.GetDrawingInstructions() on whatever shape it's provided with. The coupling is then reduced to just that one method and whatever format the set of instructions take.

3
  • The trouble is, it would be a shame to render a Sphere as a collection of polygons if the plotting library I'm using supports rendering spheres directly. So okay, we need a SphereDrawingInstruction... and later on a CylinderDrawingInstruction, TextDrawingInstruction... and then we're back to where we started: the Visualizer has to switch on the subclass of the DrawingInstruction object, so we may as well be switching on the Shape subclass to begin with.
    – Jack M
    May 24, 2018 at 13:12
  • @JackM, not quite. Don’t think polygons; think vectors. There are already a number of vector formats that can be used to describe any 2D or 3D shape without having to have special squateDraw, sphereDraw, textDraw etc instructions.
    – David Arno
    May 24, 2018 at 13:25
  • This would be nice in the 1970's perhaps, but no library would be implemented this way today. The "low-level" graphics library will already have an abundance of shape specific drawing routines. May 24, 2018 at 18:01
1

I've done precisely what you are talking about with a Model/View/ViewModel (MVVM) architecture. As long as your rendering supports this design pattern (i.e. it supports bindings) then it can work. The problem is that you will have a number of views that you are working with.

The Model represented the things being represented. In my case it was a mapping application. We had application specific objects and user objects that helped the user summarize the map better. Since position (i.e. lat/lon) were key elements of the model, we included that here.

The ViewModel took care of things like color, and in some cases switching the representation (i.e. icon representing a thing). The View Models worked for different classes of things. I.e. whether something was a specific point on a map (pin cushion or custom icon), an area (polygon), a line, or an ellipse. These things had specific meaning in our app, so they had to be supported.

The View took care of actually rendering the objects on screen. This is the only portion that was platform specific (i.e. XAML vs. UAP vs. something else). We did have to create a custom panel to render the controls (and optimizations so it wouldn't waste time on things that are out of the view scope).

What makes this possible is the ability to bind attributes from your ViewModel and Model to the View. We had code that lived in the View layer that matched the more specific view to the corresponding ViewModel.

Sure it's a lot of code, but if you wanted to run a simulation without the map being visible it was possible. You would simply execute the Model and VieModel layers.

If you actually have to have drawing instructions, the other option is to still go as far as making the ViewModel but have an abstract Draw(Canvas) method. You would then have to subclass your ViewModels for the specific rendering code you need. Or you can create a set of primitives that are assembled as drawing instructions.

As a general rule with drawing you can get away with the following 2D primitives:

  • Point
  • Line
  • Rectangle
  • Polygon
  • Ellipse
  • Arc
  • Text

Your 3D equivalents wouldn't be too far different from those basic shapes.

1

Imagine how a 3D engine works for a second. The renderer doesn't know how to render circles or cubes or meshes, it knows how to render triangles, and triangles only, so every shape will translate their definitions into a description, which are lists of points, triangles, and normals (there are others for sure but let's omit them) and that is what the renderer understands.

In your case, your Visualizer should ask each Shape a description on how to render them, and the Visualizar will use that information to show them on the screen or any other medium.

I would say, each shape returns a list of triangles that represents the best what their shape is. If you really want to render native circles, you can also add a list of circles and even rectangles, that depends on you. And each Visualizer will take care of rendering that.

When you create a new Shape, you only need to create the algorithm that will create those lists from the description of the Shape, removing the Shape concept for the Visualizer.

1

Fundamentally, what we're trying to accomplish here is multiple polymorphism or double dispatch.

In regular polymorphism, shape.render() does something different depending on what concrete type shape is. If we think of shape as the first argument to a render(Shape shape) function, we can think of this as a situation where which exactly procedure is called depends on the concrete type of the argument in the call.

In multiple polymorphism, which procedure is called should depend on all the concrete types of the arguments in do_something(AbstractType1 argument1, ..., AbstractTypeN argumentN). We'll have combinatorially many implementations of do_something, one for every possible combination of concrete types, and the system should call the correct one according to the tuple of concrete types it sees coming into the call.

What we're trying to do here is exactly that. We have a procedure render(Shape shape, Visualizer visualizer), and we want all the implementations, for every pair of a Shape and a Visualizer, to be totally decoupled, so that we can write each one in any way we like with no constraints. Of course we could always just inspect the types at run-time and then switch on them, and actually have a bunch of procedures called renderSphereWithOpenGL and renderPyramidWithDirectX or whatever, but we'd rather do it in a "pretty" way, using polymorphism.

I'm not aware of any programming language that has explicit multiple dispatch features. Implementing double polymorphism using single polymorphism is the purpose of the Visitor design pattern.

What we would do is have an abstract Shape class with an abstract render(Visualizer visualizer) method that's implemented like this in each concrete class:

class Sphere implements Shape
{
    void render(Visualizer visualizer)
    {
        visualizer.renderSphere(this);
    }
}

and so on for other Shape classes. Each concrete Shape type has to correspond to its own method on the abstract Visualizer class. Then each concrete Visualizer can implement all of those methods in its own way.

Note that this means rendering-related code, even if it's just a one-line method, has to be in the Shape class, which we didn't want. This can be done away with by abstracting out the notion of "an operation that depends on the specific shape subtype" with a ShapeProcessor interface, and rewrite the above as:

class Sphere implements Shape
{
    void acceptProcessor(ShapeProcessor processor)
    {
        processor.processSphere(this);
    }
}

Then Visualizer can implement ShapeProcessor. Of course, if Visualizer is the only ShapeProcessor implementation in the system, then this is a lot of extra abstraction and complexity just to avoid what, let's face it, is pretty much an aesthetic quibble. What I'm calling "process" here is what's called "visit" in a usual exposition of the Visitor pattern.

Note that since double polymorphism is a "symmetrical" problem, we could have done the opposite: put the one-liner methods in the Visualizer classes and put renderWithOpenGL and so on methods in each Shape class. But at that point having rendering code in the Shape class is truly unavoidable. Fundamentally, if we have a constraint that class hierarchy A needs to not know anything about class hierarchy B, then B has to be the "visitor" in the visitor pattern.

For the record, in the end I just put a render(Shape shape) method in Visualizer that inspected shape and switched on its concrete type (this was a Python project) to call renderSphere(), etc, which were implemented by the concrete Visualizer subclasses. Turns out the sky didn't fall in on my head. It was just too ugly for me to have a method called render() in the shape classes, which were supposed to be rendering-agnostic.

0

The World object and its children should be totally ignorant about the fact that I'm planning on displaying them onscreen.

I know, it's not necessarily constructive, but let me ask the question: Why? It seems to me one of the features of your application is to present the shapes. It is in fact the only business logic you chose to describe.

That makes Shapes business objects, and presenting a business function. If this is the case, then having the Shapes display themselves would be my first choice.

So what is the reason you don't want that? Maybe it's the next point:

Since I might change my mind about what graphics library I'm using...

The question should be: Will you change the graphics library so often that it warrants to abstract over it. Abstraction will always cost you, and makes you application more complicated. There is no magical architecture that let's you be more flexible at no costs.

So, if you haven't settled yet for a good library, but once you settle it's unlikely you'll change again, then don't abstract. It will probably not worth it.


That all being said, and without knowing what you "actual" business logic looks like, an alternate design would be just to make the World an interface, with specific implementations for different libraries, and make Shape an interface too, without a hierarchy.

public interface Shape {
    ...business logic methods, for example area(), etc....
}

public interface World {
    Shape addCircle(int x, int y, int r);
    ...

    void display(); // With parameters as needed
}

Then implementation would look like this:

public final class AWTWorld {
    ...
    public void display() {
        JPanel panel = ... // Whatever
    }
}
1
  • In my case I'm writing a physics simulator, so it may be necessary to run the simulator with no visualization, just outputting the results to a text file. So the simulator code should really be totally separate from the visualization code. For the graphics library, I actually am in the process of looking for a good fit for the project, and I'd like to be able to swap new candidates in and out without having to backup the codebase and rewrite it. But both good points.
    – Jack M
    May 24, 2018 at 14:26
0

In the Visualizer? But now the Visualizer has to know about all the different subclasses of Shape, it can't just treat a Shape as a Shape and encapsulate what kind of shape it is. I'd have to switch on Shape subclass and then have .plotRectangle, .plotCircle methods, adding more and more of them as I create more shape types.

In my opinion this solution is actually not so bad if your rendering concerns get sufficiently complicated (ex: a cross-platform advanced real-time multipass 3D renderer with deferred shading). Of course it might manifest itself in a more sophisticated way than a giant switch statement, as in the case of an ECS which might do like:

// Inside visualizer/renderer: draw all particles in the scene.
for (scene.get<ParticleEmitter>(): emitter)
     draw_particles(*emitter);

Yet internally that scene.get<ParticleEmitter>() might involve some downcasting and type checking. The main reason I find that acceptable for sufficiently complex cross-platform rendering concerns is that if you try to abstract the renderer/visualizer at a sufficient level of abstraction to work for all backends, it'll often end up working its way towards the form of the components you are trying to render just specifying exactly themselves:

class IRenderer
{
public:
    virtual ~IRenderer() {}
    virtual void render_particles(const ParticleEmitter& emitter) = 0;
};

You usually can't get more generalized than that in a way that works for all rendering backends with sufficiently complex rendering needs, at which point we don't really have an extensible/reusable solution across disparate types of things to render, and we can't introduce a new type of thing to render without requiring intrusive changes to the renderer/visualizer, only now the intrusive changes are not only to its implementation, but to its interface design as well (and we should absolutely favor changes to implementation over changes to interface when we can help it).

With sufficiently complex rendering concerns those particles might need to be rendered in a separate pass away from other types of things to render. Models with skin materials might need a separate subsurface scattering pass. All lights in the scene might need to be discovered/specified prior to rendering anything. If you try to make the objects tell the renderer what to draw through an abstraction, often the result is not very extensible in those cases, and worse by trying to tell the renderer what to do, the renderer will tend to have to jump through additional hoops storing auxiliary data structures to reorder the rendering requests and figure out the appropriate passes and so forth to make over the rendering requests (which aren't specified in the desired order it needs to render things properly), which it could avoid if it wasn't being told what to render and discovered what to render in your scene.

In the Rectangle and Circle classes? But now the Shape classes not only have to know they're being plotted, they have to know what plotting library I'm using. If I change libraries alot, the Rectangle class will accumulate lots of Rectangle.library1Plot(), Rectangle.library2Plot() methods.

In that case if your rendering concerns aren't so complex, you can simply abstract the renderer like the above and do like so as Bart suggested:

void Rectangle::draw(IVisualizer* visualizer)
{
     // Call abstract, high-level functions in the visualizer
     // to plot a rectangle.
     ...
}

And you can implement that visualizer interface/abstraction for different rendering backends. Yet I think that only works effectively for simpler rendering needs, not like cutting-edge realtime graphics, because trying to come up with a decent abstraction for the visualizer/renderer in the complex cases becomes too difficult and unwieldy and gets into those problems mentioned above.

Another solution is to return some sort of data which the renderer might use to draw which is implemented uniformly for all shapes as David Arno suggested, like:

// Overriden by Shape subtypes like Rectangle to return
// generalized data specifying how to draw each type of shape.
virtual DrawingInfo Shape::drawing_info() const;

In which case all shapes might uniformly return this data which the particular rendering backend can then use to draw the particular shape. The info could be anything required to draw such shapes (a example is a string containing an SVG). If you can come up with a data format (or use an existing one) to specify what to render that works for all your rendering needs, then that also gives you the breathing room to swap out rendering back ends without changing your shape implementations. This also tends to be a tiny bit more flexible than having the shape tell the renderer what to do, since it'll at least allow the renderer a bit more breathing room to more easily do things like request that data at the precise time it needs it in a particular rendering pass (it's at least no longer being told what to do and now discovering what to do).

Somewhere in the code, somebody is going to have to have simultaneous access to two pieces of information: what subclass a Shape is, and what graphics library I'm using. Where's the best place for that?

What graphics library you're using should ideally just be knowledge your renderer/visualizer subtype has, no one else. Those are really gory details that I'd often seek to hide from the rest of the world not only to allow you to swap out rendering backends if you anticipate such a need, but also to simplify everything outside. Those graphics libraries often provide a massive superset of the functionality your software might need, so I've often found it beneficial to just restrict its usage/exposure to one place in the system unless we're talking about a very small and simple project.

The ideal place from a general SE perspective for a subclass/subtype is only for the subclass/subtype to know about its concrete self. However, here it can get a bit tricky because the amount of information required to render something non-trivial often wants to get detailed (in ways that could be difficult to abstract/generalize), and if your rendering needs are very complex, the amount of information required to render anything properly also gets very detailed (in ways that could be even harder to abstract/generalize). So among the options I discussed we have like:

Rendering Implementation -> Concrete Renderable Type

And that's what I suggest for the most complex rendering needs where the rendering implementation for a backend is so complex and likewise needs so much specific information about exactly what it's rendering and when. This leaves the maximum breathing room for the renderer to do whatever it needs to do without being told anything and with full access to the concrete details it needs to perform its elaborate rendering operation. It does require changes if you introduce new types of things to render, for example, but only to the implementations of the concrete renderer(s), and no intrusive central changes to any interfaces.

This is the sort of solution I'd favor if you're writing, say, a complex real-time game engine with sophisticated rendering capabilities designed to target multiple platforms and disparate hardware ranging from mobile devices to consoles to powerful gaming PCs. It provides the absolute maximal breathing room to implement your concrete renderers as needed to perhaps take maximum advantage of the underlying hardware and APIs and GPU shaders and so forth.

The second option mentioned is:

Abstract Renderable/Subtype/Implementation -> Abstract Renderer

And that can work well if you can effectively abstract the rendering requests to a high enough level and it's not a big deal for renderables to tell the render what to render (in some form of abstract, high-level, generalized request). The last is:

Abstract Renderable/Subtype/Implementation -> Generalized Drawing Data
Renderable Implementation -> Generalized Drawing Data

And that can work well if you can come up with an effective (and stable, not prone to change all the time) data format to specify what to render and your rendering needs, perhaps, still aren't too complex so at to make this data format require endless changes to it.

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