Dependency injection
A little modified quote from Wikipedia, Dependency injection article (This quote is licensed under CC BY-SA 3.0):
In software engineering, dependency injection is a technique whereby one object (or static method) supplies the dependencies of another object. A dependency is an object that can be used (a service). An injection is the passing of a dependency to a dependent object (a client) that would use it. The service is made part of the client’s state. Passing the service to the client, rather than allowing a client to build or find the service, is the fundamental requirement of the pattern.
The intent behind dependency injection is to achieve Separation of Concerns of construction and use of objects.
Dependency injection is one form of the broader technique of inversion of control. As with other forms of inversion of control, dependency injection supports the dependency inversion principle. The client delegates the responsibility of providing its dependencies to external code (the injector). The client is not allowed to call the injector code; it is the injecting code that constructs the services and calls the client to inject them. This means the client code does not need to know about the injecting code, how to construct the services or even which actual services it is using; the client only needs to know about the intrinsic interfaces of the services because these define how the client may use the services. This separates the responsibilities of use and construction.
The Dependency Injection design pattern solves problems like:
How can an application or class be independent of how its objects are created?
How can the way objects are created be specified in separate configuration files?
How can an application support different configurations?
Creating objects directly within the class that requires the objects is inflexible because it commits the class to particular objects and makes it impossible to change the instantiation later independently from (without having to change) the class. It stops the class from being reusable if other objects are required, and it makes the class hard to test because real objects can’t be replaced with mock objects.
Dependency injection separates the creation of a client’s dependencies from the client’s behavior, which allows program designs to be loosely coupled and to follow the dependency inversion and single responsibility principles. It directly contrasts with the service locator pattern, which allows clients to know about the system they use to find dependencies.
An injection, the basic unit of dependency injection, is not a new or a custom mechanism. It works in the same way that “parameter passing” works. Referring to “parameter passing” as an injection carries the added implication that it’s being done to isolate the client from details.
An injection is also about what is in control of the passing (never the client) and is independent of how the passing is accomplished, whether by passing a reference or a value.
Dependency injection involves four roles:
-
- the service object(s) to be used
- the client object that is depending on the service(s) it uses
- the interfaces that define how the client may use the services
- the injector, which is responsible for constructing the services and injecting them into the client
Any object that may be used can be considered a service. Any object that uses other objects can be considered a client. The names have nothing to do with what the objects are for and everything to do with the role the objects play in any one injection.
The interfaces are the types the client expects its dependencies to be. An issue is what they make accessible. They may truly be interface types implemented by the services but also may be abstract classes or even the concrete services themselves, though this last would violate DIP and sacrifice the dynamic decoupling that enables testing. It’s only required that the client does not know which they are and therefore never treats them as concrete, say by constructing or extending them.
The client should have no concrete knowledge of the specific implementation of its dependencies. It should only know the interface’s name and API. As a result, the client won’t need to change even if what is behind the interface changes. However, if the interface is refactored from being a class to an interface type (or vice versa) the client will need to be recompiled. This is significant if the client and services are published separately. This unfortunate coupling is one that dependency injection cannot resolve.
The injector introduces the services into the client. Often, it also constructs the client. An injector may connect together a very complex object graph by treating an object like a client and later as a service for another client. The injector may actually be many objects working together but may not be the client. The injector may be referred to by other names such as: assembler, provider, container, factory, builder, spring, construction code, or main.
Taxonomy
Inversion of control (IoC) is more general than DI. Put simply, IoC means letting other code call you rather than insisting on doing the calling. An example of IoC without DI is the template method pattern. Here, polymorphism is achieved through subclassing, that is, inheritance.
Dependency injection implements IoC through composition so is often identical to that of the strategy pattern, but while the strategy pattern is intended for dependencies to be interchangeable throughout an object’s lifetime, in dependency injection it may be that only a single instance of a dependency is used. This still achieves polymorphism, but through delegation and composition.
Advantages
-
- Dependency injection allows a client the flexibility of being configurable. Only the client’s behavior is fixed. The client may act on anything that supports the intrinsic interface the client expects.
- Dependency injection can be used to externalize a system’s configuration details into configuration files, allowing the system to be reconfigured without recompilation. Separate configurations can be written for different situations that require different implementations of components. This includes, but is not limited to, testing.
- Because dependency injection doesn’t require any change in code behavior it can be applied to legacy code as a refactoring. The result is clients that are more independent and that are easier to unit test in isolation using stubs or mock objects that simulate other objects not under test. This ease of testing is often the first benefit noticed when using dependency injection.
- Dependency injection allows a client to remove all knowledge of a concrete implementation that it needs to use. This helps isolate the client from the impact of design changes and defects. It promotes reusability, testability and maintainability.
- Reduction of boilerplate code in the application objects, since all work to initialize or set up dependencies is handled by a provider component.
- Dependency injection allows concurrent or independent development. Two developers can independently develop classes that use each other, while only needing to know the interface the classes will communicate through. Plugins are often developed by third party shops that never even talk to the developers who created the product that uses the plugins.
- Dependency Injection decreases coupling between a class and its dependency.
Disadvantages
-
- Dependency injection creates clients that demand configuration details be supplied by construction code. This can be onerous when obvious defaults are available.
- Dependency injection can make code difficult to trace (read) because it separates behavior from construction. This means developers must refer to more files to follow how a system performs.
- Dependency injection frameworks are implemented with reflection or dynamic programming. This can hinder use of IDE automation, such as “find references”, “show call hierarchy” and safe refactorings.
- Dependency injection typically requires more upfront development effort since one can not summon into being something right when and where it is needed but must ask that it be injected and then ensure that it has been injected.
- Dependency injection forces complexity to move out of classes and into the linkages between classes which might not always be desirable or easily managed.
- Dependency injection can encourage dependence on a dependency injection framework.
Types of dependency injection
There are at least three ways a client object can receive a reference to an external module:
-
- constructor injection: the dependencies are provided through a client’s class constructor.
- setter injection: the client exposes a setter method that the injector uses to inject the dependency.
- interface injection: the dependency’s interface provides an injector method that will inject the dependency into any client passed to it. Clients must implement an interface that exposes a setter method that accepts the dependency.
Pure dependency injection
Note a quote from the above: “It directly contrasts with the service locator pattern, which allows clients to know about the system they use to find dependencies.” Despite Wikipedia says so, the words “dependency injection” are usually (unfortunately) used to denote namely service locator pattern. What Wikipedia calls “dependency injection” is sometimes called pure dependency injection. It is also sometimes called poor dependency injection, but the word “poor” should not confuse you into thinking that pure dependency injection is bad or worse that service locator pattern. In fact the word poor means only that the concept of pure dependency injection is simpler than the concept of service locator pattern and requires less advanced base libraries.
I will not consider here what is service locator pattern because it is outside of the topic of this article and also not a very good way of doing things, because it creates “illogical” couplings in code.
Python Dependency Injector
My D library API is made similar to Python Dependency Injector library, which inspired it.
However, I do not implement overriding “overriding of providers” feature (and thus also abstract providers) from Dependency Injector, because:
- this feature seems a code smell, it is a nonlocal dependency in user programs and nonlocal dependencies are considered bad;
- to improve performance a little;
- it anyway can be added in a future version of my software;
- to reduce the amount of coding.
I decided not to implement any container classes (in the sense the word “container” used in Python Dependency Injector docs and other dependency injection texts) in this library as one can use regular D structs for this purpose.
My D dependency injector
Install the pure-dependency-injector
package with DUB (see the DUB documentation; I strongly recommend using DUB to build D projects) or clone my GitHub repository.
It implements features similar to Python Dependency Injector except:
- no “containers” (in the sense of “dependency injector containers” not “collection containers”) because one can use just D structs (or classes) as containers (In fact I don’t quite understand why Python Dependency Injector defines container classes, as one can use regular Python classes for this);
- no “overriding” and “delegating” with other dependency injectors to increase efficiency and for simplicity (thus also no
Abstract*
classes); - no configuration support;
- also no dependencies support (at least in the current version);
- also no injections (honestly saying, I don’t know what it is, Python package docs are unclear on this);
- also no coroutines support in the current version.
Object factories are not implemented as it is easier to do it using callables and lambda functions in D.
Also thanks to forum.dlang.org users, especially Simen Kjærås, for explaining my errors while writing this software.
Providers
The only module in my library is pure_dependency.providers
.
Classes
Provider
and ReferenceProvider
These is the most basic classes of my library.
They are abstract base classes for providers.
Sadly D language specification (as of v2.085.1) seems not to provide a mean to join passing/returning both value objects and references into one template. Even C++ is better in this regard. D developers, please do something to improve this.
As you can guess, Provider
is the base provider class for value objects and ReferenceProvider
is the base provider class for reference objects.
class Provider(Result_, Params_...) { alias Result = Result_; /// the type of the object to be provided. alias Params = Params_; /// the type of provider parameters. /// Call the provider. final const(Result) opCall(Params params) { return delegate_(params); } /// Call it with a structure or class as the argument (expanding its members into arguments /// in order). final const(Result) call(S)(S s) const { return callMemberFunctionWithParamsStruct!(this, "opCall", S)(s); } /// The abstract virtual function used to create the provided object. abstract const(Result) delegate_(Params params); alias DelegateType = const(Result) delegate (Params params); /// Returns `delegate_` as a delegate. final @property DelegateType provider() { return &delegate_; } }
So Provider
is a class template with type arguments Result_
and Params_
which specify correspondingly the provided object type and provider’s parameter types list.
The most important member function is opCall()
which returns the provided object. It can be called like object = provider.opCall(3, 7.2, otherObject)
where provider
is the provider object. opCall
is a special method name in D, so it can be called instead with this syntax: object = provider(3, 7.2, otherObject)
(that is object provider
is a callable object).
The actual creation (or retrieval) of the provided object is accomplished by the delegate_()
method. (It is called this way because delegate
is a keyword in D.) This method has the same signature as our opCall
method, that is receives some parameters and returns the object.
The property provider
returns a reference (“delegate” in D terms, don’t confuse with delegate providers in the Python library) to the delegate_
method (note that the delegate also contains a reference to the provider object). Not sure how to use it, but I provide it for completeness. Example: provider.provider
.
The call
method template can be used to call the provider with a struct (or class) as its only argument. The members of the struct are “expanded” in order of their declaration and passed to opCall
method. This uses my struct-params
library. Example:
struct Params { int x; float y; } MyProvider provider; Params theParams = {x: 3, y: 7.4}; MyObject obj = provider.call(theParams); // suppose MyProvider has (int, float) Params argument.
ReferenceProvider
is the same as Provider
except it uses a reference type (ref
keyword) for methods returning the provided object.
Callable
and ReferenceCallable
Callable
is probably the most often used kind of providers. It simply calls an user-specified function to obtain the provided object. This is useful mainly for non-singleton objects (or singleton objects provided by some other library or an OS).
class Callable(alias Function) : Provider!(ReturnType!Function, Parameters!Function) { override const(ReturnType!Function) delegate_(Params params) { return Function(params); } }
So, it is a class template receiving a function as its template argument. It is inherited from Provider
with the function return type as the provider type and function parameters as the provider arguments.
Note it uses the templates ReturnType
and Parameters
from the module std.traits
of D standard library.
Example:
struct Complex { float real, imaginary; } Complex polarForm(float radius, float angle) { Complex result = {real: radius * cos(angle), imaginary: radius * sin(angle)}; return result; } PolarFormProvider provider = new Callable!polarForm; // not necessary to use a provider in this simple example, but that’s just an example Complex ourComplex = provider(3.2, 1.24);
ReferenceCallable
is similar except that it returns the provided object by reference.
Singletons
Singletons are objects which are created (in our case by our singleton provider) only once. If the provider is called again, the same object is returned. It is useful in such situations as to read and parse a configuration file only once, to create no more than one dialog window per document window, to do some complex computations only once, etc.
BaseGeneralSingleton
and ReferenceBaseGeneralSingleton
class BaseGeneralSingleton(Base) : Provider!(Base.Result, Base.Params) { private Base _base; /// Create the singleton with given base provider. this(Base base) { _base = base; } /// Get the base provider. @property Base base() { return _base; } }
BaseGeneralSingleton
is a class template with an argument Base
which should be another provider class.
The abstract delegate_()
method is supposed to create an object by calling Base
provider (but do it only once).
The constructor of BaseGeneralSingleton
receives a provider of class Base
. This provider is returned by base
property of our singleton provider like singletonProvider.base
.
ReferenceBaseGeneralSingleton
is analogous.
In this version of the library all singleton provider classes are inherited from BaseGeneralSingleton
or ReferenceBaseGeneralSingleton
, but this is not the most efficient implementation and may change in a future version. A patch is welcome.
Three kinds of singletons
There are three kinds of singletons in my library:
- thread-unsafe singletons (You must not call one such a provider simultaneously in more than one thread of a multi-threaded program to avoid hard to debug bugs, but if you use it properly it is probably the most efficient of the three kinds);
- thread-safe singletons (You can call it in multi-threaded programs);
- thread-local singletons (thread safe and also creates/obtains a single provided object per thread a provider is called in, instead of single object per provider, also efficient).
Below I will describe only thread-safe singletons, as API of the two other kinds is quite analogous.
ThreadSafeSingleton
and ReferenceThreadSafeSingleton
class ThreadSafeSingleton(Base) : BaseGeneralSingleton!Base { this(Base base) { super(base); } override const(Result) delegate_(Params params) { return synchroizedMemoizeMember!(const(Base), "delegate_")(base, params); } }
This class just overrides the abstract delegate_()
method with an appropriate implementation for a thread-safe singleton. The implementation uses my memoize library.
ThreadSafeCallableSingleton
and ReferenceThreadSafeCallableSingleton
class ThreadSafeCallableSingleton(alias Function) : ThreadSafeSingleton!(Callable!Function) { this() { super(new Callable!Function); } }
It is a thread-safe singleton provider with the same API as Callable
provider (but singleton).
FixedObject
and ReferenceFixedObject
These provider classes return some already existing object (or reference to an object).
class FixedObject(alias obj) : Provider!(typeof(obj)) {
override const(Result) delegate_() const {
return obj;
}
}
Example:
float obj = 3.2;
auto provider = new FixedObject!obj;
auto referenceProvider = new ReferenceFixedObject!obj;
assert(provider() == obj);
assert(&referenceProvider() == &obj);
ProviderWithDefaults
It is recommended to use pure-dependency-injector
together with struct-params
library which allows to specify just a subset of (for example provider’s) parameters and use default values for the rest.
Example:
import struct_params;
mixin StructParams!("S", int, "x", float, "y"); // declare parameters x of type int and y of type float
float calc(int x, float y) { // some example float object creator
return x * y;
}
immutable S.Regular myDefaults = { x: 3, y: 2.1 }; // default values
alias MyProvider = ProviderWithDefaults!(Callable!calc, S, myDefaults); // modify Callable!calc to have default values
immutable S.WithDefaults providerParams = { x: 2 }; // set value for x
auto provider = new MyProvider;
assert(provider.callWithDefaults(providerParams) - 4.2 < 1e-6); // check that 2*2.1 == 4.2 (2 is an explicit parameter x, 2.1 is y default)
Note that because of a deficiency of the current version of D, it uses struct declarations to define default and particular arguments, not “inline” literals. See about struct-params
for more details.
Why I can’t call new
from a template
In this blog post (TODO: link) it is explained why we can’t call operator new
from a template (provider template class in our case) to create an object of a specified type.
Use this workaround to create a provider which constructs objects with new
:
class C { int v; this(int a, int b) { v = a + b; } }
auto cFactory = new Callable!((int a, int b) => new C(a, b));
assert(cFactory(1, 2).v == 3);
Here we use the above described Callable
provider together with lambda function (int a, int b) => new C(a, b)
which constructs objects of class C
using operator new
.