2

I'm currently developing an application using SFML. My biggest concern at the moment is making a layer of abstraction over the library, so I can easily change it to something else if needed. What I'm struggling with is designing an abstraction that's intuitive and not leaky.

An example: drawing in SFML is done using drawable objects (implementing sf::Drawable) and objects capable of rendering them (sf::RenderTarget).

Say I want to wrap those and create my own IDrawable:

class IDrawable {
public:
    virtual void draw(IRenderTarget& target) const = 0;
};

Even though it looks nice, in order for the concrete classes to achieve their goal, they somehow still need to exchange information specific to SFML, i.e. IRenderTarget has to declare an abstraction breaking method, such as

virtual sf::RenderTarget& impl() = 0;

The only solution I can think of is creating concrete classes that require all library-specific parts to be passed via constructor, so there's no cross-class communication of library-specific data. This way, IDrawable would change to:

class IDrawable {
public:
    virtual void draw() = 0;
}; 

Another solution would be abusing the friending mechanism, but I guess that's not great, either.

In other words -- is there an approach for wrapping a 3rd party library, so that the created abstraction is not leaky (doesn't require public accessor methods that break the encapsulation)?

5
  • 1
    I do not fully understand. The concrete subclass Box of IDrawable implements its draw method by calling methods on IRenderTarget, 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 of IRenderTarget will know about SFML's RenderTarget classes, But that is okay, because it IS the wrapper. Jun 14, 2020 at 7:58
  • 2
    You might like to reconsider creating the wrapper in the first place. How likely is it actually, that you will change SFML to something else? How much effort will the initial wrapper be? It is easy to hide away the concrete implementation. It is very hard to hide away the concepts of SFML. If you eventually replace SFML your wrapper's concepts must be mapped to the concepts of that library. That library might not have Drawables nor Renderers. Jun 14, 2020 at 8:05
  • "Where does the need to break the abstraction arise?" The abstraction would get broken at the very call to 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).
    – mdx
    Jun 14, 2020 at 15:35
  • 1
    “I fail to see any problem, sir.” You write _ in order for the concrete classes to achieve their goal_. Please provide a pseudo code example of that concrete class. Then we can tell you how to avoid the problem. Jun 15, 2020 at 4:49
  • 1
    @TuRtoise: don't bury explanations here in comments; instead, use the edit button to improve it. And the answer to your question is: those SFML types need to be wrapped as well.
    – Doc Brown
    Jun 15, 2020 at 5:41

4 Answers 4

3

I think you're well into YAGNI territory here. When you know what your second library looks like, then is the time to look at the abstractions it exposes, compare those with the abstractions from SFML and only then make some decisions as to what your wrapper should look like. Otherwise you are just guessing what you think the common abstractions are, and at risk making a wrapper which doesn't match your second library anyway.

Even given all that:

  • This assumes you'll ever actually replace SFML. Replacing the guts of a system is something software engineers love to talk about but much less often actually do.
  • I'd argue that a lot of the value of a wrapper is actually only when you want to run two implementations simultaneously - if you're actually replacing SFML with something else, just replace it. It's only if you want one version of your code to be easily able to talk to both SFML and Hypothetical Library Two that a wrapper is necessary.
1
  • 1
    I totally agree. Yet, I do not upvote, because it does not answer the actual question. Jun 15, 2020 at 4:42
2

Avoiding Leakage

To avoid leakage, wrap everything that is exposed. Create wrapper classes until you only expose basic types. This means you will have to come up with an abstraction not only for each class, but for all concepts of SFML.

In your example you are wrapping 1:1, i. e. each class in SFML is wrapped to one class of your wrapper: sf::Drawable is mapped to turtoise::IDrawable, sf::RenderTarget is mapped to turtoise::IRenderTarget. If you continue doing that, the user of turtoise::IDrawable will not see any SFML classes. Leakage has been avoided.

Replacing SFML

Yet, it will be a lot of work and you will not be able to replace SFML with anything but another version of SFML. Any non-SFML library will use different concepts than your wrapper layer. Your wrapper does not abstract from SFML.

Abstraction

If you truely like to abstract away from SFML, you must first define your own Turtoise Multimedia Library on the interface level. Then you can create an implementation of your new library using SFML.

When you define your library you might restrict yourself to basic functions. If your library is much simpler than SFML, you will be able to reimplement it using another library. Your replacement will work. Of course you have to restrict yourself when writing your application.

As an alternative you can predict which library you most likely will replace SFML with. Then derive the common core of these two (or three) libraries and combine it in your Tortoise Multimedia Library. Because every concept your libray contains is present in all the libraries, you will be able to create wrappers.

Last Words

I agree with @Phillip Kendall: I doubt you need the abstraction at all.

0
1

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.

1

You are abstracting at the wrong level of detail.

All code that renders or needs to manage drawings of some kind is by its very nature Graphical. It will have a graphical language of some sort be that this library, or that library etc...

When you change graphics libraries you will doubtlessly need to change these graphical classes too. Unless of course the new graphical library is drop in compatible (openGL libraries for instance).

What you want to do is separate the Graphical component of the class from the non graphical component, or the simplistic graphical component from the actual graphical component.

By this I mean a Combo class should separate its model and Domain logic (Model and Controller from MVC) from its display (The View part).

Similarly a Graphical component may be very simple in its needs say a few 2D drawing primitives. It could in theory describe the main logic in terms of those primitives, just drop in a suitable adaptor to the graphical library of choice.

The line here is somewhat blurry though, the more, and more complex the graphical primitives become the fewer and fewer libraries can actually satisfy them. The more and more simple the primitives are obviously the less of the actual graphics capabilities are available for use, including optimised implementations of software algorithms. Unless you really need that cross-gui support (and couldn't use an actual cross platform graphics library) I would lean away from this strategy and prefer to out-right replace the graphical components, even if that means two concurrent versions.

1
  • +1. I think very much the same way. I am personally not a big fan of MVC (prefer a more one-way flow of data) but I always thought it's abstracting at the wrong level... or as I like to put it, choosing the path of maximum resistance.
    – user377672
    Nov 4, 2020 at 11:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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