Introduction to Dependency Containers: Part 1
Learn about Dependency Containers in an intuitive way
In the last article we learned about
Dependency Injection
(DI for short). We said that DI is a mechanism that removes the burden
of dependency creation from software components (e.g. classes). As the
burden is removed from one place it has to be placed in another. That
place are the Dependency Containers.
In this article we will
learn about Dependency Containers and we will build one ourselves!
Dependency Containers usually provide a way to:
- Register a software component with the Dependency Container. This allows the Dependency Container to provide the software component as a dependency to other software components.
- Request a software component from the Dependency Container.
The methods that correspond to these features are usually called:
- register
- resolve
Let's start building our Dependency Container. We will be doing so in the JavaScript language.
We define a function that returns an object with two properties. These two properties are functions that allow us to register and resolve software components.
function createDependencyContainer() {
function register() {}
function resolve() {}
return {
register,
resolve
};
}
When we register a software component with the Dependency Container we
need to specify a unique identifier for the software component. We
will use string values for the identifiers.
We also need to specify the dependencies of the software component we
want to register. We will use string arrays to specify the
dependencies. The strings will correspond to the identifiers of other
software components registered in the Dependency Container.
Finally, we need to tell the Dependency Container how to construct an
instance of the software component. We will use a function for this.
The function will have a single parameter and its value will be an
array of the resolved dependencies of the software component.
The
code with the modified signature of the "register" function becomes:
function createDependencyContainer() {
function register(id, dependencies, factory) {}
function resolve() {}
return {
register,
resolve
};
}
Let's add an object that will keep track of the software components registered in the Dependency Container. The object keeps track of the registrations by mapping the identifiers to objects that contain information about the registered software component.
function createDependencyContainer() {
const registrations = {};
function register(id, dependencies, factory) {
registrations[id] = {
id,
dependencies,
factory
};
}
function resolve() {}
return {
register,
resolve
};
}
Let's move on to the "resolve" function. It will accept a single parameter related to the identifier of the software component that we want to resolve. We will throw an error if the Dependency Container has no registration with that identifier.
function createDependencyContainer() {
const registrations = {};
function register(id, dependencies, factory) {
registrations[id] = {
id,
dependencies,
factory
};
}
function resolve(id) {
const registration = registrations[id];
if (!registration) {
throw new Error('No registration with ID ' + id + ' found.');
}
}
return {
register,
resolve
};
}
If the Dependency Container has a registration with the specified identifier we have to create an instance of the software component. There are two possibilities:
- The requested software component has no dependencies.
- The requested software component has dependencies.
In case there are no dependencies, we simply call the factory function to create an instance of the software component.
function createDependencyContainer() {
const registrations = {};
function register(id, dependencies, factory) {
registrations[id] = {
id,
dependencies,
factory
};
}
function resolve(id) {
const registration = registrations[id];
if (!registration) {
throw new Error('No registration with ID ' + id + ' found.');
}
const { dependencies, factory } = registration;
if (dependencies.length === 0) {
return factory();
}
}
return {
register,
resolve
};
}
In case there are dependencies, we have to resolve them. We do this by calling the "resolve" function for every dependency. After we resolve the dependencies, we call the factory function with an array of the resolved dependencies.
function createDependencyContainer() {
const registrations = {};
function register(id, dependencies, factory) {
registrations[id] = {
id,
dependencies,
factory
};
}
function resolve(id) {
const registration = registrations[id];
if (!registration) {
throw new Error('No registration with ID ' + id + ' found.');
}
const { dependencies, factory } = registration;
if (dependencies.length === 0) {
return factory();
}
const resolvedDependencies = dependencies.map(dependency => resolve(dependency));
return factory(resolvedDependencies);
}
return {
register,
resolve
};
}
See the following link for an example of how our Dependency Container is used: https://github.com/ivan-georgiev-dev/introduction-to-dependency-containers/blob/part-1/script.js
This Dependency Container can be improved. For example, it always
creates new instances of the dependencies and we could improve it by
allowing the option to reuse already created instances.
We could
improve it by allowing the option to manage singleton dependencies
(i.e. limit to one instance).
We could improve it by adding
guards against cyclical dependencies (e.g. A depends on B and B
depends on A).
We will tackle these improvements in the next
parts of this series.
If you enjoy my articles, please consider supporting me.