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.