Dependency Injection in Javascript
This article explores dependency injection in Javascript using higher-order functions and compares it against dependency injection in classes.
Dependency injection (DI) is a programming pattern in which a dependency is passed using the parameters instead of instantiating it within the function or class. DI enables creating isolated individual components within application code and makes it easy to switch those dependencies in the future as the requirement changes. Passing parameters as a dependency also allows to easily unit test those components in isolation by injecting their mocked version.
This article explores dependency injection in Javascript using higher-order functions and compares it against classes. This pattern is appropriate in any language that supports standalone function definitions.
For the rest of the article, let us assume that we are building a simple hypothetical application with an Course
entity and the Course has Lesson
. AWS DynamoDB
stores the Course and S3
bucket stores lessons as JSON objects.
First, to understand this pattern, let us explore how we would achieve dependency injection using classes, then we will explore some examples of this pattern using javascript functions.
Dependency Injection using classes
First, let us explore dependency injection using classes, then we can compare it with dependency injection using higher-order functions.
DI using classes
In the above example, we have defined a Course class that has two methods. courseById
gets the course from DynamoDB and addLesson
adds a given lesson to S3. We have also injected documentClient
and S3
from aws-sdk
.
Now that we have this class with some methods, let us see how we could use it. To use this in our application, we first need to create an instance of the Course
. Unless we are using a dependency injection framework, typically, we achieve this using a Factory
class.
We can then use the Factory.createCourse
in our application to create an instance, of course, then invoke the appropriate method. We could extend Factory
to include other static methods to create an instance of other entities like Lesson
.
Now, if the requirement changes in the future and we need to store Course
in some other store, then we can update the Factory
or Course
Moreover, our business logic will be untouched.
Dependency Injection using the higher-order function
With the examples using classes, notice that we would always need to have an instance of S3
and DocumentClient
before we can have an instance of Course
.
Imagine a scenario where we have a webhook or AWS Lambda Function
whose job is to getCourseById
. In this scenario, we will end up creating an instance S3
even though we will not use it in this request.
In an actual application, there might be much more complex logic to setup and teardown each dependency which might degrade the performance.
Let us explore dependency injection at the function level.
The premise of this pattern is based on a function that returns another function ( higher-order function). The higher-order function accepts all the dependencies that are required for the child function to perform its job.
DI with a higher-order function
In the above example, we create a function called makeGetCourseById
. Its job is to create a function that we can use to get a course by id.
We can then use the higher-order function as above in our application code. We can follow the same pattern for addLesson
.
DI with a higher-order function
The idea with the higher-order function is that we create individual functions like getCourseById
or addLesson
once and pass the instance around in our application. If we did not use the higher-order components, then we would need to pass an instance of documentClient
or s3
every time we want to get course by id.
The following code sample demonstrates how we can use the higher-order function in AWS Lambda Handler
.
Creating services with higher-order functions
There will be scenarios where we might need to create a collection of methods that we can use without initializing each method. For example, we want to attach a service with a collection of methods in the GraphQL
context to use them in GraphQL resolvers.
To accomplish this, we can create a function that returns an object with a collection of functions.
We can then use it in our application code as following. Keep in mind that we create an instance of courseService
once per application execution if possible or can even cache it if necessary.
Passing methods around
Often we will run into scenarios where we might want to use the higher-order function inside another higher-order function. For example, we might want to check if the Course exists before adding a lesson. We can approach this by accepting getCourseById
and S3
in the parameters.
The application code will look like the following:
Summary
We explored two different ways to achieve dependency injection in Javascript. One way of achieving dependency injection in Javascript is to use Class
and a Factory
. The other approach is to use higher-order functions to achieve granular injection on the function level. Depending on the application use cases, it is best to pick one approach and stick to it. Higher-order functions are relatively simple to work with and are more flexible than Javascript classes.