Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dependency inversion (from S.O.L.I.D principles) in C++

Tags:

c++

oop

After reading and watching much about SOLID principles I was very keen to use these principles in my work (mostly C++ development) since I do think they are good principles and that they indeed will bring much benefit to the quality of my code, readability, testability, reuse and maintainability. But I have real hard time with the 'D' (Dependency inversion). This principal states that:
A. High-level modules should not depend on low-level modules. Both should depend on abstractions.
B. Abstractions should not depend on details. Details should depend on abstractions.

Let me explain by example:
Lets say I am writing the following interface:

    class SOLIDInterface {
      //usual stuff with constructor, destructor, don't copy etc
      public:
      virtual void setSomeString(const std::string &someString) = 0;
    };

(for the sake of simplicity please ignore the other things needed for a "correct interface" such as non virutal publics, private virtuals etc, its not part of the problem.)

notice, that setSomeString() is taking an std::string.
But that breaks the above principal since std::string is an implementation.
Java and C# don't have that problem since the language offers interfaces to all the complex common types such as string and containers.
C++ does not offer that.
Now, C++ does offer the possibility to write this interface in such a way that I could write an 'IString' interface that would take any implementation that will support an std::string interface using type erasure
(Very good article: http://www.artima.com/cppsource/type_erasure.html)

So the implementation could use STL (std::string) or Qt (QString), or my own string implementation or something else.
Like it should be.

But this means, that if I (and not only I but all C++ developers) want to write C++ API which obeys SOLID design principles ('D' included), I will have to implement a LOT of code to accommodate all the common non natural types.
Beyond being not realistic in terms of effort, this solution has other problems such as - what if STL changes?(for this example)
And its not really a solution since STL is not implementing IString, rather IString is abstracting STL, so even if I were to create such an interface the principal problem remains.
(I am not even getting into issues such as this adds polymorphic overhead, which for some systems, depending on size and HW requirements may not be acceptable)

So may question is:
Am I missing something here (which I guess the true answer, but what?), is there a way to use Dependency inversion in C++ without writing a whole new interface layer for the common types in a realistic way - or are we doomed to write API which is always dependent on some implementation?

Thanks for your time!

EDIT: From the first few comments I received so far I think a clarification is needed: The selection of std::string was just an example. It could be QString for that matter - I just took STL since it is the standard. Its not even important that its a string type, it could be any common type.

EDIT2: I have selected the answer by Corristo not because he explicitly answered my question but because the extensive post (coupled with the other answers) allowed me to extract my answer from it implicitly, realizing that the discussion tends to drift from the actual question which is: Can you implement Dependency inversion in C++ when you use basic complex types like strings and containers and basically any of the STL with an effort that makes sense. (and the last part is a very important element of the question). Maybe I should have explicitly noted that I am after run-time polymorphism not compile time. The clear answer is NO, its not possible. It might have been possible if STL would have exposed abstract interfaces to their implementations (if there are indeed reasons that prevent the STL implementations to derive from these interfaces (say, performance)) then it still could have simply maintained these abstract interfaces to match the implementations).

For types that I have full control over, yes, there is no technical problem implementing the DIP. But most likely any such interface (of my own) will still use a string or a container, forcing it to use either the STL implementation or another. All the suggested solutions below are either not polymorphic in runtime, or/and are forcing quiet a some coding around the interface - when you think you have to do this for all these common types the practicality is simply not there.

If you think you know better, and you say it is possible to have what I described above then simply post the code proving it. I dare you! :-)

like image 234
dkish Avatar asked Apr 23 '17 21:04

dkish


People also ask

What is Dependency Inversion principle example?

In our example, CustomerBusinessLogic depends on the DataAccess class, so CustomerBusinessLogic is a high-level module and DataAccess is a low-level module. So, as per the first rule of DIP, CustomerBusinessLogic should not depend on the concrete DataAccess class, instead both classes should depend on abstraction.

What determines the dependency inversion rule of SOLID rules?

The Dependency Inversion Principle (DIP) states that high-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details. Details should depend upon abstractions.

What is inversion of control in SOLID principles?

Inversion of Control (IoC) is a principle in which code receives the flow of control from outside instead of being responsible to create it itself.

Why do we need dependency inversion C#?

Benefits of Implementing the Dependency Inversion Principle Our classes are not tightly coupled with the lower-tier objects and we can easily reuse the logic from the high-tier modules. So, the main reason why DIP is so important is the modularity and reusability of the application modules.


2 Answers

Note that C++ is not an object-oriented programming language, but rather lets the programmer choose between many different paradigms. One of the key principles of C++ is that of zero-cost abstractions, which in particular entails to build abstractions in such a way that users don't pay for what they don't use.

The C#/Java style of defining interfaces with virtual methods that are then implemented by derived classes don't fall into that category though, because even if you don't need the polymorphic behavior, were std::string implementing a virtual interface, every call of one of its methods would incur a vtable lookup. This is unacceptable for classes in the C++ standard library supposed to be used in all kinds of settings.

Defining interfaces without inheriting from an abstract interface class

Another problem with the C#/Java approach is that in most cases you don't actually care that something inherits from a particular abstract interface class and only need that the type you pass to a function supports the operations you use. Restricting accepted parameters to those inheriting from a particular interface class thus actually hinders reuse of existing components, and you often end up writing wrappers to make classes of one library conform to the interfaces of another - even when they already have the exact same member functions.

Together with the fact that inheritance-based polymorphism typically also entails heap allocations and reference semantics with all its problems regarding lifetime management, it is best to avoid inheriting from an abstract interface class in C++.

Generic templates for implicit interfaces

In C++ you can get compile-time polymorphism through templates. In its simplest form, the interface that an object used in a templated function or class need to conform to is not actually specified in C++ code, but implied by what functions are called on them.

This is the approach used in the STL, and it is really flexible. Take std::vector for example. There the requirements on the value type T of objects you store in it are dependent on what operations you perform on the vector. This allows e.g. to store move-only types as long as you don't use any of the operations that need to make a copy. In such a case, defining an interface that the value types needs to conform to would greatly reduce the usefulness of std::vector, because you'd either need to remove methods that require copies or you'd need to exclude move-only types from being stored in it.

That doesn't mean you can't use dependency inversion, though: The common Button-Lamp example for dependency inversion implemented with templates would look like this:

class Lamp {
public:
    void activate();
    void deactivate();
};

template <typename T>
class Button {
    Button(T& switchable)
        : _switchable(&switchable) {
    }

    void toggle() {
        if (_buttonIsInOnPosition) {
            _switchable->deactivate();
            _buttonIsInOnPosition = false;
        } else {
            _switchable->activate();
            _buttonIsInOnPosition = true;
        }      
    }

private:
   bool _buttonIsInOnPosition{false};
   T* _switchable;  
}

int main() {
   Lamp l;
   Button<Lamp> b(l)

   b.toggle();
}

Here Button<T>::toggle implicitly relies on a Switchable interface, requiring T to have member functions T::activate and T::deactivate. Since Lamp happens to implement that interface it can be used with the Button class. Of course, in real code you would also state these requirements on T in the documentation of the Button class so that users don't need to look up the implementation.

Similarly, you could also declare your setSomeString method as

template <typename String>
void setSomeString(String const& string);

and then this will work with all types that implement all the methods you used in the implementation of setSomeString, hence only relying on an abstract - although implicit - interface.

As always, there are some downsides to consider:

  • In the string example, assuming you only make use of .begin() and .end() member functions returning iterators that return a char when dereferenced (e.g. to copy it into the classes' local, concrete string data member), you can also accidentally pass a std::vector<char> to it, even though it isn't technically a string. If you consider this a problem is arguable, in a way this can also be seen as the epitome of relying only on abstractions.

  • If you pass an object of a type that doesn't have the required (member) functions, then you can end up with horrible compiler error messages that make it very hard to find the source of the error.

  • Only in very limited cases it is possible to separate the interface of a templated class or function from its implementation, as is typically done with separate .h and .cpp files. This can thus lead to longer compile times.

Defining interfaces with the Concepts TS

if you really care about types used in templated functions and classes to conform to a fixed interface, regardless of what you actually use, there are ways to restrict the template parameters only to types conforming to a certain interface with std::enable_if, but these aren't very readable and very verbose. In order to make this kind of generic programming easier, the Concepts TS allows to actually define interfaces that are checked by the compiler and thus greatly improves diagnostics. With the Concepts TS, the Button-Lamp example from above translates to

template <typename T>
concept bool Switchable = requires(T t) {
    t.activate();
    t.deactivate();
};

// Lamp as before

template <Switchable T>
class Button {
public:
    Button(T&);    // implementation as before
    void toggle(); // implementation as before
private:
    T* _switchable;
    bool _buttonIsInOnPosition{false};
};

If you can't use the Concepts TS (it is only implemented in GCC right now), the closest you can get is the Boost.ConceptCheck library.

Type erasure for runtime polymorphism

There is one case where compile-time polymorphism doesn't suffice, and that is when the types you pass to or get from a particular function aren't fully determined at compile-time but depend on runtime parameters (e.g. from a config file, command-line arguments passed to the executable or even the value of a parameter passed to the function itself).

If you need to store objects (even in a variable) of a type dependent on runtime parameters, the traditional approach is to store pointers to a common base class instead and to use dynamic dispatch via virtual member functions to get the behavior you need. But this still suffers from the problem described before: You can't use types that effectively do what you need but were defined in an external library, and thus don't inherit from the base class you defined. So you have to write a wrapper class.

Or you do what you described in your question and create a type-erasure class. An example from the standard library is std::function. You declare only the interface of the function and it can store arbitrary function pointers and callables that have that interface. In general, writing a type erasure class can be quite tedious, so I refrain from giving an example of a type-erasing Switchable here, but I can highly recommend Sean Parent's talk Inheritance is the base class of evil, where he demonstrates the technique for "Drawable" objects and explores what you can build on top of it in just over 20 minutes.

There are libraries that help writing type-erasure classes though, e.g. Louis Dionne's experimental dyno, where you define the interface via what he calls "concept maps" directly in C++ code, or Zach Laine's emtypen which uses a python tool to create the type erasure classes from a C++ header file you provide. The latter also comes with a CppCon talk describing the features as well as the general idea and how to use it.

Conclusion

Inheriting from a common base class just to define interfaces, while easy, leads to many problems that can be avoided using different approaches:

  • (Constrained) templates allow for compile-time polymorphism, which is sufficient for the majority of cases, but can lead to hard-to-understand compiler errors when used with types that don't conform to the interface.

  • If you need runtime polymorphism (which actually is rather rare in my experience), you can use typ-erasure classes.

So even though the classes in the STL and other C++ libraries rarely derive from an abstract interface, you can still apply dependency inversion with one of the two methods described above if you really want to.

But as always, use good judgment on a case-by-case basis whether you really need the abstraction or if it is better to simply use a concrete type. The string example you brought up is one where I'd go with concrete types, simply because the different string classes don't share a common interface (e.g. std::string has .find(), but QStrings version of the same function is called .contains()). It might be just as much effort to write wrapper classes for both as it is to write a conversion function and to use that at well-defined boundaries within the project.

like image 165
Corristo Avatar answered Oct 15 '22 16:10

Corristo


Ahh, but C++ lets you write code that is independent of a particular implementation without actually using inheritance.

std::string itself is a good example... it's actually a typedef for std::basic_string<char, std::char_traits<char>, std::allocator<char>>. Which allows you to create strings using other allocators if you choose (or mock the allocator object in order to measure number of calls, if you like). There just isn't any explicit interface like an IAllocator, because C++ templates use duck-typing.

A future version of C++ will support explicit description of the interface a template parameter must adhere to -- this feature is called concepts -- but just using duck-typing enables decoupling without requiring redundant interface definitions.

And because C++ performs optimization after instantiation of templates, there's no polymorphic overhead.

Now, when you do have virtual functions, you'll need to commit to a particular type, because the virtual-table layout doesn't accommodate use of templates each of which generates an arbitrary number of instances each of which require separate dispatch. But when using templates, you'll won't need virtual functions nearly as much as e.g. Java does, so in practice this isn't a big problem.

like image 37
Ben Voigt Avatar answered Oct 15 '22 16:10

Ben Voigt