preface

Almost every large application is the result of component aggregation, and as applications grow, the method of connecting components becomes one of the determining factors. This involves not only scalability issues, but also increased application complexity. In such cases, there is a cost to modifying or extending the functional code.

Dependency injection sounds complicated, but it is a straightforward concept. It is primarily a module for decoupling overly dependent state instances. The main idea is to provide dependencies as output components through external entities, either components or global containers, that receive connections from all modules of the system. This means that modules can be configured to use any dependency for reuse in different contexts.

Dependency injection

High cohesion and low coupling

High cohesion and low coupling are the criteria for judging good software design. Generally speaking, to judge whether a program design is good, mainly depends on whether the class cohesion is high or not, whether the coupling degree is low. High cohesion and low coupling make the reusability and portability of program modules greatly enhanced.

  • Cohesion: This measures the relationships within a module from a functional perspective, a good cohesive module should do exactly one thing, and all the other parts help achieve that single task.
  • Coupling: This is a measure of the interconnections between modules in a software structure. The strength of coupling depends on the complexity of the module’s indirect ports. For example, when a module directly reads or modifies another module’s data, that module is tightly coupled to the other module, and the two modules are poorly coupled simply by passing parameters to communicate.

A module ideally has high cohesion and low coupling, which means modules are easier to understand, easier to reuse, and easier to extend.

Hard-coded dependencies

In JavaScript, you can explicitly load a module using import, which is a hard-coded dependency. This is also the most common relationship between two modules, and it is the simplest and most efficient way to establish a module dependency.

Build an HTTP request component using hard-coded dependencies

In this example, Axios is used as the HTTP request library.

The HTTP request module is in utils/http.js:

import Axios from 'Axios'
export default Axios.create({
  baseURL: 'https://some-domain.com/api/'
});
Copy the code

This module creates an Axios instance and sets baseURL. So the exported object of a module is both a stateful instance and a singleton, because the module is cached after the first call to import, ensuring that it is not executed on any subsequent calls, and that the cached instance is returned.

Build the API module in/API /auth.js:

import http from '@utils/http.js';
const gateway = '/auth-api'
export const login = async (params) => {
    const res = await http.post(`${gateway}/login`, params);
    return res;
}
export const logout = async() = > {const res = await http.post(`${gateway}/logout`, params);
    return res;
}
Copy the code

This module implements two apis: one for performing login requests and the other for performing logout requests. Gateway Indicates the name of the module registered with the gateway by the back-end interface.

As you can see, the API module is not a specific instance that requires an HTTP module; any instance can run. The code above is hardcoded into a particular Axios instance, which means that when that instance is combined with another Axios instance, the code cannot be reused without modification.

Call API module in /views/login.js file:

import * as API from '@api/auth.js';

function login () {
    constparams = {... } API.login(params).then(res= >{... })}function logout () {
    constparams = {... } API.logout(params).then(res= >{... })}Copy the code

The example above shows a common approach to linking modules in JavaScript, leveraging the power of a module system to manage dependencies between the various components of an application. Export state instances from modules and load them directly from other components. This module organization is intuitive, easy to understand and debug, and each module is initialized and connected without any external interference.

On the other hand, hard-coded dependence on stateful instances limits the possibility of a module connecting to other instances, making it unreusable and difficult to unit test.

Refactoring using dependency injection

There is a simple way to use a dependency injection refactoring module: create a factory that contains a set of dependencies as parameters, rather than hard-coding the dependencies into stateful instances. Look at the code to understand this sentence.

Next, refactor the above example with dependency injection:

The HTTP request module is in utils/http.js:

import Axios from 'Axios'

export default (option) => {
    return Axios.create(option);
}
Copy the code

The first step in the refactoring is to convert the HTTP module into a factory so that it can be used to create as many AXIOS instances as possible, which means that the entire module is now reusable and stateless.

Build the API module in/API /auth.js:

export default (gateway, http) => {
    const login = async (params) => {
        const res = await http.post(`${gateway}/login`, params);
        return res;
    }
    const logout = async() = > {const res = await http.post(`${gateway}/logout`, params);
        return res;
    }
    return {
        login,
        logout
    }
}
Copy the code

At this point, the API module is also stateless. It no longer exports any specific instance, just a simple factory function. The most important thing, however, is to take HTTP dependency injection as an argument to the factory function, removing the previous hard-coded dependencies. This simple modification can create a new Auth module by connecting it to any Axios instance.

Finally, create and connect the above module in /views/login.js module, which represents the top layer of the application.

import axiosFactory from '@utils/http.js';
import authFactor from '@api/auth.js';

const axios = axiosFactory({
    baseURL: 'https://some-domain.com/api/'
});
const authApi = authFactor('/auth-api', axios);

function login () {
    constparams = {... } authApi.login(params).then(res= >{... })}function logout () {
    constparams = {... } authApi.logout(params).then(res= >{... })}Copy the code

In this case, dependency injection decouples modules from specific dependency instances, making it easy to reuse each module without changing their code. It is also much more feasible to unit test each module, easily providing simulated dependencies to test modules independently of the state of the other components of the system.

Although there are many advantages, there are still disadvantages. Using dependency injection moves the dependency responsibility from the bottom layer to the top layer of the architecture, meaning that the top layer components have a high coupling cost. And all dependencies must be instantiated sequentially in the top-level component, effectively forcing you to manually build the dependency diagram for the entire application. As the number of connected modules increases, the dependency graph becomes more difficult to manage.

One solution to this shortcoming is to split the dependency ownership between multiple components rather than centralize them in one place. This reduces the complexity of dependency management because each component is only responsible for a particular dependency subgraph.

Another solution is the following: use dependency injection containers.

Dependency injection container

At the heart of the dependency injection container is a central registry. It manages the components of the system and acts as a modifier when each module needs to load dependencies, and needs to identify the module’s dependency requirements before instantiation.

Build the dependency injection container

In/utils/DIContainer. Js

import fnArgs from 'parse-fn-args';
class DIContainer {
    constructor () {
        this.dependencies = {};
        this.factories = {};
        this.diContainer = {};
    }
    factory (name, factory) {
        this.factories[name] = factory;
    }
    register (name, dep) {
        this.dependencies[name] = dep;
    }
    get (name) {
        if (!this.dependecies[name]) {
            const factory = this.factories[name];
            this.dependecies[name] = factory && this.inject(factory);
            if (!this.dependecies[name]) {
                throw new Error('Cannot find module: '+ name); }}return this.dependencies[name];
    }
    inject (factory) {
        const args = fnArgs(factory).map(dependency= > this.get(dependency));
        return factory.apply(null, args); }}export default() = > {return new DIContainer();
}
Copy the code

The DIContainer module is a factory with four methods:

  • Factory ()Use to associate component names with factories
  • The register ()Use to associate component names directly with instances
  • The get ()Retrieves components by name. If an instance is already available, return itself, otherwise the method will try to callinjectMethod, which resolves the module’s dependencies and uses them to invoke the factory.
  • Inject ()The first to useparse-fn-argsLibrary that extracts a list of arguments from the factory function as input. Each parameter name is then mapped to useThe get ()Method to obtain the corresponding dependency instance. Finally, the factory is invoked by providing the list of dependencies you just generated.

Here’s how DIContainer works: the HTTP request module is in utils/http.js:

import Axios from 'Axios'

export default (diContainer) => {
    const axiosOption = diContianer.get('axiosOption');
    return Axios.create(axiosOption);
}
Copy the code

Build the API module in/API /auth.js:

export default (diContainer) => {
    const gateway = diContainer.get('gateway');
    const http = diContainer.get('http');
    const login = async (params) => {
        const res = await http.post(`${gateway}/login`, params);
        return res;
    }
    const logout = async() = > {const res = await http.post(`${gateway}/logout`, params);
        return res;
    }
    return {
        login,
        logout
    }
}
Copy the code

Call API module in /views/login.js file:

import DIContainer from '@utils/diContainer.js
import axiosFactory from '@utils/http.js';
import authFactory from '@api/auth.js'; const diContainer = DIContainer(); diContainer.register('axiosOption', { baseURL: 'https://some-domain.com/api/'
});
diContainer.register('gateway'.'/auth-api');
diContainer.factory('http', axiosFactory);
diContainer.factory('authApi', authFactory)

const authApi = diContainer.get('authApi');

function login () {
    constparams = {... } authApi.login(params).then(res= >{... })}function logout () {
    constparams = {... } authApi.logout(params).then(res= >{... })}Copy the code

It is worth noting that diContainer is “lazy,” meaning that each instance is created only as needed. In addition, each dependency is automatically wired without having to manually handle it in advance. That way, you don’t have to know in advance the correct order of instantiation and connection modules, and it all happens automatically on demand.

At the end

For more articles, please go to Github, if you like, please click star, which is also a kind of encouragement to the author.