Module extending_generic_factory
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.
The interface presented in previous tutorial is a minimalistic interface, and does not provide any ability to customize the component that is created. It is practically a black box, that is convenient to use by containers, and not by client code that needs to configure components registered in container.
To support better configuration, without loosing black boxiness of presented Factory
interface,
the process of creating and configuring a component was split into three distinct steps below:
- Allocation and construction of component
- Configuration of component
- Destruct and deallocation of component
The following steps are formalized into a set of interfaces shown below, where each step itself is encapsulated into objects implementing presented interfaces.
interface PropertyConfigurer(T) : LocatorAware!() {
public {
void configure(ref T object);
}
}
interface InstanceFactory(T) : LocatorAware!(), AllocatorAware!() {
public {
T factory();
}
}
interface InstanceDestructor(T) : LocatorAware!(), AllocatorAware!() {
public {
void destruct(ref T destructable);
}
}
interface GenericFactory(T) : Factory!T, InstanceFactoryAware!T, PropertyConfigurersAware!T {
public {
@property {
alias locator = Factory!T .locator;
Locator!() locator();
}
}
}
interface InstanceFactoryAware(T) {
public {
@property {
InstanceFactoryAware!T setInstanceFactory(InstanceFactory!T factory);
}
}
}
interface InstanceDestructorAware(T) {
public {
@property {
InstanceDestructorAware!T setInstanceDestructor(InstanceDestructor!T destructor);
}
}
}
interface PropertyConfigurersAware(T) {
public {
PropertyConfigurersAware!T addPropertyConfigurer(PropertyConfigurer!T configurer);
}
}
The methods used to configure components in examples up to this point are actually wrappers
over building blocks that implement interfaces presented above. register
method is a
wrapper over an implementation of GenericFactory
interface.
When instantiation or configuration of a component is not possible to implement in terms of existing tools and interfaces provided by the framework, it is recommended to implement one of building block interface, depending on what stage the problem resides. For car simulation app, with- out reason the company decided that tires, should be instantiated using their own implementation of instance building block. Same with inflating a tire they've decided to use their own implementation, along with tire recycling, making a closed production cycle.
Next example shows a custom implementation of tire manufacturing process. By implementing
InstanceFactory
interface, the building block can be used along the rest of components already
present in framework, such as GenericFactory
. The only hinder is that what remains is the ugliness, and
verbosity, due to need to manually instantiate the building block, and pass to generic factory. Instead
of manually doing it, wrapping instantiator into a function will lessen the verbosity (check makeTire
definition and usage), and increase the readability of configuration code, thanks to UFCS feature of
D programming language.
auto makeTire(T : GenericFactory!Z, Z : Tire)(auto ref T factory, int size) {
factory .setInstanceFactory(new TireConstructorInstanceFactory(size));
return factory;
}
class TireConstructorInstanceFactory : InstanceFactory!Tire {
private {
int size;
Locator!(Object, string) locator_;
IAllocator allocator_;
}
public {
this(int size) {
this .size = size;
}
@property {
TireConstructorInstanceFactory locator(Locator!(Object, string) locator) {
this .locator_ = locator;
return this;
}
TireConstructorInstanceFactory allocator(IAllocator allocator) pure nothrow @safe {
this .allocator_ = allocator;
return this;
}
}
Tire factory() {
Tire tire;
import std .stdio;
write("Creating a tire of ", size, " inches: ");
tire = this .allocator_ .make!Tire;
tire .size = size;
writeln("\t[..OK..]");
return tire;
}
}
}
The code for tire inflator configurer can be seen in examples from source code. The difference
from TireConstructorInstanceFactory
is that implementation should extend PropertyConfigurer
.
Same can be said about tire destructor, it implements InstanceDestructor
to provide
destruction mechanisms for generic factory.
Once the custom implementation and their wrapper are implemented, it is possible to use them as any other built-in building blocks from framework. Next example shows how implemented building blocks can be used along with other configuration primitives.
void main() {
SingletonContainer container = new SingletonContainer;
scope (exit) container .terminate();
with (container .configure) {
register!Tire("logging.tire")
.makeTire(17)
.inflateTire(3.0)
.set!"vendor"("hell tire")
.destroyTire;
}
container .instantiate();
container .locate!Tire("logging.tire") .writeln;
}
Running it, produces following result:
Creating a tire of 17 inches: [..OK..]
Inflating tire to 3 atm: [..OK..]
Tire(17 inch, 3 atm, hell tire)
Destroying tire: [..OK..]
Try to implement your own instance factory or property configurer, wrap them
in a function, and use in component configuration. Try to do this for Car
component from previous examples.
Functions
Name | Description |
---|---|
destroyTire(factory)
|
|
inflateTire(factory, pressure)
|
|
main()
|
|
makeTire(factory, size)
|
Classes
Name | Description |
---|---|
Tire
|
|
TireConstructorInstanceFactory
|
|
TireInflatorConfigurer
|
|
TireInstanceDestructor
|