Search code examples
c++inversion-of-controlioc-containerkangaru

How to properly use IoC container?


I'm very new to IoC idea and I'm trying to jump over the Service Locator pattern. I chose the Kangaru implementation. Suppose I want to use Audio and Logger services in different places of my app. What I have now:

    #include <kangaru/kangaru.hpp>
    #include <iostream>
    using namespace std;

    struct IAudio {
        virtual void playSound(int id) = 0;
    };

    struct Audio : IAudio {
        void playSound(int id) { cout << "Playing " << id << '\n'; }
    };

    struct IAudioService : kgr::abstract_service<IAudio> {};
    struct AudioService : kgr::single_service<Audio>, kgr::overrides<IAudioService> {};
    
    struct ILogger {
        virtual void log(const std::string& message) = 0;
    }; 

    struct Logger : ILogger {
        void log(const std::string& message) { cout << "Log: " << message << '\n'; }
    };

    struct ILoggerService : kgr::abstract_service<ILogger> {};
    struct LoggerService : kgr::single_service<Logger>, kgr::overrides<ILoggerService> {};

    struct User {
        User(kgr::container& ioc) : ioc_{ioc} {}
        kgr::container& ioc_;

        void f1() { ioc_.service<IAudioService>().playSound(1); }
        void f2() { ioc_.service<ILoggerService>().log("Hello"); }
    };

int main()
{
    kgr::container ioc;
    ioc.emplace<AudioService>();
    ioc.emplace<LoggerService>();
    User u(ioc);
    u.f1();
    u.f2();

    return 0;
}

If I understand it correctly, at this point it's just a Service Locator, isn't it? But if I have some nested structure, like this:

    struct A {
        B b;
    }
    struct B {
        C c;
    }
    struct C {
        D d;
    }

, it should be composed in some Composition Root, and class A should be created through IoC-container, which will resolve dependencies automatically. Here will IoC take true advantage, am I right? And I still have to pass IoC-container everywhere, where I need some service. The advantage is passing single parameter for all services against passing multiple.

And one more thing: dependency injection works for free functions in a same way; if I want to use logger in some void f() I should either pass IoC-container through arguments, or use directly inside - and there is no dependency injection in that case. But if I don't want to clutter parameters list, I have no choice.


Solution

  • The main advantages of using a library to handle dependency injection is:

    • The automation of boilerplate code
    • Having a central place that contains the instance about the current context

    Using a dependency injection container, you have that single entity that contains the all the instances. It can be tempting to send that thing everywhere since you'll have the whole context available, but I would advise against.

    In the kangaru documentation, I added this in the guidelines:

    This library is a great tool to minimize coupling, but coupling with this library is still coupling.

    So for example, if void f(); (as a free function) needs the logger, then it should be passed as parameter.

    void f(ILogger& logger) {
        // ...
    }
    

    Now this is where the dependency injection library comes in. Yes you can use the container to get what's inside and send it to the function, but it can be a lot of boilerplate:

    f(ioc.service<ILogger>());
    

    Same thing with your user type, you're using the container as a single thing containing the context, without using it's boilerplate reducing capabilities.

    The best would be to let the library minimize the boilerplate:

    ioc.invoke(f);
    

    The kangaru container has the invoke function. You send it a function like object or a function pointer and it injects the parameter automatically.

    Same thing for the User class. The best would be receive the necessary thing in the constructor:

    struct User {
        User(ILogger& l, IAudio& a) : logger{&l}, audio{&a} {}
    
        ILogger* logger;
        IAudio* audio;
    
        // You can use both logger and audio in the f1 and f2 functions
    };
    

    Of course, it will require to make User a service, but a non-single one:

    struct UserService : kgr::service<User, kgr::dependency<ILoggerService, IAudioService>> {};
    

    Now, defining those service for non single classes can look like boilerplate, but there are ways to reduce it significantly if you use kangaru's newer features like the service map:

    // Define this beside the abstract services
    // It maps A parameter (ILogger const& for example) to a service
    auto service_map(ILogger const&) -> ILoggerService;
    auto service_map(IAudio const&) -> IAudioService;
    

    Then, you can declare services as a one liner, using the service map utility to generate service:

    struct User {
        User(ILogger& l, IAudio& a) : logger{&l}, audio{&a} {}
    
        ILogger* logger;
        IAudio* audio;
    
        // You can use both logger and audio in the f1 and f2 functions
    
        // Map a `User const&` parameter to a autowired service
        friend auto service_map(User const&) -> kgr::autowire;
    };
    

    Then, you can give a name to the generated service:

    // Optional step
    using UserService = kgr::mapped_service_t<User const&>;
    

    Now finally, you can use the container to generate instances:

    // Creates a new instance, since it's a non single
    User newUser1 = ioc.service<User>();
    
    // Or use a generator:
    auto userGenerator = ioc.service<kgr::generator_service<UserService>>();
    
    User newUser2 = userGenerator();
    User newUser3 = userGenerator();
    
    // Or use invoke:
    ioc.invoke([](User user) {
        // user is generated instance
    });
    
    ioc.invoke([](kgr::generator<UserService> gen) {
        User newUser1 = gen();
        User newUser2 = gen();
    });
    

    As you notice, using invoke don't necessarily need to define sevices, as simply adding friend auto service_map(...) -> kgr::autowire in a class that need services in its constructor to make it useable with invoke