oceandrift.di

Members

Aliases

DIContainer
alias DIContainer = Container

Dependency Container

Classes

DI
class DI

Dependency Injection

Enums

_diConstructorUDA
enum _diConstructorUDA
Undocumented in source.
dependency
enum dependency

UDA to mark fields as dependency for Field Assignment Constructor Application.

Mixin templates

DIConstructor
mixintemplate DIConstructor()

Generates a constructor with a parameter for each @dependency field and assigns the passed value to the corresponding field.

Templates

determineWhyNotConstructableByDI
template determineWhyNotConstructableByDI(T)

Determines why a type T is not constructable by the DI framework.

diConstructorString
template diConstructorString(T)

Generates code for a constructor with a parameter for each @dependency field and assigns the passed value to the corresponding field.

Variables

isConstructableByDI
enum bool isConstructableByDI(T);

Determines whether a type T is constructable by the DI framework.

isForbiddenType
enum bool isForbiddenType(T);

Determines whether a type T is denied for user registration.

isSupportedDependencyType
enum bool isSupportedDependencyType(T);

Determines whether a type T is supported to be used as dependency by the framework.

isntForbiddenType
enum bool isntForbiddenType(T);

Determines whether a type T is applicable for user registration.

Detailed Description

About Dependency Injection

How does it work?

Dependency instances only need to be created once and can be used for all other dependent objects. As there is only one object of each type, these instances can be called “singletons”.

They can be stored in one big repository, the dependency container, and retrieved later as needed.

Declaration of dependencies

One of the most useful ways to specify the dependencies of a type (as in class or struct) is to declare a constructor with them as parameters.

// Example: `LoginService` depends on `PasswordHashUtil`, `DatabaseClient` and `Logger`.
class LoginService {
	public this(
		PasswordHashUtil passwordHashUtil,
		DatabaseClient databaseClient,
		Logger logger,
	) {
		// …
	}
}

Retrieval of dependencies

Getting the dependencies from the container into the service object is where the DI framework comes in. While the user could manually retrieve them from the container and pass them to the constructor (i.e. new LoginService( container.get!PasswordHashUtil(), container.get!Logger(), container.get!DatabaseClient() ), this would get tedious quickly. The framework comes to the resuce.

auto di = new DI();
// …

LoginService service = di.resolve!LoginService();

Registration of dependencies

But where did the PasswordHashUtil, the Logger and the DatabaseClient come from? How did they get into the DI container?

Standalone dependencies

Let’s assume the PasswordHashUtil has zero dependencies itself – it is a self-contained service class. Its constructor has either no parameters or isn’t declared explicitly (and it uses the implicit default constructor).

The DI framework can easily construct a singleton instance (of the dependency type) on the fly. This happens automatically the first time one is needed.

class PasswordHashUtil {
	public string hash(string password) { /* … */ }
	public bool verify(string hash, string userPassword) { /* … */ }
}
Custom dependency registrations

In case a user-provided PasswordHashUtil instance has been registered in advance, the framework will skip the construction and use that one instead.

Transitive dependencies

Aka dependencies of a dependency.

The 2nd dependency of the aforementioned LoginService is Logger. For illustration purposes, we’ll assume the Logger class depends on Formatter, a type that implements formatting facilities. Logger is dependency-less class (like PasswordHashUtil was).

The DI framework will retrieve a Formatter before it can construct the Logger. Since Formatter has not dependencies, the framework can construct it as mentioned in the previous chapter. If it there is one in the dependency container already, it will be used instead (and the construction will be skipped).

The next step is to construct a Logger which receives the Formatter through its constructor.

class Logger {
	public this(Formatter formatter) {
		// …
	}
	public void log(string message) { /* … */ }
}
Custom dependency registrations

In case a user-provided Logger instance has been registered in advance, the framework will skip all construction steps and use that one instead.

If the user-provided Logger uses a different Formatter than the one in the dependency container, the Logger of the resulting LoginService will use said different Formatter, not the one from the container.

This has no impact on a direct dependencies of Logger. If Logger itself depended on Formatter, it would still receive the one from the dependency container. Which in turn would lead to LoginService.formatter being different to Logger.formatter.

Logically, this distinction would be no longer relevant, if the user registered their Formatter instance with the framework in addition to their custom Logger one.

Complex dependencies

Unlike the dependencies shown in the previous two chapters, however, the DatabaseClient doesn’t depend on a bunch of other services. Instead it is constructed from four strings (socket, username, password, database name).

While the injection of a DatabaseClient into dependent services principally works like before, the framework cannot instantiate a new one by itself. Hence it is that an instance has to be registered with the framework in advance. A user-created DatabaseClient can provided by passing it to register().

The framework will pick up on it later, when it constructs LoginService or, failing that, if no custom instance has been registered beforehand, crash the program as soon as it falls back to instantiating one on its own.

class DatabaseClient {
	public this(
		string socket,
		string username,
		string password,
		string databaseName,
	) {
		// …
	}
	public Statement prepare(string sql) { /* … */ }
	public void disconnect() { /* … */ }
}

// Construct a DatabaseClient and register it with the DI framework.
auto databaseClient = new DatabaseClient(cfg.socket, cfg,username, cfg.password, cfg.dbName);
di.register(databaseClient);

Unconventional use

This chapter deals with solutions to overcome the conventions of the framework. (Malicious gossip has it that these are “design limitations”. Don’t listen to them…)

Injecting non-singleton instances

Non-singleton objects are sometimes also called “transient” or “prototype”.

By default, the framework will instantiate a singleton instance for each dependency type and use it for all dependent objects.

When this behavior is not desired, it’s recommended to instantiate a new object in the constructor. Check out makeNew!(T).

The DI framework can inject a reference to itself.
public this(
	DI di,
) {
	this.dependency = di.makeNew!Dependency();
}

Alternatively, one could also create a factory type and use that as a dependency instead.

Injecting dependencies that are primitive/unsupported types

The recommended way to inject primitives types (like integers, booleans, floating-point numbers, or enums) or other unsupported types (e.g. strings, other arrays, associative arrays, class pointers or pointers in general) is to wrap them in a struct.

For pointers, deferencing them might also be an option.

Examples

Bootstrapping

Bootstrapping the framework is as simple as:

auto di = new DI();
class Dependency {
}

class Foo {
	private Dependency d;

	public this(Dependency d) {
		this.d = d;
	}
}

// Bootstrap the DI framework.
auto di = new DI();

// Then let it resolve the whole dependency tree
// and construct dependencies as needed.
Foo foo = di.resolve!Foo();

// The DI framework has constructed a new instance of `Dependency`
// and supplied it to the constructor of `Foo`.
assert(foo.d !is null);

User-supplied dependency instances

How about types the framework cannot construct on its own?

… or instaces that have been constructed in before and could be reused?

class Dependency {
	private int number;

	public this(int number) {
		this.number = number;
	}
}

class Foo {
	private Dependency d;

	public this(Dependency d) {
		this.d = d;
	}
}


// Construct an instance of the dependency manually.
// Then register it with the framework.
auto dep = new Dependency(128);
di.register(dep);

// alternative syntax variants (They all come down to the same thing.):
di.register!Dependency(dep);
di.register!Dependency = dep;

// Resolve dependencies of `Foo` and create instance.
Foo foo = di.resolve!Foo();

// The DI framework constructed a new instance of `Dependency`
// and supplied it to the constructor of `Foo`.
assert(foo.d !is null);

Injecting dependencies that are interfaces

It’s really straightforward: Tell the framework which implementation type to use for dependencies of an interface type.

interface Logger {
	void log(string message);
}

class StdLogger : Logger {
	private int lines = 0;

	void log(string message) {
		++lines;
		// …writeln(message);…
	}
}

class Service {
	private Logger logger;

	this(Logger logger) {
		this.logger = logger;
	}

	void doSomething() {
		this.logger.log("Did something.");
	}

}


// Register `StdLogger` as the implementation to construct for the
// `Logger` interface.
di.registerInterface!(Logger, StdLogger);

// Now the framework is set up to
// construct an instance of the `Service` class.
Service service1 = di.resolve!Service();

// Our service instance is using the `StdLogger` implementation
// that has been registered a few lines above.
service1.doSomething();
service1.doSomething();
assert(di.resolve!StdLogger().lines == 2);

Injecting dependencies that are interfaces (Part II)

What if we had a type with a complex constructor, one that the framework cannot instantiate on its own?

interface Logger {
	void log(string message);
}

class FileLogger : Logger {
	private int lines = 0;

	this(string logFilePath) {
		// …
	}

	void log(string message) {
		++lines;
		// …
	}
}

class Service {
	private Logger logger;

	this(Logger logger) {
		this.logger = logger;
	}

	void doSomething() {
		this.logger.log("Did something.");
	}

}


// Easy. Construct one and register it with the framework.
auto fileLogger = new FileLogger("/dev/null");
di.registerInterface!Logger(fileLogger);

// Now the framework is set up to
// retrieve an instance of the `Service` class.
Service service2 = di.resolve!Service();

// Just for the record:
// The file logger starts with a line count of `0`.
assert(fileLogger.lines == 0);

// Let’s use the service and see whether the supplied logger is used.
service2.doSomething();
assert(fileLogger.lines == 1); // alright!

DI-constructor generator

> All that typing gets tedious quickly, doesn’t it?

The framework can generate all the constructor boilerplate. It’s as easy as:

  1. Annotate all dependency fields with @dependency.
  2. Add mixin DIConstructor to your type. This generates a constructor with a parameter for each @dependency field and a body that assigns the values to the corresponding fields.
class Dependency1 {
}

class Dependency2 {
}

class Foo {
	// Mark dependencies with the attribute `@dependency`:
	private @dependency {
		Dependency1 d1;
		Dependency2 d2;
	}

	// Let the framework generate the constructor:
	mixin DIConstructor;
}

auto di = new DI();
Foo foo = di.resolve!Foo();

// It works:
assert(foo.d1 !is null);
assert(foo.d2 !is null);

Meta