# 依赖注入
# 前置知识
# 依赖倒置 DIP
面向对象六大基本原则之一。
指一种特定的解耦形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象。
该原则规定:
- 高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。
- 抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。
# 控制反转 IOC
控制反转是面向对象编程中的一种设计原则,用来降低计算机代码之间的耦合度。
是实现依赖倒置原则的一种代码设计思路。
其中最常见的方式叫做依赖注入,还有一种方式叫依赖查找。
# 依赖注入 DI
依赖注入是实现控制反转的一种方式。常用的依赖注入方法有 3 种:
- 接口注入
- 构造函数注入
- 属性注入
# 案例
常用的后端架构可以分为 3 层:
web 层接收前端请求,处理请求参数,匹配后端路由,调用对应的 service 层处理业务。
service 层接收 web 层的参数,处理业务逻辑,如果需要读取数据,会调用 database 层。
database 层处理数据库相关的层,负责连接数据库,以及常用的增删改查方法的封装。
# 非依赖注入
非依赖注入的开发模式很符合常规逻辑,即:web 层依赖 service 层,service 层依赖 database 层。
class Database {
select(sql) {
const mysql = require('mysql');
return new Promise(resolve => {
// 连接数据库,并执行 sql 语句进行查询
mysql.createConnection().query(sql, (error, results, fields) => {
const success = results.length > 0 ? true : false;
resolve(success);
});
});
}
}
class Service {
async login(username, password) {
const db = new Database();
const success = await db.select(
`select * from user where username=${username} and password=${password}`
);
return success ? '登录成功' : '登录失败';
}
}
class Web {
matchRouter(path) {
switch (path) {
case 'login':
const service = new Service();
const { username, password } = path.query;
return service.login(username, password);
}
}
}
// 使用 web 层
const web = new Web();
web.matchRouter('login');
非依赖注入开发模式的优缺点:
- 代码复杂度低,逻辑清晰,使用方便,直接 new 最上层的模块就行了。
- 不利于维护,如果底层( database 层)修改了 select 方法的传参方式,上层(service 层和 web 层)也必须同步修改调用方式。
- 不利于测试,各个模块耦合在一起,要单独测试 web 层是不行的,因为它内部引入了 service 层和 database 层,只能当成一个整体测试。
# 依赖注入
删除内部依赖关系,将需要的依赖通过构造函数注入:
class Database {
select(sql) {
const mysql = require('mysql');
return new Promise(resolve => {
// 连接数据库,并执行 sql 语句进行查询
mysql.createConnection().query(sql, (error, results, fields) => {
const success = results.length > 0 ? true : false;
resolve(success);
});
});
}
}
class Service {
constructor(db) {
this.db = db;
}
async login(username, password) {
// const db = new Database();
const success = await this.db.select(
`select * from user where username=${username} and password=${password}`
);
return success ? '登录成功' : '登录失败';
}
}
class Web {
constructor(service) {
this.service = service;
}
matchRouter(path) {
switch (path) {
case 'login':
// const service = new Service();
const { username, password } = path.query;
return this.service.login(username, password);
}
}
}
// 使用 web 层之前,必须手动创建依赖,并注入
const database = new Database();
const service = new Service(database);
const web = new Web(service);
web.matchRouter('login');
依赖注入开发模式的优缺点:
- 代码复杂度低,逻辑清晰,使用较复杂,需要手动注入依赖。
- 便于维护,代码耦合度低,各个模块互不依赖。
- 便于测试,不同模块之间可以单独的进行单元测试。
# 依赖注入容器
每一次使用都需要手动传入依赖,当依赖太多时,也会造成难以维护的问题。
可以在一个地方统一进行依赖注入,即在一个依赖注入容器里。
创建依赖注入容器:
// ioc.js
export default function createIoC() {
const iocMap = new Map();
return {
bind(key, callback) {
iocMap.set(key, { callback });
},
use(key) {
const { callback } = iocMap.get(key);
return callback();
}
};
}
在配置文件中配置依赖关系:
// ioc-config.js
import createIoC from 'ioc.js';
const ioc = createIoC();
// 绑定依赖关系
ioc.bind('Database', () => {
return new Database();
});
ioc.bind('Service', () => {
const database = ioc.use('Database');
return new Service(database);
});
ioc.bind('Web', () => {
const service = ioc.use('Service');
return new Web(service);
});
export default ioc;
依赖注入:
import ioc from 'ioc-config.js';
// 使用 web 层
const web = ioc.use('Web');
web.matchRouter('login');
- 代码复杂度较高,逻辑较复杂,使用方便,要什么就注入什么就行了。
- 便于维护,只需在一个地方(ioc-config.js)定义依赖关系,这个文件可以抽离出来作为单独的配置文件,实现数据驱动。
- 便于测试,不同模块之间可以单独的进行单元测试。
引用: