Module decorating_containers

Aedi, a dependency injection library.

Aedi is a dependency injection library. It does provide a set of containers that do IoC, and an interface to configure application components (structs, objects, etc.)

Aim

The aim of library is to provide a dependency injection solution that is feature rich, easy to use, easy to learn, and easy to extend up to your needs.

Aedi does provide basic containers singleton and prototype, which can be combined into some- thing more complex. Though, on top of their behavior additional one can be built, such as a container that can be enabled/disabled, or a container that adds observer pattern. Out of the box framework does provide following decorating containers:

  • aliasing - provides aliasing functions
  • gc rigistered - registers instantiated components with gc, when other that gc allocator are used
  • subscribable - provides observable pattern on container events
  • typed - serves components based on their implemented interfaces
  • proxy - serves component proxies instead of original ones
  • deffered - provides deffered construction of components

Each decorating container listed below will be explained in detail, followed by a short explanation of what current example does.

Aliasing

The aliasing container decorator adds ability to add aliases to existing components in container. Therefore a "foo" component can have multiple aliases "moo", "boo" etc. Each alias will resolve to same component in container. Such feature is useful when an existing component in container for compatibility reasons should have another identity as well. To use aliasing container simply wrap any container in aliasing method which will decorate existing one. UFCS syntax is desired for fluent like style.

auto cont = aggregate(
    singleton, "singleton",
    prototype, "prototype",
    values, "parameters",
    gasoline, "gasoline",
    electric, "electric",
    diesel, "diesel"
).aliasing;

GcRegistered

GC registered decorating container registers any created component with global gc available in D. Since any component can have customized memory allocation strategy through std.experimental.allocator, were one of strategy is to use garbage collector, and each component can have dependencies to other components, in some cases, gc on collection cycle could deallocate a component (which technically isn't referenced by anyone) even if it is still referenced by other component of which gc is not aware of. GC registered container aims to register all public components provided by underlying container into GC in order to avoid the case from above. To use it, just suffix any container with gcRegistered.

auto cont = aggregate(
    singleton, "singleton",
    prototype, "prototype",
    values, "parameters",
    gasoline, "gasoline",
    electric, "electric",
    diesel, "diesel"
).aliasing.gcRegistered;

Subscribable

Subscribable container offers a list of events to which a listener can subscribe and perform operations on. Currently only two events are provided:

  1. instantiation pre event - fired before instantiation of container
  2. instantiation post event - fired after instantiation of container
An example of such listeners can be like in listing below, which will enable a container before instantiation based on profile argument

cont.subscribable.subscribe(
    ContainerInstantiationEventType.pre,
    {
        if (cont.has(cont.locate!string("profile"))) {
            cont.locate!Switchable(cont.locate!string("profile")).enabled = true;
            return;
        }

        throw new Exception("No engine profile selected. Specify it with -p");
    }
);

Typed

Typed container decorates underlying container with ability to provide a component based on interface it implements. Code using typed container attempts to get a component by an interface such as Engine in this example, typed container will check if underlying container has a component identified by provided interface. If yes it will give the component from decorated container. If not, it will search for all registered components that implement the required interface and use first found component to serve it. Notice that in this example, no Engine was registered in container by Engine interface. Each instance GasolineEngine, ElectricEngine, and DieselEngine are registered by their type only. No attempt to alias one of them to Engine is done, yet when Car is instantiated, a component implementing Engine interface is supplied. This magic is done by typed container, which supplied first available component implementing Engine interface.

Proxy

A proxy container proxies all components supplied by decorated container. It will supply proxy objects instead of real components, which in turn will on each public call of a proxy will redirect it to component in redirected container, and return a value from it. The proxy container alone is not much useful. It is intended to be used along with containers that are dependendet on some global state to provide components. An example of such containers could be containers that provide different instances of same component per thread or fiber, or in case of web application, a container that gives new instances of same component for each new available request web application receives.

Deffered

Deffered container, executes delayed components construction and configuration. Construction and configuration of a component can occur in cases when there is a circular dependency between a set of components. In such case to resolve it, injection of one dependency can be delayed. Delaying of construction or configuration is left to component factory, while execution of delayed action is enforced upon Deffered container. To enable circular dependency resolution, decorate container with deffered decorator, and register components using withConfigurationDefferring configuration context (simply append it after container.configure method). This exampe shows a typical example of circular dependency. A car has a dependency on four tires, while each tire has a dependency on a car instance, resulting in a circular dependency. Removing deffered or withConfigurationDefferring will result in circular dependency exception thrown by container.

Example

Such behavior can be useful, like in car company example, that at certian point of time decided that their application should provide upon start cars with different engines, electric/diesel/gasoline.

The workflow needed to implement in application in order to allow 3 different configurations is shown below and consists of following steps:

  1. read profile argument from command line or environment
  2. switch on it, and register diesel/gasoline/electric engine by Engine interface depending on selected profile

Following snippet of code shows all decorating containers in use except of proxy one.

auto decorated() {
    auto gasoline = singleton.typed.switchable.enabled(false);
    auto electric = singleton.typed.switchable.enabled(false);
    auto diesel = singleton.typed.switchable.enabled(false);

    auto cont = aggregate(
        singleton, "singleton",
        prototype, "prototype",
        values, "parameters",
        gasoline, "gasoline",
        electric, "electric",
        diesel, "diesel"
    ).aliasing.gcRegistered.deffered;

    return cont.subscribable.subscribe(
        ContainerInstantiationEventType.pre,
        {
            if (cont.has(cont.locate!string("profile"))) {
                cont.locate!Switchable(cont.locate!string("profile")).enabled = true;
                return;
            }

            throw new Exception("No engine profile selected. Specify it with -p");
        }
    );
}

The profile based container is assembled from 3 typed and switchable containers, and a subscribable composite container with gc component registration and construction of deffered components. When the application is booted up, the code from main(string[] args) loads into container "profile" argument. Afterwards components are registered into container, and for each profile, the right engine is registered in respective gasoline, electric, diesel containers. Once this is finished, the container is instantiated using intantiate method. During instantiation phase, subscribable composite container fires an pre-instantiation event on which, a delegate is attached, that checks for "profile" argument, and enables the container identified by value in profile container. Afterwards construction of components proceeds. When car is constructed typed container jumps in and provides an implenentation of Engine for car depending on enabled container. When construction arrives at a Tire, a circular dependency is detected, and construction of a component is deffered at later stage in order to break dependency chain. Once component is constructed, it is registered with global gc instance in order to avoid destruction of gc managed components while they are still referenced by non-managed components. Once instantiation is finished, car is fetched from container and displayed in console.

Try running this example, pass as argument --profile with value of gasoline, electric, diesel. Experiment with it, to understand decorating containers.

Functions

NameDescription
decorated()
drive(car, name)
main(args)

Interfaces

NameDescription
Engine Interface for engines.

Classes

NameDescription
Car A class representing a car.
DieselEngine A concrete implementation of Engine that uses diesel for propelling.
ElectricEngine A concrete implementation of Engine that uses electricity for propelling.
GasolineEngine A concrete implementation of Engine that uses gasoline for propelling.
Tire Tire, what it can represent else?

Structs

NameDescription
Color A struct that should be managed by container.
Size Size of a car.