From my standpoint, when you run into heavy resistance trying to design things like this without leaking third-party details (or any details that you're uncomfortable leaking), you might be directing the bulk of dependencies and designing the abstractions in the opposite direction of the path of least resistance.
For example, in your case, you are trying to abstract away all required rendering capabilities to allow non-intrusive replacement of the rendering backend and directing a whole lot of dependencies towards abstractions of rendering concepts. Yet is that really the path of least resistance? Unless your design requirements for rendering are very simple and easy to fully anticipate in advance, that could get explosively complex and involve repeated design iterations and cascading design breakages in the future as you tackle more and more rendering targets and discover their unique capabilities and individual requirements.
What about the other way around? Can you reason more confidently about the design requirements on your end, absent third-party rendering requirements, and have your abstract interfaces for the logical components in your architecture (ex: game components) expose enough data so that they can be rendered (and possibly serialized, controlled by user input, etc) by anything? That might be so much easier to design and get right without getting tangled up in low-level implementation details you get tempted to expose in public interfaces, in which case you might do something like this:
// Abstracts an entire renderer (not its individual rendering capabilities).
class IRenderer
{
public:
virtual ~IRenderer() {}
// Renders all the visible objects in the scene.
virtual void render(const IScene& scene) = 0;
};
... where something like scene
above provides everything any renderer will need to access in order to render the scene/world/etc. Then you might have an OpenGL renderer, SFML renderer, DirectX renderer, and you don't have to try to build abstractions that target the common-denominator aspects of all their shared capabilities that you require in advance among all third party libraries that you use or possibly could use in the future in your engine.
Over time, as you implement multiple concrete renderers, you might find ways to reuse some code between them and so forth to simplify their implementation and make it easier to introduce new rendering targets. But at least you don't have to start out trying to build some super ambitious set of abstractions that are prone to leak details that could change in the future. This type of design also gives you tremendous breathing room to adapt to the unique capabilities of the renderer (even ones you didn't anticipate in advance), build efficient caches to accelerate drawing under the hood without building abstractions over them, easily handle both immediate and retained mode designs, even have an implementation that outputs to the console, etc. And most of all it should allow you to focus on building higher-level abstractions where you don't get knee-deep in complex details while trying to design abstract interfaces that vary wildly from one concrete implementation to the next in ways that are extremely difficult to fully hide away.
Box
ofIDrawable
implements itsdraw
method by calling methods onIRenderTarget
, e. g.target.line(a,b,c,d)
. Where does the need to break the abstraction arise? Could you please clarify? Of course, the concrete implementation ofIRenderTarget
will know about SFML'sRenderTarget
classes, But that is okay, because it IS the wrapper.target.line(a,b,c,d)
, because most likely, either one of the parameters would be an "SFML type" or it would expose such types in its API. I mean - no matter how many wrapper classes I create, there has to be this one spot in where SFML code is actually used. So either I encapsulate it all within one class, or I make my interfaces dependent on SFML API (but this way the code structure gets more modular).