Skip to content

Tutorial: Assisted injection

Marco Poletti edited this page Aug 6, 2017 · 6 revisions

In this part of the tutorial, we'll build a component that scales double values by a constant factor. The full source is available in examples/scaling_doubles. To warm up, let's write a Multiplier component.

// multiplier.h
class Multiplier {
public:
    // Returns the product of x and y.
    virtual double multiply(double x, double y) = 0;
};

fruit::Component<Multiplier> getMultiplierComponent();
// multiplier.cpp
#include "multiplier.h"

class MultiplierImpl : public Multiplier {
public:
    double multiply(double x, double y) override {
        return x * y;
    }
};

fruit::Component<Multiplier> getMultiplierComponent() {
    return fruit::createComponent()
        .bind<Multiplier, MultiplierImpl>()
        .registerConstructor<MultiplierImpl()>();
}

When there is a canonical implementation of an interface, as in this case, we can put the definition of the interface and the get*Component() function declaration in the same header file, to avoid having two separate header files.

Note that the MultiplierImpl constructor was not wrapped with INJECT(). It's a convenient way to tell Fruit what injector to use, but it can't be used in some cases, for example if MultiplierImpl was provided by another project that doesn't use Fruit. Here we could use it, but we don't just to show the (equivalent) explicit registerConstructor call with the constructor signature.

// scaler.h
class Scaler {
public:
    virtual double scale(double x) = 0;
};

using ScalerFactory = std::function<std::unique_ptr<Scaler>(double)>;

fruit::Component<ScalerFactory> getScalerComponent();

At first, one might be tempted to write Component<Scaler>. However, there is no single Scaler implementation, there is a Scaler for each double value (the scaling factor). So we expose this in the component signature. When returning a value type we can just return it by value, but for interfaces we must return a unique_ptr. Fruit can inject any class, but there is special support for types of the form std::function<std::unique_ptr<T>(...)> as we'll see shortly. Let's write the implementation now.

// scaler.cpp
#include "scaler.h"
#include "multiplier.h"

class ScalerImpl : public Scaler {
private:
    Multiplier* multiplier;
    double factor;

public:
    INJECT(ScalerImpl(ASSISTED(double) factor, Multiplier* multiplier))
        : multiplier(multiplier), factor(factor) {
    }

    double scale(double x) override {
        return multiplier->multiply(x, factor);
    }
};

fruit::Component<ScalerFactory> getScalerComponent() {
    return fruit::createComponent()
        .bind<Scaler, ScalerImpl>()
        .install(getMultiplierComponent());
}

Here we see for the first time the use of the ASSISTED() macro. It's used to mark types in a constructor wrapped with INJECT() that don't have to be injected, but will become the parameters of an injected factory function.

Note that here we installed the multiplier component directly, instead of declaring it as a requirement and then writing a component that composes ScalerImplComponent and MultiplierComponent. It's a tradeoff: this slightly reduces modularity, but makes the code more concise (it avoids 2 files). There is no definite answer on what's better; this tradeoff must be evaluated on a case-by-case basis.

Note also that the bind operation now is doing something different: instead of binding Scaler to ScalerImpl, it's binding an std::function<std::unique_ptr<Scaler>(double)> to a std::function<std::unique_ptr<ScalerImpl>(double)>, which is then injected using constructor injection.

// main.cpp
#include "scaler.h"

int main() {
    fruit::Injector<ScalerFactory> injector(getScalerComponent);
    ScalerFactory scalerFactory(injector);
    
    std::unique_ptr<Scaler> scaler = scalerFactory(12.1);
    std::cout << scaler->scale(3) << std::endl;

    return 0;
}

Here is the main() function, which should be easy to understand at this point. We get an instance of the factory from the injector, and then we call the factory ourselves to get an instance of Scaler.

Now you know the basics of how to use Fruit. To get more familiar with it, try rewriting the above system by yourself or any other program that you like, and experiment a bit by changing the code.

In the next part of the tutorial we'll see how to use Fruit to write a server.