di
di Documentation

Dependency Injection

Introduction

Structuring an application with inversion of control (IoC) requires organised and easy to comprehend technique for composing complex hierarchy of components. That is where Dependency Injection - DI comes in hand. In basic understanding, DI is about automated resolution of dependencies. Dependences for a component are defined at the level at which that particular component is resolved. As such interaction with dependency injection framework comprises pf 2 phases:

  1. Registration of components, when recipes of how to create components are registered with component factory map.
  2. Activation of components, where starting from requested component, recursively the component and all dependencies are created using definitions registered in the previous step.

Those two phases cannot non-interleave. Meaning component registration has to be completely finished before any activation takes place. No additional definitions can be added once advanced to activation phase.

All components related to dependency injection are located in tools::di namespace.

Concepts

Registration

Component registration is implemented with definition_builder. Any object convertable to std::function can be registered as a factory. A factory can define ownership and cleaup of allocated objects in 3 ways:

activation_context can be optionally specified as first argument factory definition. This componet provides entry point to activation of dependencies and interaction with activation hierarchy. For instance:

[...]

event_detector event_loader::construct_detector(
        const activation_context& context,
        const wavepatml::abstractEvent& event)
{
    vector<comparison_function> comparisons;

    if (event.base())
    {
        auto base_name = event.base().get();
        comparisons.push_back(
                context.activate<event_detector>(base_name)
                       .as<comparison_function>());
    }

[...]     

In the above context.activate is used to resolve a dependency from a context of factory method.

Activation

Once all definitions are registered with definition_builder that builder is used to create an instance of instance_activator. Instance activator allows instatiation of registered types in 3 different ways:

Modules

A module is a small class that can be used to bundle up a set of related components behind a ‘facade’ to simplify configuration and deployment. The module exposes a deliberate, restricted set of configuration parameters that can vary independently of the components used to implement the module.

The components within a module still make use dependencies at the component/service level to access components from other modules.

Modules do not, themselves, go through dependency injection. They are used to configure definitions, they are not actually registered and resolved like other components.

Example:

#include <di/definition_builder.hpp>
#include <di/instance_activator.hpp>

[...]

struct TestModule
{
    void operator()(definition_builder& builder)
    {
        builder.define_default<TestObject_1>(*this);
    }

    TestObject_1 operator()()
    {
        return { sample_id };
    }
};

definition_builder builder;
builder.define_module(TestModule());

instance_activator activator(std::move(builder));
auto instance = activator.activate_default_unique<TestObject_1>();

Interception

An interceptor is a method injected just after a component is activated. In this way client code can subscribe to notifications about component instances activation with corresponding activation arguments.

Example:

#include <di/definition_builder.hpp>
#include <di/instance_activator.hpp>

[...]

definition_builder builder;
builder.define_default<TestObject_1>([]() -> TestObject_1
{
    return { sample_id };
});

auto intercepted = false;
builder.define_interceptor<TestObject_1>([&intercepted](TestObject_1& activated, const activation_context& context)
{
    intercepted = true;
});

instance_activator activator(std::move(builder));
auto instance = activator.activate_default_raii<TestObject_1>();

An interceptor can be defined as a lambda expression or any other object convertable to std::function.

Decoration

decorator pattern support has been implemented as a special type of interception. Registered decorator function (or any functional object which can be converted into std::function) is invoked whenever an instance of decorated type is activated. Decorator factory is then given with rvalue of the created component and is intended to return a new instance of component of that type wrapping given component with additional 'decorations'.

For example:

#include <di/definition_builder.hpp>
#include <di/instance_activator.hpp>

[...]

using test_function = std::function<void()>;

static auto component_count = 0u;
static auto decorator_count = 0u;

struct component
{
    void operator()()
    {
        component_count++;
    }
};

struct decorator
{
    decorator(test_function&& undecorated)
            : undecorated_(std::move(undecorated))
    {

    }

    void operator()()
    {
        decorator_count++;
        undecorated_();
    }

    test_function undecorated_;
};

definition_builder builder;
builder.define_default<test_function>([]() -> component
{
    return component();
});

// decorator 1
builder.define_decorator<test_function>([](test_function&& undecorated) -> decorator
{
    return decorator(std::move(undecorated));
});
// decorator 2
builder.define_decorator<test_function>([](test_function&& undecorated) -> decorator
{
    return decorator(std::move(undecorated));
});

instance_activator activator(std::move(builder));
auto instance = activator.activate_default_raii<test_function>();

Due to nature of C++ following two modes of decoration are supported:

Annotations

Annotation mechanism is used to pass context information into activation stack during activation. For instance, consider passing detector base resolution in a way it would become available to all invoked factory methods:

// detector_loader.cpp
[...]

auto instance = activator.activate_raii<detection_function>(
        pattern.name(),
        annotations_map(
                pattern.resolution(),
                pattern.instrument()));

[...] 

This can be limitation in some situation, where multiple annotations of the same type would need to be defined. In such situation annotation type can be used. annotation is a template wrapping annotated entry with tag information. Consider following example:

static const auto name_tag = 1u;
static const auto surname_tag = 2u;

definition_builder builder;
builder.define_default<TestObject_1>([](const activation_context& context) ->TestObject_1
{
    auto& name = context.annotation<annotation<string, name_tag>::type>();
    auto& surname = context.annotation<annotation<string, surname_tag>::type>();

    return {  name.value + surname.value };
});

instance_activator activator(std::move(builder));

annotations_map annotations(
        make_annotation<name_tag>(string("John")),
        make_annotation<surname_tag>(string("Smith")));

auto instance = activator.activate_default_raii<TestObject_1>(std::move(annotations));

Usage

Compilation

Project has been verified to build with GCC 6/7 and Clang 4/5 on Ubuntu 14.04+. Boost 1.61 or newer has to be available to the build system.

Build steps:

  1. Clone this repository:
$ git clone https://github.com/lukaszlaszko/di.git
  1. Create build directory and configure cmake from it:
$ cmake <location of cloned repository>
  1. Build with configured build system:
$ cmake --build . --target all
  1. Run unit tests
$ ctest --verbose

To point cmake at custom installation of boost, pass -DBOOST_ROOT=<path to boost root directory> during configuration.

If GCC 6+ is not installed on the build system, make sure libstd++-6 or newer is installed. To install it from apt type:

$ sudo apt-get install libstdc++-6-dev

after prior registration of ubuntu-toolchain-r-test source channel.

Conan package

Conan is an opena source package manager for C/C++. Bintray shadow repository provides latest redistributable package with project artefacts. In order to use it in your cmake based project:

  1. Add following block to root CMakeLists.txt of your project:

    ```cmake if(EXISTS ${CMAKE_BINARY_DIR}/conanbuildinfo.cmake) include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake) conan_basic_setup() else() message(WARNING "The file conanbuildinfo.cmake doesn't exist, you have to run conan install first") endif() ```

  2. Add conanfile.txt side by side with your top level CMakeLists.txt:

    ```text [requires] di/1.0/stable

    [generators] cmake ```

  3. Add shadow to your list of conan remotes:

    ``` $ conan remote add shadow https://api.bintray.com/conan/lukaszlaszko/shadow ```

  1. Install conan dependencies into your cmake build directory:

    ``` $ conan install . -if <build dir>=""> ```

    if for whatever reason installation of binary package fails, add --build di flag to the above. This will perform install the source package and compile all necessary modules.

  2. To link against libraries provided by the package, either add:

    ```cmake target_link_libraries([..] ${CONAN_LIBS}) ```

    for your target. Or specifically:

    ```cmake target_link_libraries([..] ${CONAN_LIBS_DI}) ```

  3. Reload cmake configuration.