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:
- instantiation pre event - fired before instantiation of container
- instantiation post event - fired after instantiation of container
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:
- read profile argument from command line or environment
- 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
Name | Description |
---|---|
decorated()
|
|
drive(car, name)
|
|
main(args)
|
Interfaces
Name | Description |
---|---|
Engine
|
Interface for engines. |
Classes
Name | Description |
---|---|
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
Name | Description |
---|---|
Color
|
A struct that should be managed by container. |
Size
|
Size of a car. |