Butterfly DI Container - Internal Design
Jakob Jenkov |
NOTE: This text is part 3 in a series on the "Design of a DI Container". To be notified of new texts in this (and other) tutorial trails, Subscribe to the RSS feed.
The internal design of Butterfly DI Container runtime is centered around two design patterns:
- Factory Pattern
- Pipes and Filters Pattern
When combined together these two patterns make up a new kind of pattern which I call "Chained Factories".
NOTE: As you read through this design you may think that all the factories created and linked internally must lead to a really slow dependency injection container. It doesn't ! Butterfly Container has been measured against Google Guice, and beats it with a large margin. Guice again claims to beat Spring by a large margin, so Butterfly Container is definately among the fastest Java DI containers around! Not bad, considering this design also yields one of the most flexible Java DI containers around.
Factory Pattern
A DI container is capable of producing object instances of whatever types (classes) you configure the container to. In Butterfly Container the concrete object is returned by a factory kept internally in the container. Some factories, like singleton factories, may cache object instances rather than instantiate a new object for each call to the factory.
Here is how the call sequence looks:
Pipes and Filters Pattern
A pipe is something you can either read from, or write to, just like IO streams. A pipe can be wrapped in a filter. The filter itself also looks like a pipe, meaning it too can be wrapped in a filter. This way you can create a chain of pipes and filters, each modifying the data passed down through the chain.
Here is a simple call sequence example:
Chained Factories
One of the main features of a DI container is that the output of one factory can be injected into the output of other factories. In other words, if a factory produces instances of A, you can inject instances of B into these A instances. In Butterfly Container this is done by chaining the factories. Let us look at a simple example configuration:
myBean = * com.jenkov.MyBean().setValue("someValue");
When the container is asked for an instance from the myBean
factory,
first a new MyBean
instance is created, then the setValue()
method is called on that instance.
Internally this configuration is divided into two factories (actually more,
but I will get back to that later). One factory (f1) that creates the MyBean
instance, and a another factory (f2) that calls setValue(...)
on the
MyBean
instance returned from f1. Here
is how it looks as a call sequence :
In reality the sequence of the chained factory 1 and factory 2
is reversed. The container only knows about
factory 2 (the last factory in the chain). Only factory 2 knows about factory 1.
Factory 2 knows it needs to obtain an instance from factory 1, and call setValue()
on that instance. Here is how it looks:
The f2
factory returns whatever the setValue()
method
returns.
If the method returns void
, the factory returns the object
the setMethod()
was invoked on. This makes it possible to chain
method calls on methods returning void. Like this configuration illustrates:
bean = * com.jenkov.AnObject().setValue1("value1") .setValue2("value2") .setValue3("value3");
Internally the local factory chain looks like this:
The factories f1, f2
and f3
all return the AnObject
instance they
called their methods on, since their setter-methods
return void. This makes it possible for the next
factory in the chain to call methods on the same object.
It is also possible to call one named factory from within another, like this:
parent = * com.jenkov.Parent(); child = * com.jenkov.Child(parent);
In the above example the factory child
references
the factory parent
. If you request a child
instance from the container (container.instance("child")
)
the call sequence will look like this:
You can obtain objects directly from both the parent
and child
factory, but notice how the child
factory obtains an object from the parent
factory, injects it into a com.jenkov.Child
object, and returns the Child
object. The factories parent
and child
are chained!
The factory chains can be quite large, and can be thought of as a large factory graph - a graph of factories calling each other, the output of one factory being the input of another factory, each factory either refining or wrapping the product obtained from other factories.
There is a lot more going on internally than you have seen here. Each value, e.g. the value "123Text" is also encapsualted in a factory, and there are different types of factories as well. All this will be covered in more detail in the next text in the series. See the link below.
Tweet | |
Jakob Jenkov |