Published on January 1, 2024 by Damiรกn Pumar
What is dependencies injection? ๐
Dependencies injection is a design pattern that is used to create objects dependent on other objects. Instead of creating dependent objects within the class that uses them, we pass them through the constructor or a method.
What is an dependencies injection container? ๐ฆ
A dependency injection container is a component of our application that knows how to create and inject dependencies. The dependency injection container is responsible for instantiating classes, resolving their dependencies, and providing an instance of the class when requested.
Why use a dependency injection container? ๐ค
The main reason to use a dependency injection container is to reduce the coupling between classes. The dependency injection container is responsible for creating and injecting dependencies, so the class that uses them does not need to know how to create them.
Advantages ๐
- Reduces coupling between classes.
- Facilitates code reuse.
- Facilitates unit testing.
- Facilitates the creation of objects.
- Facilitates application configuration.
- Optimizes application performance.
In short, using dependency injection and particularly using a dependency injection container allows us to create more flexible, maintainable and testable applications.
Disadvantages ๐
- Increases the complexity of the application.
- Increases the learning curve.
How to implement a dependency injection container in frontend? ๐ค
In frontend we have many options to implement a dependency injection container, in this post we will see how to implement it in a Typescript application, but you can implement it in any frontend framework or library, or in vanilla javascript or even on any backend application.
For this occasion we will use ts-injecty a dependency injection container that I have created myself, but you can use any other dependency injection container.
ts-injecty ๐
Take a look ๐ https://github.com/damianpumar/ts-injecty
Advantages of ts-injecty ๐
- Zero dependencies.
- Javascript and Typescript support.
- Lightweight.
- No configuration (no reflect-metadata, or extra configurations in tsconfig).
- Open source.
- Easy to use.
Installation ๐ฆ
npm install ts-injecty --save
Index ๐
Instantiate objects without dependencies
class A {
public sum(x: number, y: number) {
return x + y;
}
}
const dependencies = [register(A).build()];
Container.register(dependencies);
const a = Container.resolve(A);
expect(a.sum(1, 2)).toBe(3);
Instantiate objects with one dependencies
Now letโs see how we can inject an object that depends on another object, in this case A
depends on B
.
With ts-injecty
we can do it in the following way, super simple ๐.
class A {
constructor(private b: B) {}
public sum(x: number, y: number) {
return this.b.sum(x, y);
}
}
class B {
public sum(x: number, y: number) {
return x + y;
}
}
Container.register([register(A).withDependency(B).build()]);
const resolved = Container.resolve(ClassWithInterfaceDependency);
expect(resolved.doSomething()).toBe("HI");
Instantiate objects with dependencies of dependencies
In this case A
depends on B
and B
depends on C
, as we can dynamically inject an object that depends on another object that in turn depends on another object, easy ๐.
class A {
constructor(private b: B) {}
public sum(x: number, y: number) {
return this.b.sum(x, y);
}
}
class B {
constructor(private c: C) {}
public sum(x: number, y: number) {
return this.c.sum(x, y);
}
}
class C {
public sum(x: number, y: number) {
return x + y;
}
}
const dependencies = [
register(B).withDependency(C).build(),
register(A).withDependency(B).build(),
];
Container.register(dependencies);
const a = Container.resolve(A);
expect(a.sum(1, 2)).toBe(3);
Instantiate objects with more than one dependency
In this case A
depends on B
and C
, letโs see it ๐.
class A {
constructor(
private b: B,
private c: C
) {}
public sum(x: number, y: number) {
return this.b.sum(x, y) + this.c.sum(x, y);
}
}
class B {
public sum(x: number, y: number) {
return x + y;
}
}
class C {
public sum(x: number, y: number) {
return x + y;
}
}
const dependencies = [register(A).withDependencies(B, C).build()];
Container.register(dependencies);
const a = Container.resolve(A);
expect(a.sum(1, 2)).toBe(6);
As you can see, ts-injecty has changed the
withDependency
method towithDependencies
in plural, this is because now we can inject more than one dependency.ts-injecty
is smart enough to know that if we pass a single argument towithDependencies
it is because we want to inject a single dependency, and if we pass more than one argument it is because we want to inject more than one dependency. In addition, this method will show us in the intellisense the type of argument that we need to pass applying type-checking at compile time.
Instantiate objects with a singleton dependency
ts-injecty
by default registers dependencies as transient
unless we register an implementation (we will see later).
Letโs see the case where A
depends on B
and B
is a singleton dependency ๐.
class A {
constructor(private b: B) {}
public sum(x: number, y: number) {
return this.b.sum(x, y);
}
}
class B {
private lastSum: number = 0;
public sum(x: number, y: number) {
this.lastSum += x + y;
return this.lastSum;
}
}
const dependencies = [
register(B).asSingleton().build(),
register(A).withDependency(B).build(),
];
Container.register(dependencies);
const a = Container.resolve(A);
expect(a.sum(1, 2)).toBe(3);
const a2 = Container.resolve(A);
expect(a2.sum(1, 2)).toBe(6); // 3 + 3
Instantiate objects with a interface dependency
As you know, in typescript
interfaces are not transpiled, so this scenario is much simpler than you imagine, since it does not matter if the implementation of that interface is a function, a class or a react hook, ts-injecty
has no problem injecting a dependency of an interface, we just need to comply with the contract agreed in the interface.
Letโs see a first implementation of an interface ๐.
interface B {
doSomething(): string;
}
class A {
constructor(private b: B) {}
public doSomething() {
return this.b.doSomething();
}
}
const Implementation = () => {
return {
doSomething(): string {
return "HI";
},
};
};
Container.register([register(A).withDependency(Implementation).build()]);
const resolved = Container.resolve(A);
expect(resolved).toBeInstanceOf(A);
expect(resolved.doSomething()).toBe("HI");
We could also solve this same case, when we have a class that implements the interface, the result is the same, what changes is the configuration of the dependency.
interface B {
doSomething(): string;
}
class A {
constructor(private b: B) {}
public doSomething() {
return this.b.doSomething();
}
}
class Implementation implements B {
public doSomething(): string {
return "HI";
}
}
Container.register([register(A).withDependency(Implementation).build()]);
const resolved = Container.resolve(A);
expect(resolved).toBeInstanceOf(A);
expect(resolved.doSomething()).toBe("HI");
I imagine that with all these alternatives, you already have an idea of how you can implement a dependency injection container in your frontend project.
Now, what would happen if you need to change your dependencies at runtime? with ts-injecty
it is super simple, the truth is, since you only have to create a class inherit this class to a Factory
that is implemented in ts-injecty
and then register that class as a dependency, letโs see it ๐.
interface X {
doSomething(): string;
}
class A {
public doSomething() {
return "HI";
}
}
class B {
public doSomething() {
return "BYE";
}
}
import { Factory } from "ts-injecty";
class FactoryImplementation extends Factory {
public create(type: "A" | "B"): X {
if (type === "A") {
return this.resolver.resolve(A);
}
return this.resolver.resolve(B);
}
}
Container.register([
register(A).build(),
register(B).build(),
register(FactoryImplementation).build(),
]);
const resolved = Container.resolve(FactoryImplementation);
expect(resolved.create("A")).toBeInstanceOf(A);
expect(resolved.create("B")).toBeInstanceOf(B);
In this case, ts-injecty
will automatically inject a Resolver
when instantiating this FactoryImplementation
that will allow us to resolve the dependencies we need at runtime.
If you liked this post, do not forget to share it with your friends and follow me in Twitter to be aware of my next posts.
Aquรญ comparto contigo un proyecto de cรณmo implementar ts-injecty
en un proyecto de React, Stackblitz
Bye ๐
Written by Damiรกn Pumar
Something wrong? Let me know ๐
← Back to blog