Dependency injection and ownership issue

Viewed 191

I have a code base where constructor dependency injection is used a lot, mainly because it makes testing much easier. So there’s a lot of code like this:

class Client
{
    public:
    Client(DependencyBase& s) : s_{s}{}
    private:
    DependencyBase& s_;
};

The problem with this approach is that now parent context must construct and own all dependencies although it has no use for them. It also makes the code ugly as constructor initializer or argument lists may become very long. Is there a better way to structure the code to avoid this problem? Unfortunately dynamic memory allocation is not allowed...

Thanks for any advices!

2 Answers

If you cannot use dynamic memory and still want the Client class to own the dependency, injecting it as a template parameter is an option, for example:

template <typename Dependency>
class Client
{
public:
    // ...

private:
    Dependency s;
};

or if you need parameters

template <typename Dependency>
class Client
{
public:
    template <typename... Args>
    explicit Client(Args&&... args) : s{std::forward<Args>(args)...} {}

    // ...
private:
    Dependency s;
};

To instantiate it, you need to pass the class of the dependency, plus its parameters:

Client<MyDependency> client{"foo", 42};

The effect is that client will internally have the dependency

MyDependency s{"foo", 42};

The advantage of template parameters is that it will not create a performance overhead, but it is more verbose. Also you will have to expose the class implementation in the header, which is a consideration if the increased compile time is a problem.

Is there a better way to structure the code to avoid this problem?

Some options:

  1. Have an extra constructor which takes an rvalue-reference for Dependency, with a default value, so you can call it as though it was a no-args constructor. You'll also need to store your DependencyBase then...:
    class Client {
    public:
        Client(DependencyBase& s) : s_{s} {}
        Client(DependencyBase&& s) : s_{s} {}
    private:
        DependencyBase s_;
    };
    
  2. Have a default constructor, which constructs a DependencyBase; if that's not possible - you're going to have to either construct it or provide the arguments for constructing it, outside the Client, anyway. So,
    class Client {
    public:
        Client() : s_{arguments,go,here} {}
    // or:
    //  Client() : s_{function_which_creates_a_DependencyBase()} {}
        Client(DependencyBase&& s) : s_{s} {}
    private:
        DependencyBase s_;
    };
    
  3. If you want both the possibility of changing an external DependencyBase and having your own DependencyBase, you could have a std::variant<DependencyBase,std::reference_wrapper<DependencyBase>> s_, and your constructors would initialize the first (given an rvalue reference) or the second (given an lvalue reference). This will make life a bit more complicated inside Client but simpler on the outside.

Unfortunately dynamic memory allocation is not allowed...

Your code didn't seem to use dynamic memory allocation anywhere.

Related