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.

At certian point of time the manufacturer decided that their car factory should provide cars with different engines, electric/diesel/gasoline based on user preference. Therefore the application itself needed to be reconfigured to satisfy user needs.

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

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
  • describing - allows to register description for components that can be used later for help information

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.describing("Car factory", "Car factory, please see available contents of our factory");

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

The profile based container is assembled from 3 typed and switchable containers joined together into a subscribable composite container with gc component registration, construction of deffered components and describing capabilities. 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-gc-managed components. Once instantiation is finished, car is fetched from container and displayed in console.

Each of decorating container shown in example above will be explained in detail below.

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,
        {
            cont.locate!Switchable(cont.locate!string("profile")).enabled = true;
        }
    );

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.

Describing:

Describing container, adds ability to store description for the container itself, and components that are managed in decorated container. Main purpose of this type of container is to be used in storing description that will be used for help information (usually displayed in command line).

auto cont = aggregate(
        singleton, "singleton",
        prototype, "prototype",
        values, "parameters",
        gasoline, "gasoline",
        electric, "electric",
        diesel, "diesel"
    ).aliasing.gcRegistered.deffered.describing("Car factory", "Car factory, please see available contents of our factory");

Registering a description can happen in two ways:

  1. By using .describe that adds a description on registered component
  • adding description directly to identity describer
  • )

    Both ways are shown in example below:

    with (cont.configureValueContainer("parameters")) {
    
        register(verbose, "verbose").describe("verbose errors", "whether to show or not appearing errors.");
    
        if (profile !is null) {
            register(profile, "profile");
        }
    
        with (cont.locate!(IdentityDescriber!())) {
            register("profile", "Car enginge type", "Type of engine to select while building a car (gasoline|diesel|electric|ecological).");
        }
    }

    The first line is using first option for registering descriptions, where .describe method will query locator for IdentityDescriber!() component which hosts those descriptions and then will register the description in it if found. If not a NotFoundException will be thrown.

    Notice that profile is wrapped in a if conditional. This is done intenionally in the example to simulate a missing profile setting when none is passed through command line, however the same situation could happen with switchable container where even if component is registered it could not be available at run time.

    Last with statement is the second variant of configuration, where instead of using .describe entire registration of description is done manually. It is better to use first method for registering descriptions, while the latter only in cases when first is not possible, just like in the example where missing property is simulated. This will work with switchable container since we do register those components, yet they won't be available at runtime.

    Second registration version used IdentityDescriber!() component, which itself is a component that describing container delegates the task of describing components. Besides IdentityDescriber!() used as main describer, container has also a describer for itself or decorated container, and a fallback describer that is used in case main one can't describe a component. All three of them are of Describer!() interface, and it is possible to pass them in describing container as arguments. By default following implementations are available:

    1. IdentityDescriber!() - a describer that describes data based upon identity of component
    2. TypeDescriber!() - a describer that uses a template and formatter for identity and component itself to generate a description
    3. StaticDescriber!() - a describer that will provide same description no matter what is passed
    4. NullDescriber!() - a describer that does not describe anyhting

    Rendering of stored description can happen like in example below:

    if (help.helpWanted) {
        defaultGetoptPrinter(
            cont.locate!(Describer!()).describe(null, cont).description,
            cont.locate!(DescriptionsProvider!string)
                .provide
                .map!(description => Option(null, description.identity, text(description.title, " - ", description.description)))
                .array
        );
        return;
    }
    
    try {
    
        cont.instantiate();
    
        cont.locate!Car.drive(profile);
    } catch (AediException e) {
        foreach (throwable; e.exceptions.filterByInterface!NotFoundException) {
            defaultGetoptPrinter(
                text("Missing \"", e.identity, "\" for proper functioning, please see detailed info of missing piece below. For more detailed options run with --help"),
                cont.locate!(Describer!()).describe(e.identity, null)
                    .only
                    .filter!(description => !description.isNull)
                    .map!(description => Option(null, description.identity, text(description.title, " - ", description.description), true))
                    .array
            );
    
            break;
        }
    
        if (cont.locate!bool("verbose")) {
            throw e;
        }
    }

    In case if help is required, or in other words all descriptions registered at certain point, a DescriptionsProvider!string should be located fetched from container and asked to provide all available descriptions. Coincidentally IdentityDescriber!() implements also DescriptionsProvider!string and therefore it will be fetched from container once asked by provider interface it implements.

    Since describing container is mostly geared towards being queried for description based on identity of a component and component itself, it is also possible to provide targeted info per problematic aspect encountered during application running, such as missing component in container, profile for example. Try - catch statement in example uses this functionality to print message about missing component, and its description.

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

    Members

    Classes

    Car
    class Car

    A class representing a car.

    DieselEngine
    class DieselEngine

    A concrete implementation of Engine that uses diesel for propelling.

    ElectricEngine
    class ElectricEngine

    A concrete implementation of Engine that uses electricity for propelling.

    GasolineEngine
    class GasolineEngine

    A concrete implementation of Engine that uses gasoline for propelling.

    Tire
    class Tire

    Tire, what it can represent else?

    Functions

    decorated
    auto decorated()
    Undocumented in source. Be warned that the author may not have intended to support it.
    drive
    void drive(Car car, string name)
    Undocumented in source. Be warned that the author may not have intended to support it.
    main
    void main(string[] args)
    Undocumented in source. Be warned that the author may not have intended to support it.

    Interfaces

    Engine
    interface Engine

    Interface for engines.

    Structs

    Color
    struct Color

    A struct that should be managed by container.

    Size
    struct Size

    Size of a car.

    Meta

    License

    Boost Software License - Version 1.0 - August 17th, 2003

    Permission is hereby granted, free of charge, to any person or organization obtaining a copy of the software and accompanying documentation covered by this license (the "Software") to use, reproduce, display, distribute, execute, and transmit the Software, and to prepare derivative works of the Software, and to permit third-parties to whom the Software is furnished to do so, all subject to the following:

    The copyright notices in the Software and this entire statement, including the above license grant, this restriction and the following disclaimer, must be included in all copies of the Software, in whole or in part, and all derivative works of the Software, unless such copies or derivative works are solely in the form of machine-executable object code generated by a source language processor.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

    Authors

    aermicioi