Intensive article: Dependency Injection in JS/TS – Part 1
Overview
Dependency injection abstracts the internal implementation of functions as parameters, making it easier for us to control these.
The original text follows the ideas of "how to solve the problem that cannot be single-tested, unify the entry of dependency injection, how to automatically ensure the correct order of dependencies, how to solve circular dependencies, and top-down vs bottom-up programming thinking", the idea of dependency injection from the idea. The starting point, to the extended features are coherently strung together.
How to solve the problem of unable to do single test
If a function content implementation is a random function, how to test it?
export const randomNumber = (max: number): number => {
return Math.floor(Math.random() * (max + 1));
};
Because the result is out of control, it is obviously impossible to do a single test, then Math.random
the function is abstracted into the parameter, and the problem will be solved!
export type RandomGenerator = () => number;
export const randomNumber = (
randomGenerator: RandomGenerator,
max: number
): number => {
return Math.floor(randomGenerator() * (max + 1));
};
But it brings a new problem: this randomNumber
the interface of the function itself, and the parameters become complicated and not so easy to use.
Factory function + instance pattern
In order to facilitate business code calls, it is not enough to export factory functions and instances for business use at the same time!
export type RandomGenerator = () => number;
export const randomNumberImplementation =
({ randomGenerator }: Deps) =>
(max: number): number => {
return Math.floor(randomGenerator() * (max + 1));
};
export const randomNumber = (max: number) =>
randomNumberImplementation(Math.random, max);
This looks good at first glance. The single test code references randomNumberImplementation
function and randomGenerator
mock as a function with a fixed return value; business code references randomNumber
, because the built-in Math.random
It is more natural to use.
As long as each file follows this dual-export mode, and the business implementation does not have additional logic other than passing parameters, this code can solve both single-test and business problems at the same time.
But it brings a new problem: there are both factory functions and instances in the code, that is, build and use at the same time, so the responsibilities are not clear, and because each file must reference dependencies in advance, it is easy to form circular references between dependencies, even if the specific At the function level, there is no circular reference between functions.
Unified dependency injection entry
This problem can be solved by collecting dependencies with a unified entry:
import { secureRandomNumber } from "secureRandomNumber";
import { makeFastRandomNumber } from "./fastRandomNumber";
import { makeRandomNumberList } from "./randomNumberList";
const randomGenerator = Math.random;
const fastRandomNumber = makeFastRandomNumber(randomGenerator);
const randomNumber =
process.env.NODE_ENV === "production" ? secureRandomNumber : fastRandomNumber;
const randomNumberList = makeRandomNumberList(randomNumber);
export const container = {
randomNumber,
randomNumberList,
};
export type Container = typeof container;
In the above example, an entry file references all constructor files at the same time, so these constructor files do not need to depend on each other, which solves the big problem of circular references.
Then we instantiate these constructors in turn, pass in the dependencies they need, and then use container
to export them uniformly and use them. For users, they don't need to care about how to build them, and they can be used out of the box.
But it brings a new problem: the entry code of unified injection must change with the change of business files. At the same time, if there is a complex dependency chain between constructors, it will become more and more complicated to maintain the order manually. : For example, A depends on B, and B depends on C. If you want to initialize the constructor of C, you must first initialize A, then initialize B, and finally initialize C.
How to automatically guarantee the correct order of dependencies
Is there a way to fix the template logic of dependency injection so that it is automatically initialized according to the dependencies when it is called? The answer is yes, and it is very beautiful:
// container.ts
import { makeFastRandomNumber } from "./fastRandomNumber";
import { makeRandomNumberList } from "./randomNumberList";
import { secureRandomNumber } from "secureRandomNumber";
const dependenciesFactories = {
randomNumber:
process.env.NODE_ENV !== "production"
? makeFastRandomNumber
: () => secureRandomNumber,
randomNumberList: makeRandomNumberList,
randomGenerator: () => Math.random,
};
type DependenciesFactories = typeof dependenciesFactories;
export type Container = {
[Key in DependenciesFactories]: ReturnValue<DependenciesFactories[Key]>;
};
export const container = {} as Container;
Object.entries(dependenciesFactories).forEach(([dependencyName, factory]) => {
return Object.defineProperty(container, dependencyName, {
get: () => factory(container),
});
});
The core code is in Object.defineProperty(container)
. All functions accessed from container[name]
will be initialized when they are called, and they will go through this processing chain:
- Initialization
container
is empty, provides no functions, and does not execute anyfactory
. - 当业务代码
container.randomNumber
时,触发get()
,randomNumber
的factory
container
incoming. -
randomNumber
的factory
,那么container
的子key 并不会被访问,randomNumber
函数就成功创建, the process is over. - The key step is coming, if the
randomNumber
offactory
uses any dependencies, assuming that the dependency is itself, it will fall into an infinite loop, which is a code logic error, and an error should be reported; if Dependency is someone else, assuming thatcontainer.abc
is called, then it will triggerabc
whereget()
, repeat step 2, untilabc
offactory
f2cafe88a9ee8factory
was successfully executed, so that the dependencies were successfully obtained
It's amazing that the fixed code logic will automatically sniff the dependency tree according to the access link, and use the correct order to start execution from the module that has no dependencies factory
, layer by layer, until the top package All dependencies are built. The construction link of each sub-module and the main module are typed, which is very beautiful.
How to solve circular dependencies
This is not to say how to solve the function circular dependency problem, because:
- If function a depends on function b, and function b depends on function a, this is equivalent to that a depends on itself, and even gods can’t save it. If the circular dependency can be solved, it is as exaggerated as declaring the invention of perpetual motion machines, so this scenario does not need to be Consider solving.
- Dependency injection makes modules not referenced, so there is no circular dependency problem between functions.
Why do you say that a depends on itself and cannot even save the gods?
- The implementation of a depends on a. To know the logic of a, you must first understand the logic of the dependency a.
- The logic of dependency a cannot be found, because we are implementing a, so recursion will lead to an infinite loop.
Does dependency injection still need to solve the circular dependency problem? Need, such as the following code:
const aFactory =
({ a }: Deps) =>
() => {
return {
value: 123,
onClick: () => {
console.log(a.value);
},
};
};
This is the most extreme scenario of circular dependence, relying on yourself. But logically, there is no infinite loop, if onClick
fires after a
is instantiated, then it makes sense that it prints 123
.
But the logic can't be ambiguous. Without special treatment, a.value
can't really be parsed.
The solution to this problem can refer to the spring three-level cache idea, which is discussed in the intensive reading section.
Top-down vs bottom-up programming thinking
The original text has been summarized and sublimated, which is quite valuable for thinking: the thinking habit of dependency injection is top-down programming thinking, that is, thinking about the logical relationship between packages first, without really realizing it first.
In contrast, the bottom-up programming thinking needs to implement the last module without any dependencies first, and then implement other modules in order, but this implementation order does not necessarily conform to the order of business abstraction, and also limits the implementation process.
intensive reading
We discuss how the spring framework solves the problem with L3 cache when the object A
and the object B
refer to each other.
Whether dependency injection is implemented with spring or other frameworks, when the code encounters such a form, it encounters a scenario of A
B
circular reference:
class A {
@inject(B) b;
value = "a";
hello() {
console.log("a:", this.b.value);
}
}
class B {
@inject(A) a;
value = "b";
hello() {
console.log("b:", this.a.value);
}
}
从代码执行角度来看,应该都可以正常执行a.hello()
与b.hello()
才对, A
B
, but their value
does not constitute a circular dependency. As long as their values can be obtained in advance, there should be no problem with the output.
But the dependency injection framework encountered a problem, initialization A
dependency B
, initialization B
dependency A
Cache implementation ideas:
The meanings of the spring three-level cache are:
L1 cache | L2 cache | L3 cache |
---|---|---|
example | Examples of semi-finished products | Factory class |
- Instance: instantiate + complete instance of dependency injection initialization.
- Semi-finished instance: Only instantiation is done.
- Factory class: A factory that generates instances of semi-finished products.
Let's talk about the process first. When A
B
cyclic dependency, the framework will be initialized in random order, assuming that the first initialization A
:
1: Look for the instance A
, but there is no first, second, and third level cache, so initialization A
At this time, there is only one address, which is added to the third level cache.
Stack: A.
L1 cache | L2 cache | L3 cache | |
---|---|---|---|
Module A | ✓ | ||
Module B |
二: A
B
,寻找B
018d68dda1f17a4b4bf9b10310b359c0--- ,但一二三级缓存都没有, B
, At this point there is only one address, which is added to the L3 cache.
Stack: A->B.
L1 cache | L2 cache | L3 cache | |
---|---|---|---|
Module A | ✓ | ||
Module B | ✓ |
Three: find the instance B
depending on the instance A
, find the instance A
, because the third-level cache is found, so execute the third-level cache to generate the second-level cache.
Stack: A->B->A.
L1 cache | L2 cache | L3 cache | |
---|---|---|---|
Module A | ✓ | ✓ | |
Module B | ✓ |
Four: Because the second level cache of the instance A
has been found, so the instance B
has completed the initialization (the stack becomes A->B), pushed into the first level cache, and emptied three level cache.
Stack: A.
L1 cache | L2 cache | L3 cache | |
---|---|---|---|
Module A | ✓ | ✓ | |
Module B | ✓ |
Five: Because the instance A
depends on the first level cache of the instance B
is found, so the instance A
completes initialization, pushes into the first level cache, and clears the third level cache.
stack: empty.
L1 cache | L2 cache | L3 cache | |
---|---|---|---|
Module A | ✓ | ||
Module B | ✓ |
Summarize
The essence of dependency injection is to abstract the internal implementation of the function as a parameter, which brings better testability and maintainability. The maintainability is "as long as the dependency is declared, and you don't need to care about how to instantiate it", and at the same time, it is automatically initialized Containers also reduce mental load. But the biggest contribution is the top-down programming mindset.
Because of its magical characteristics, dependency injection needs to solve the problem of circular dependencies. This is also a frequently asked point in interviews and needs to be kept in mind.
The discussion address is: Intensive Reading "Introduction to Dependency Injection" Issue #440 dt-fe/weekly
If you'd like to join the discussion, click here , there are new topics every week, with a weekend or Monday release. Front-end intensive reading - help you filter reliable content.
Follow Front-end Intensive Reading WeChat Official Account
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
Copyright notice: Free to reprint - non-commercial - non-derivative - keep attribution ( Creative Commons 3.0 license )
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。