Dependency Inversion in C++: choosing the right reference

30. December 2012 19:18 by Sergey in   //  Tags: ,   //   Comments (2)

Inversion of controls, or Dependency inversion, is an interesting and value-proof technique which is used to modularize the system and decouple the components from each other. Low coupling is always an advantage: it simplifies automatic (unit) testing of the component and makes the code better conforming to single responsibility principle.

There are basically 3 ways to declare a dependency to another class.

Either on constructor call or lazy-initialized with the call to public method, the dependency class is created explicitly with "new" operator or requested from global-scoped registry class

  •  constructor injection

The class gets the dependency as an argument to its constructor. It saves the reference to dependent class as a private variable.

  •  property injection

The class gets the dependency by calling its public method or setting a public property. It saves a reference to use later as well.

I'd rather insist on using only constructor injection approach. Though it is probably the most difficult approach from the listed three to implement, it comes with severe advantages: all the dependencies are truly visible with constructor signature; cyclic dependencies may not happen because of the well-defined order of instantiation.

Things are easy (because of the lack of choice) when programming with memory-managed languages like C#. Dependency is injected to the constructor as a reference and saved to a private variable.

C++ brings us more choices. The injected object may come in several different ways:    

1) Instance copyable class

2) Raw pointer

3) Reference

4) STL/Boost smart pointer

Though all of them work in some way to represent a dependency, let's look for a best choice. The pros and cons of each as I may judge:

Instance copyable class

class Object
{
private:

    Dependency m_dep;

public:

    Object(Dependency dep)
    : m_dep (dep)
    {
    }
};



The most useless approach which works only in case Dependency class is completely stateless, e.g. does not have any members. Practically, this rarely happens because Dependency class may store its own dependency.

Raw pointer

class Object
{
private:

    Dependency* m_dep;

public:

    Object(Dependency* dep)
    {
        if (dep==nullptr) throw std::exception("dep null");
        m_dep=dep;
    }
};



This works like true injection. The code is required to check the passed pointer for null. Object class does not own Dependency class, thus it is the responsibility of calling code to make sure the Object is destroyed before the Dependency object. In real application, it is sometimes very difficult to validate.

Reference

class Object : private boost::noncopyable
{
private:

    Dependency& m_dep;

public:

    Object(Dependency& dep)
    : m_dep (dep)
    {
    }
};



Dependency injected to the constructor as a reference. Reference may not be null, so it is a bit safer in this prospective. However this approach brings additional constraints to Object class - it has to be non-copyable since reference may not be copied. You have to either inherit it from something like boost::noncopyable or manually override assignment operator and copy constructor to stop from copying. Like with raw pointer, the ownership constraint is in place. Calling code should provide the correct destruction order for both classes, otherwise the reference becomes invalid and application crashes with access violation.

STL/Boost smart pointer

class Object
{
private:

    std::shared_ptr<Dependency> m_dep;

public:

    Object(std::shared_ptr<Dependency> dep)
    {
        if (!dep) throw std::exception("dep null");
        m_dep=dep;
    }
};


The same approach as Raw pointer approach, but the ownership is controlled by smart pointer mechanism. Still need to check for null in the constructor body. The major advantage is the Dependency object lifetime control - there is no need for the calling application to properly control the destruction order. Once the Dependency class is no longer used it is automatically destroyed by shared_ptr destructor. There are cases when shared_ptr owned objects are not destroyed - so called cyclic references. However with constructor injections cyclic dependencies are not possible due to the specific well-defined order of construction. This works of course if no other injection methods are used across the application. The other con of smart pointer is a small overhead it brings which is not the real problem in the majority of cases.

I vote for smart pointer for the purpose to inject a dependency. While having a smart pointer overhead and the necessity to check for null, it has all the other as advantages. It is even safer than using a reference, since reference may become invalid but shared_ptr cannot. The only thing that is required to do is to check for null during the injection, but once done the pointer is safe. In most of the cases, it is easier to do than to validate the whole lyfecycle chain of all classes in an application when reference or raw pointer is used.

If dependency is declared with service locator or property injection pattern, you may consider using reference approach as an alternative. I still strongly encourage using constructor injection only. Good designed application should not have cyclic dependencies ever.

Outcome of the article



- Use constructor injections only across the application
- Use shared_ptr to pass a dependency
- Do some work in a constructor to validate shared pointer and save it
- Consider making those classes non-copyable to forbid improper usage

Comments (2) -

Javier Diaz
Javier Diaz
11/1/2013 8:35:24 AM #

Very interesting. As for the smart pointer approach, however, I think the most important con is missing: All dependencies need to be heap-allocated, which introduces an important run-time overhead. Regards.

sergey
sergey
11/27/2013 12:36:13 AM #

For most dependency injection implementations all objects are to be long-living objects which means they have to be allocated on a heap. On the other hand it is common to have a limited number of instances of them in the application so memory fragmentation is not a problem.

Add comment

  Country flag

biuquote
  • Comment
  • Preview
Loading