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?

  • 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 '18 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 '18 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 '18 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 =) – Patrick Hughes Dec 19 '18 at 20:51
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.

  • 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 '18 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. – Bart van Ingen Schenau May 24 '18 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 '18 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. – Frank Hileman May 24 '18 at 18:03
  • @FrankHileman: Although I mention it as an interface, this interface could just as well be provided by a base class. – Bart van Ingen Schenau May 25 '18 at 6:41
2

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.

  • 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 '18 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 '18 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. – Frank Hileman May 24 '18 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.

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
    }
}
  • 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 '18 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.

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.