Dependency Injection in Conjoon
Configuring conjoon's IoC-Container for using Constructor Injection
tip
With conjoon, you are able to configure the dependencies of your classes upfront and inject them dynamically during runtime. This guide gives details on how to use the IoC-Container that comes with conjoon.
info
This guide originates from the blog article Dependency Injection in JavaScript.
Motivation
Low coupling and high cohesion does not imply the total absence of associations: The usage of abstraction layers rightfully demands specifics that provide concrete implementations satisfying contracts between parts of a system. After all, a program doesn’t work if there is nothing that conforms to its Interfaces and Abstract Classes.
Figure 1 The most basic expectation regarding a computer program is that a given input produces some output.
However, the way these dependencies are wired throughout source code often comes with a bitter taste: Dependencies are found in convoluted and deeply nested program code, effectively violating SRP and DIP.
We are in the need of tools and design patterns that can help us with detangling the code, or we will end up with a program that has intricate boilerplate code for setting up mocks and stubs for testing, or worse: Becomes a nightmare to integrate with different environments and infrastructures. Granted, languages like JavaScript makes it easy to mock dependencies during tests, but other languages are not so forgiving and test cases tend to get more complicated the more dependencies are hardwired.
The following source code was taken from a REPOSITORY-implementation that uses a concrete Storage
-class that hides away infrastructure that is used for writing data:
class DataRepository {
async storeDate(data) {
const {Storage} = await import("storageApi");
const store = new Storage(...);
store.save(data);
}
}
It seems to align nicely with our code standards — it’s small, it explains its intends clearly without additional comments, and it delegates to a sub-program responsible for communicating with infrastructure that is not of interest to us.
However, this method uses a hardwired dependency to serverApi
which gets imported as a module. Even more, everything the constructor of Storage
requires has to be handled from inside the storeData()
-method. For testing this code, the developer has to create a Mock not only for Storage
, but also needs to stub the call to import
. The DataRepository directly accesses the low-level API from within its boundaries, which results in strong coupling between two different layers that actually should be unaware of each other.
Figure 2 The “DataRepository” directly accessing “Storage” creates strong coupling between two concretes that should be unaware of each other.
The Dependency Inversion Principle requires high-level modules to not import anything from low-level modules. This is one of the SOLID design principles.
Figure 3 The “DataRepository” is configured with the “Storage”-instance. Now, the repository can focus on using the Storage’s public API and doesn’t have to take care of importing and configuring it. In tests, the API of the “Storage”-instance can easily be mocked.
This article introduces an Inversion of Control (IoC)-Container that decides during runtime if code has additional dependencies defined, and if any existing dependency should be resolved by the IoC-Container. This is realised through bindings configured by a client and passed to the the IoC-Container: These provide information for the concrete that has to be instantiated for a type, i.e. an Interface, or any (Abstract) Class, required by an arbitrary host.
Bindings can be used and adjusted application-wide, but it’s good practice to provide them during bootstrapping. This makes it easy to run programs with different implementations for selected clients, contexts or environments, and this all works without having to change a single line of low/high-level code at all.
Figure 4 Our Proxy wraps the constructors of selected target classes and injects dependencies with arguments as needed.
Proxies help with the implementation of resolving objects and dependencies, and this approach is not exclusive to JavaScript: For example, Java has Proxies, Spring uses them for its IoC and AOP.
If you need to catch up with the concept of Proxies and how they work in JavaScript, I recommend you read through this elaborate article that provides details on how to use Proxies with Promises to create Fluent Interfaces.
For the following examples, I will constantly refer to the IoC-Container implementation of coon.core.ioc: This is an implementation specific for the Sencha Ext JS framework, but it’s concepts can easily be carried over to other frameworks or framework agnostic code.
How it works
Target classes need to provide information if they are injectable, i.e. if they should be considered by the IoC-Container during instantiation. This is needed because we want to autowire dependencies to keep our program as flexible as possible: The IoC-Container gets configured with bindings during the startup sequence, then takes care of publishing the configured types with their concrete implementations during the runtime of the application.
Figure 5 The IoC-Container will take care of dynamically resolving dependencies for a concrete instance that is required by the client.
Most programming languages and their platforms already provide the tools for handling additional information written with source code: Metadata is often created with the help of annotations in Java, or Attributes in PHP8.
Metadata: Static builds vs. runtime configuration
With the almost incomprehensible amount of tooling options for JavaScript, using annotations would probably cost little effort; however, it would most certainly mean that the build stack of our project changes: An additional tool that has the implementation for parsing our source code also extracts and translates metadata and makes sure that the resulting build does not break during runtime.
An annotation in the form of
/\*\*
\* @injectable store:Storage
\*/
class Repository {
...
}
would be useful, and additional annotations can be defined in some sort of dictionary.
The parser could then build configuration files out of the metadata found in the sources, connect them with the names of the target classes (and the paths to the imports), along with the properties (i.e. names of the instance variables of the target classes) which expect a particular type, and then plug it all together by applying the bindings configured by the developers and stored in the IoC-Container.
Figure 6 Using annotations with JavaScript code would require a separate build step in the ci/cd pipeline.
We strive for an implementation that does not need such additional tooling: We will provide the metadata as static class members on top of the injectable classes.
The following source code demonstrates the use of the static
-property: A property named required
is the root for the meta information looked up by the IoC-Container and its Dependency Resolver: It holds all the names of the instance variables that expect specifics of a given type: In the example, an instance of Repository
only works with a store
-member that holds a reference to an instance of Storage
.
class Repository {
static: {
required: {
store: "Storage"
}
}
...
}
With the help of the Proxy-Api, we can then add a trap for the calls to the constructor of the injectable classes, in this case the Repository
-class:
class Repository {
static: {
required: {
store: "Storage"
}
}
constructor({store}) {
this.store = store;
}
...
}
const constructorHandler = {
construct (target, argumentsList, newTarget) {
if (target.required) {
// container holds a reference to the ioc-container
container.inject(argumentsList, target.require);
}
return new target(...argumentsList);
}
};
Repository = new Proxy(Repository, constructorHandler)
The handler will delegate to the IoC-Container before the instance of the target class is created: The IoC-Container then inspects the argument-list and looks for any missing properties in a previously contracted argument-object that’s used to configure the instance. Denoted by the required
-property, the instance variable’s name must be the same as the configuration object containing the property needed by the constructor:
// IoC-container will not inject anything, since the instance gets configured
// with a "store"-property
new Repository({store: new Storage(), uri: "/resourceUri"});
// since the "store"-property is missing, the IoC-container will
// inject a concrete of "Storage" according to the available bindings
new Repository({, uri: "/resourceUri"});
Creating the Bindings
Bindings are the Point of Truth for our application since we rely on builders and resolvers that are configured upfront and take care of assembling associations during runtime. Bindings map concrete Sub Types to Types, means: They “bind” a typed variable to the specific implementation of the Type, so our IoC-Container knows what to apply and where to apply it (the when is implied by the usage of a Constructor Injector). The requested specific implements an interface or extends an (abstract) class and the LSP gives us the freedom to provide arbitrary implementations of this given Type.
If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2, then S is a subtype of T. — Barbara Liskov, Data Abstraction and Hierarchy
Since we are loosely typed, our Dependency Resolver (think of it as sort of a Builder, ) must and will make sure that our specifics are indeed instances of the required type.
Finding a common language
We will introduce a model language that will help us with formulating the bindings needed throughout our program.
We have a class A that uses an instance of a class B:
Figure 7 A needs B.
The code for
“A has a dependency to Type B, and this dependency is reflected in A’s instance variable b”
could look like this:
// Pseudo code
abstract class B {
abstract calculate();
}
class A {
constructor (B b)
{
this.b = b;
}
calculation()
{
this.b.calculate();
}
}
Obviously, sources that rely on A will not work without an [instanceof A].b — as soon as calculation()
delegates to b.calculate()
and b is undefined
, an exception will be thrown.
We are looking for a formal (yet simple) definition that can be used with JavaScript to configure these dependencies: We’ll agree on JSON as the format, since it allows for key/value-pairs whereas the keys are of type string
and their values can be any of string
, integer
, boolean
, NULL
, object
and array
— we’ll make use of string
and object
.
Let’s refine the task for resolving the dependencies of A:
**when** A
**requires** B
**give** _new instance_ of B
That’s a rather simple term which will be translated later into an assignment by the Dependency Resolver. For now, this is how it’s transposed to JSON (don’t mind the explanatory comments):
{
/\* when \*/
"A": {
/\* "needs": "give" \*/
"B" : "InstanceOfB"
}
}
Use Case: Injecting Authentication Methods
With coon.core.ioc
as part of a coon.js-application, here’s a typical call to coon.core.ioc.Container.bind()
:
// Some class names have been shortened in favor of
// readability.
coon.core.ioc.Container.bind({
"conjoon.dev.cn\_mailsim": {
"conjoon.SimletAdapter": "conjoon.BasicAuthSimletAdapter"
},
"conjoon.cn\_mail": {
"coon.core.data.request.Configurator": {
"$ref": "#/$defs/RequestConfiguratorSingleton"
}
},
"$defs": {
"RequestConfiguratorSingleton": {
"xclass": "conjoon.cn\_imapuser.data.request.Configurator",
"singleton": true
}
}
});
This configuration represents bindings of the extjs-app-imapuser-package, an npm package providing user authentication for conjoon’s extjs-app-webmail, which is an email client written in JavaScript.
The Login-Screen for the JavaScript webmail client used in conjoon, depending on the authentication module configured with this instance.
extjs-app-webmail communicates with a backend that is agnostic of the authentication being used — its architecture allows for guarding the endpoints with arbitrary authentication methods: It could be Basic access authentication, or the API could use a guard that relies on token based authentication. That is why the requesting client — in this case extjs-app-webmail — needs to be configured with the proper security technique the backend understands. This is done by using Request Configurators that hook into (some/all/none at all) outgoing requests and add the authentication information if required by the backend, e.g. a Authorization
-header field, holding Bearer
- ,Basic
- or other information.
Bindings explained
Let’s have a detailed look at the given binding configuration. First of, the bindings configured here are introduced with namespaces instead of class names. This is just another way of defining bindings for a set of classes owned by a namespace (i.e. a whole module): Instead of individually defining dependencies for
conjoon.dev.cn_mailsim.A
, conjoon.dev.cn_mailsim.B
, conjoon.dev.cn_mailsim.C
, …
we fall back to their common namespace, so the Dependency Resolver can look up bindings configured for this module when a target class is not explicitly specified in the configuration. Target classes are always given precedence, then namespaces are queried.
The same goes for the following section, albeit the value of the give- implication is not the name of a class: It’s a configuration that references another section.
"conjoon.cn\_mail": {
"coon.core.data.request.Configurator": {
"$ref": "#/$defs/RequestConfiguratorSingleton"
}
}
Based on the JSON-schema specification, $ref
uses an URI to reference another section of the document it’s embedded in, which allows for defining a reusable, complex configuration at one place, then re-use this configuration throughout the document by referencing it.
The (resolved)$ref
in the above example states that
**when** any class of conjoon.cn\_mail
**requires** coon.core.data.request.Configurator
**give** _Singleton_ of conjoon.cn\_imapuser.data.request.Configurator
Singletons are great when stateless objects are needed, and reduce the memory footprint in a target application.
Resolving dependencies — trapping the Factories
The class-system of Sencha Ext JS makes — almost exclusively — use of Factories when instances are created. This is useful for dynamically loading classes: Its microloader will take care of mapping class-names to the existing directory-structure of a project, but loading happens synchronously (in the worst case, if a class was not pre-loaded). Using Sencha Ext JS without it’s own class system is almost impossible. In some cases, this is an unpleasant surprise for users starting with Ext JS in 2022.
Figure 8 Sencha Ext JS Factory methods take care of resolving dependencies and instantiating objects.
However, the use of factories and factory methods throughout the framework makes it really easy to apply Proxies to them and plays into our hands with our constructor injection approach. Carefully selecting constructors of injectable target classes becomes obsolete and we can rely on the interiors of the framework when we have to decide if dependencies need to be injected.
A Wrapper-Proxy is installed as soon as coon.core.ioc.Container.bind()
is called:
installProxies () {
const me = this;
Ext.Factory = new Proxy(
Ext.Factory,
Ext.create("coon.core.ioc.sencha.resolver.FactoryHandler")
);
Ext.create = new Proxy(
Ext.create,
Ext.create("coon.core.ioc.sencha.resolver.CreateHandler")
);
},
There are two Proxy-Handlers that serve two different purposes. Let’s have a look at them:
Handler for Ext.create
The CreateHandler is a method that traps calls to Ext.create
. It checks if the argument passed to Ext.create()
is
- a String: If that is the case, it’s assumed to be the name of the class an instance should be created for
- an Object: This generally indicates that the client submits a configuration, holding an
xtype
orxclass
providing further details on the class that serves as the template. All the properties the object contains are usually passed to the constructor of the target class, except forxtype
/xclass
{
"xtype": "alias-of-class",
// or "xclass": "fqn.of.class"
"cArg1": "foo",
"cArg2": "bar"
}
As Figure 9 shows, once the target class was successfully resolved given the internals of Ext JS, the handler fires the classresolved
-event, along with information about the class name and the JavaScript prototype of the resolved class.
Figure 9 The Handler installed for Ext.create fires an event as soon as a class was successfully resolved.
Handler for Ext.Factory
The FactoryHandler implements traps for properties requested by a client (using [get](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/get)
) and a trap for any method that could possibly be a factory method.
Factory methods are based on types that get published with class definitions. Aliases are constantly used throughout Sencha Ext JS and facilitate lazy instantiation. Those aliases are using prefixes which represent the domain they serve, for example, aliases for Ext.data.Store
have the prefix "store.”
, Ext.app.Controller
use the prefix "controller.”
and so on.
The Ext.Factory wires these configurations through to the corresponding factory methods, and that’s where the apply-handler previously installed by the get
-handler comes into play: It works just like the CreateHandler and differs only in subtle details when arguments to the factory methods are scrutinized. In the end, this proxy will trigger the classresolved
-event for the same reason as its complement.
if (cls) {
const className = Ext.ClassManager.getName(cls);
me.fireEvent("classresolved", me, className, cls);
}
Proxying the constructors
It all boils down to the ConstructorInjector: Once the classresolved
-event is published, the ConstructorInjector is used with an observer to decide whether it should inject dependencies into the target class’ constructor (the information about the target class is exposed with the event details). It checks whether the class is injectable and if that’s the case, it will apply a trap for the target class’ constructor.
Think of the Strategy Pattern here that allows us to dynamically change implementation details. It should be noted that the ConstructorInjector is working on objects passed to the constructor as arguments, rather than a list of arguments. So the ConstructorInjector is more of a Property Injector. The name was chosen since ConstructorInjector better reflects the step in the build chain the injector is woven into: The implementation for the Sencha Ext JS framework is kept simple and provides mainly (but not less than) qol-improvements for this framework.
The trap for the constructor will probe the target class for all the dependencies required (defined as metainformation), then use the Dependency Resolver to create something useful out of the binding definition that was previously registered with coon.core.io.Container.bind()
.
Figure 10 When the client requests a new instance, the ConstructorInjector will make sure that dependencies required by the target instance are created and injected, if not already specified by the client.