In this title, in addition to JS is disorderly into the other several words are there is a common point, that is to rely on.
So what is dependency?
For example, right now I’m writing this blog post, but I have to edit it on my computer, which is what I rely on to get it done. In code, the most intuitive embodiment is the dependency between modules. If a module depends on another module, that other module is a dependency of that module. In fact, in our last blog post, Modules in JaVaScript, we also wrote a module dependency manager by hand.
Dependency is easy to understand, but it doesn’t mean you can rely on it at will. When writing modules, pay attention to a high cohesion and low coupling, in order to improve the scalability and maintainability of modules. Who a module depends on and how it depends on it all determines whether the final module is good or bad.
Fortunately, there is a golden rule in the programming world to improve code quality, and we can use theory to guide practice and write better code.
Dependency inversion principle
The Dependency Inversion principle (DIP) is a specific form of decoupling in which a high-level module is not dependent on the implementation details of a low-level module, and the dependencies are reversed (reversed) so that low-level modules are dependent on the requirements abstraction of high-level modules. ———— Wikipedia
The principle provides that:
- High-level modules should not depend on low-level modules; both should depend on abstract interfaces.
- Abstract interfaces should not depend on concrete implementations. Concrete implementations should rely on abstract interfaces.
Now let’s use an example to explain a wave.
// Ajax.js
class Ajax {
get() {
return this.constructor.name; }}export default Ajax;
// main.js
import Ajax from './Ajax';
class Main {
constructor() {
this.render()
}
render() {
let content = (new Ajax()).get();
console.log('content from', content); }}new Main();
Copy the code
When we started, we wrapped Ajax around the XMLHttpRequest object to request data. Then fetch came out, and we wanted to keep up with The Times and wrap FETCH to replace Ajax.
// Fetch.js
class Fetch {
fetch() {
return this.constructor.name; }}export default Fetch;
// main.js
import Fetch from './Fetch';
class Main {
constructor() {
this.render();
}
render() {
let content = (new Fetch()).fetch();
console.log('content from', content); }}new Main();
Copy the code
As you can see from the above, the whole substitution process is cumbersome, and we need to find all the references that encapsulate the request module (Ajax, Fetch) and replace them. In addition, Ajax and Fetch methods are named differently, so they need to be changed accordingly.
That’s the traditional way of dealing with dependencies. Here Main is a high-level module, Ajax and Fetch are low-level modules. Dependencies are created in high-level modules, and high-level modules directly depend on low-level modules, which limits the reusability of high-level modules.
The dependency inversion principle reverses this dependency and uses the two provisions mentioned above as its guiding principles.
// Service.js
class Service {
request(){
throw `The ${this.constructor.name}Request method not implemented! `}}class Ajax extends Service {
request(){
return this.constructor.name; }}export default Ajax;
// Main.js
import Service from './Service.js';
class Main {
constructor() {
this.render();
}
render() {
let content = (new Service).request();
console.log('content from', content); }}new Main();
Copy the code
Here we consider the co-dependent Service as an abstract interface, which is the contract that high-level and low-level modules need to abide by. In higher-level modules, it defaults to a Service having a request method to request data. In low-level modules, it follows that Service overwrites methods that should exist. Much the same goes for implementing the Expense () method for both branch and leaf objects in Experimenting with Composite patterns in JavaScript.
Even if we later need to wrap AXIos instead of fetch, we just need to change it in service.js.
Let’s go back to the traditional dependencies.
Dependencies are created in high-level modules, and high-level modules directly depend on low-level modules.
At best, we have solved the problem of high level modules relying directly on low level modules. What about the problem of dependencies being created in high-level modules?
Inversion of control
If dependency inversion tells us who to rely on, inversion of control tells us who to control dependencies.
Like the Main module above, it relies on the Service module. To get a reference to a Service instance, Main internally new a Service instance by itself. This explicit reference to other modules increases the coupling between modules.
Inversion of Control: When an object is created, it has an external entity that controls all objects in the system, passing a reference to the object on which it depends. So to speak, dependencies are injected into objects. ———— Wikipedia
This means moving the creation and binding of dependent objects outside of the dependent object class. The most common way to implement inversion of control is dependency injection, and another way is dependency lookup.
Dependency injection
In software engineering, Dependency Injection (DI) is the implementation of inversion of control to solve Dependency design patterns. A dependency refers to an object that can be exploited (that is, the service provider). Dependency injection is the passing of dependencies to dependent objects that will be used (that is, clients). The service will become part of the client state. Passing the service to the client, rather than allowing the client to create or find the service, is the basic requirement of this design pattern.
Don’t understand? it doesn’t matter It’s about putting the process outside and bringing the results inside. We’ve already used dependency injection in Modules in JaVaScript, where a dependency is used as an argument for modules that depend on modules.
So let’s do it again,
// Service.js
class Service {
request() {
throw `The ${this.constructor.name}Request method not implemented! `}}class Ajax extends Service {
request() {
return this.constructor.name; }}export default Ajax;
// Main.js
class Main {
constructor(options) {
this.Service = options.Service;
this.render();
}
render() {
let content = this.Service.request();
console.log('content from', content); }}export default Main;
// index.js
import Service from './Service.js';
import Main from './Main.js';
new Main({
Service: new Service()
})
Copy the code
In the Main module, Service instantiation is done externally and injected in index.js. Compared to last time, the changed code does not see much benefit. What if we add another module?
class Router {
constructor() {
this.init();
}
init() {
console.log('Router::init')}}export default Router;
Copy the code
# Main.js
+ this.Service = options.Router;
# index.js
+ import Router from './Router.js'
new Main({
+ Router: new Service()
})
Copy the code
Internal instantiation is not easy to handle. This problem is easily solved by dependency injection.
// utils.js
export const toOptions = params= >
Object.entries(params).reduce((accumulator, currentValue) = > {
accumulator[currentValue[0]] = new currentValue[1] ()return accumulator;
}, {});
// Main.js
class Main {
constructor(options) {
Object.assign(this, options);
this.render();
}
render() {
let content = this.Service.request();
console.log('content from', content); }}export default Main;
// index.js
import Service from './Service.js';
import Router from './Router.js';
import Main from './Main.js';
import { toOptions } from './utils.js'
@params {Object} class @return {Object} {Service: Service instance, Router: Router instance} */
const options = toOptions({Service, Router});
new Main(options);
Copy the code
Since dependency injection introduces references to dependencies from the outside, object.assign (this, options) is used to assign all dependencies to this. Even if additional modules are added, they can only be introduced in index.js.
At this point, the concepts of DIP, IoC and DI should have a clear understanding. And then we’ll add a feature to reinforce it.
As a function independent module, there is usually an initialization process.
Now all we have to do is follow an initialization convention, define an abstract interface,
// Interface.js
export class Service {
request() {
throw `The ${this.constructor.name}Request method not implemented! `}}export class Init {
init() {
throw `The ${this.constructor.name}Init method not implemented! `}}// Service.js
import { Init, Service } from './Interface.js';
import { mix } from './utils.js'
class Ajax extends mix(Init.Service) {
constructor() {
super(a); } init() {console.log('Service::init')
}
request() {
return this.constructor.name; }}export default Ajax;
Copy the code
Main, Service, and Router all rely on the Init interface (which in this case is a protocol). The Service module is special, so it does mixins. To do this, Main needs to do a few more things.
// Main.js
import { Init } from './Interface.js'
class Main extends Init {
constructor(options) {
super(a);Object.assign(this, options);
this.options = options;
this.render();
}
init() {
(Object.values(this.options)).map(item= > item.init());
console.log('Main::init');
}
render() {
let content = this.Service.request();
console.log('content from', content); }}export default Main;
Copy the code
At this point, the end
// index.js
import Service from './Service.js';
import Router from './Router.js';
import Main from './Main.js';
import { toOptions } from './utils.js'
/** * toOptions * convert to parameter form * @params {Object} class * @return {Object} * {Service: Service instance, * Router: Router instance *} */
const options = toOptions({ Service, Router });
(new Main(options)).init();
// content from Ajax
// Service::init
// Router::init
// Main::init
Copy the code
(All examples above can be seen on GitHub)