依赖倒置、控制反转,依赖注入傻傻分不清
依赖倒置、控制反转和依赖注入都是面向对象中常见的概念,它们有一定的联系和区别,但是大家容易混淆这三个名词,搞错概念,接下来给大家讲讲他们的区别
依赖倒置
依赖倒置(Dependence Inversion Principle,DIP)一般是指SOLID原则中的D原则,是指高层模块不应该依赖底层模块,两者都应该依赖其抽象。 同时,抽象不应该依赖细节,细节应该依赖抽象。简单来说,就是依赖抽象,而不是依赖具体实现。比如Class A 依赖 Class B,这是直接依赖,如果Class A依赖 interface B,而Class B实现了interface B,这就是依赖倒置了
控制反转
控制反转(Inversion of Control,IoC)也是一种面向对象编程的设计原则,它通过反转调用方和被调用方之间的关系,实现解耦合。 比如比如Class A 依赖 Class B,在A内部实例化了B。这样两个类就耦合在一起,如果是通过外部实例化B,然后在注入到A中,这样A和B就解除了耦合
依赖注入
依赖注入(Dependency Injection,DI)则是一种实现控制反转的方式,通过将被依赖对象的创建和管理由依赖方转移到外部容器(或者框架),然后将依赖的对象注入到需要使用的地方,实现解耦合。 在依赖注入中,组件不会自己创建或管理它所依赖的对象,而是由外部的组件将依赖的对象注入到它的构造函数、属性或方法中。
例子
下面的例子使用自己实现的DI库@lujs/di,源码非常简单,只有几百行,很适合上手学习,觉得有帮助的同学记得给个star
例子1
假设我们有一个用户登录界面例子来自从零开始构建用户模块:前端开发实践,在登录界面需要这些页面状态和方法。
View State:
- loading: boolean; 页面loading
- mobile: string; 输入手机号
- code: string; 输入验证码
methods:
- showLoading
- hideLoading
- login
我们为此创建了一个PagePresenter类,而这个类依赖于UserService这个服务类,UserService是提供一个loginWithMobile,用于实现发起http请求,登录校验等功能的服务类. 代码如下:
class PagePresenter extends Presenter<IViewState> {
private userService: UserService = new UserService()
constructor() {
super();
this.state = {
loading: false,
mobile: '',
code: '',
};
}
_loadingCount = 0;
showLoading() {
if (this._loadingCount === 0) {
this.setState((s) => {
s.loading = true;
});
}
this._loadingCount += 1;
}
hideLoading() {
this._loadingCount -= 1;
if (this._loadingCount === 0) {
this.setState((s) => {
s.loading = false;
});
}
}
login = () => {
const { mobile, code } = this.state;
this.showLoading();
return this.userService
.loginWithMobile(mobile, code)
.then((res) => {
if (res) {
message.success('登录成功');
}
})
.finally(() => {
this.hideLoading();
});
};
}
export class UserService {
/**
* 手机号验证码登录
*/
loginWithMobile(mobile: string, code: string) {
// mock 请求接口登录
...
}
}
在上面的PagePresenter,我们new了一个UserService作为PagePresenter的属性。这样两个类就紧紧的耦合在一起,阻碍了以后的维护和扩展。
这时候就需要引入依赖注入技术了。我们把代码改写成下面这,不在PagePresenter中初始化UserService,而是把它当做一个参数注入进去
class PagePresenter extends Presenter<IViewState> {
constructor(private userService: UserService) {
...
使用的时候就再实例化参数,这样两个类就解耦了
const u = new UserService()
const p = new PagePresenter(u)
不过这样使用起来还是不方便,要是框架可以自动帮我初始化UserService并注入就更好了,比如像下面这样
const p = container.resolve(PagePresenter)
于是就有了一大批依赖注入框架,本人也实现了一个@lujs/di,API是参考TSyringe实现的。
使用方法如下,需要在类前面加一个装饰器injectable,表明当前类需要使用依赖注入功能
@injectable()
class PagePresenter extends Presenter<IViewState> {
constructor(private userService: UserService) {
injectable会在运行阶段找到PagePresenter所依赖的类并标记起来。 在需要初始化的时候,帮你初始化依赖的类,并自动注入到目标中
在前端中的应用
为了在前端中使用这些设计原则,我顺手实现了一个包含状态库、IOC容器的辅助库「clean-js」,可以用在react和vue中
一个简单的例子
// 具体的业务类
export class NameService {
getName() {
return Promise.resolve('name')
}
}
import { injectable, Presenter } from "@clean-js/presenter";
import { NameService } from './name.service.ts'
interface IViewState {
loading: boolean;
}
const defaultState: () => IViewState = () => {
return {
loading: false,
};
};
@injectable()
export class IndexPresenter extends Presenter<IViewState> {
constructor(public nameS: NameService) {
super();
this.state = defaultState();
}
showLoading() {
this.setState((s) => {
s.loading = true;
});
}
hideLoading() {
this.setState((s) => {
s.loading = false;
});
}
getName() {
this.showLoading()
this.names.getName().finally(() => {
this.hideLoading()
})
}
}
const Name = () => {
const { presenter, state } = usePresenter(IndexPresenter);
return (
<div>
name: {state.name}
<button onClick={() => {
presenter.getName()
}}>getName</button>
</div>
);
};
export default Name;
更多的例子
各位大佬,记得一键三连,给个star,谢谢
本文正在参加「金石计划」