Dependency Injection

December 21, 2023

An object in an application has several "dependencies", i.e. various things that affect how it behaves at run-time. For a capsule part that gets incarnated with capsule instances at run-time, examples of such dependencies include which capsule to create an instance of, what data to pass to the capsule constructor, and which thread that should run the created capsule instance. But the behavior of a capsule instance also depends a lot on which other capsule instances it communicates with at run-time, so those are also examples of dependencies.

Dependency injection is a technique where run-time dependencies of objects are managed by a central injector object, instead of being hardcoded across the application. The injector is configured so it provides the desired dependencies for objects when they are needed at run-time. One benefit with using dependency injection is that objects in your application become more loosly coupled and it becomes much easier to configure and customize the behavior of your application.

There are many dependency injection frameworks for C++ which you can use for the passive C++ classes of your application. To use dependency injection for capsules, the TargetRTS of Model RealTime provides a class RTInjector. You can use this class for registering create-functions for capsule parts at application start-up. When the TargetRTS needs to incarnate a capsule part, it will check if a create-function is registered for it. If so, that create-function will be called for creating the capsule instance. Otherwise, the capsule instance will be created by the TargetRTS itself, as usual.

To use dependency injection in your realtime application you need to specify a capsule factory in your transformation configuration.

Refer to a previous newsletter for more information about capsule factories. Note that use of capsule factories requires C++ 11 or newer.

You can implement the capsule factory using an Artifact in your model. Declare it in the header code snippet:

#include <RTInjector.h>

class CapsuleFactory : public RTActorFactoryInterface {
public:    

    RTActor* create(RTController *rts, RTActorRef *ref, int index) override {

        return RTInjector::getInstance().create(rts, ref, index);
    }

    void destroy(RTActor* actor) override {
        delete actor;
    }

    static CapsuleFactory factory;
};

And then define the capsule factory object referenced from the TC in the implementation code snippet:

CapsuleFactory CapsuleFactory::factory;

The code above ensures that the TargetRTS will create each capsule instance by means of the RTInjector singleton object. Now you just need to register create-functions for all capsule parts where you want to customize how a capsule instance should be created. You need to do this early, typically at application start-up. At least it must be done before the TargetRTS attempts to create a capsule instance in a capsule part which you want to customize with dependency injection. A good place can be to do it in the constructor of the top capsule, or in the main function of your application.

Here is an example of how to register a create-function:

RTInjector::getInstance().registerCreateFunction("/logSystem:0/logger",
    [this](RTController * c, RTActorRef * a, int index) {                       
        return new TimestampLogger_Actor(c, a);
    }
);

Note the following:

In most cases your application will only register create-functions once at start-up. However, RTInjector allows to do it at any time, and you can also remove or replace an already registered create function. This makes it possible to implement very dynamic dependency injection scenarios. For example, you can change which capsule that gets instantiated depending on how much memory is currently available.

Dependency injection can for example be useful when implementing capsule unit testing in order to "mock out" capsules which the capsule-under-test depends on. In this case you could for example let the registration of create-functions be controlled by a configuration file that is part of the test case.

Another scenario is to combine dependency injection with Build Variants to build multiple variants of an application by means of high-level build settings (so called "build variants"). A sample application that uses dependency injection for this purpose can be found on GitHub.


Read more about dependency injection in the TargetRTS documentation.