控制反转与依赖注入,看这篇“应该”够了
一、背景
在学习.Net Core 的过程中遇到了两个概念:控制反转和依赖注入。接下来我们就围绕以下几个主题来唠一唠:
- 为什么要使用依赖注入和控制反转
- 什么是控制反转、依赖注入
- 怎么使用依赖注入(Ioc Container 的基础使用)
二、控制反转与依赖注入
2.1 为什么要使用依赖注入和控制反转?
先从一个简单的示例开始
Model
namespace IocAndDIExample.Models
{
public class Mbiz
{
public Int32 BizId { get; set; }
public String BizName { get; set; }
public String Category { get; set; }
}
}
Repository
namespace IocAndDIExample.Example01
{
public class MBizRepository
{
public List<Mbiz> MockData()
{
return new List<Mbiz>()
{
new Mbiz(){ BizId = 1 ,BizName = "占豪", Category = "资讯"},
new Mbiz(){ BizId = 2 ,BizName = "共青团中央", Category = "资讯"},
new Mbiz(){ BizId = 3 ,BizName = "科技美学", Category = "数码"},
new Mbiz(){ BizId = 4 ,BizName = "小米", Category = "数码"},
new Mbiz(){ BizId = 5 ,BizName = "中公教育", Category = "教育"}
};
}
public Mbiz GetMBizById(int bizId)
{
return MockData().FirstOrDefault(e => e.BizId == bizId);
}
public List<Mbiz> GetAllMBizs()
{
return MockData();
}
}
}
Service
namespace IocAndDIExample.Example01_耦合的类
{
/// <summary>
/// 耦合的类
/// </summary>
public class CoupledService
{
private MbizRepository mbizRepository => new MbizRepository();
public List<Mbiz> GetMbizs()
{
return mbizRepository.GetAllMBizs();
}
public Mbiz GetMbiz(int id)
{
return mbizRepository.GetMBizById(id);
}
}
}
在面向对象编程的思想里,类与类之间应该尽可能保持松耦合的状态。松耦合意味着改变其中一个类不会导致另一个类的改变,让整个程序变得可扩展和可维护。
在上面的例子中,CoupledService 和 MBizRepository 是紧密耦合的类,因为 CoupledService 类包含了具体 MBizRepository 类的引用。它还创建 MBizRepository 类的对象,并管理对象的生命周期。这样会导致以下几个问题:
- 改变 MBizRepository 的类名,或者改变其中的方法名,都需要对 CoupledService 进行修改
- 如果 CoupledService 是这样调用 MBizRepository,那么肯定还有若干个 xxxService 去调用 MBizRepository,那么当 MBizRepository 修改的时候就要对这些地方进行同步修改,就做了很多重复的工作。
- 不能够这个 CoupledService 进行单独测试,因为他依赖着 MBizRepository,这个类不能被模拟数据替换。
而这就是为什么要使用控制反转和依赖注入的原因,让我们开始一步步改善这个示例。
2.2 什么是控制反转和依赖倒置原则
控制反转
先对上面的例子做一个小小的变动
public class RepositoryFactory
{
public static MbizRepository GetMBizRepository()
{
return new MbizRepository();
}
}
public class CoupledService
{
private MbizRepository mbizRepository => RepositoryFactory.GetMBizRepository();
...
}
这个小小的改动就是简单使用到了控制反转法则中的工厂模式。
定义
控制反转(Inversion of Control,IoC)是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体(RepositoryFactory)将其所依赖的对象的引用(mbizRepository )传递给它。也可以说,依赖被注入到对象中。
依赖倒置原则
SOLID object-oriented principle --<u>Robert Martin</u> (a.k.a. Uncle Bob)
1. 单一职责(SRP: Single Responsibility Principle):一个类应该只有一个发生变化的原因
2. 开闭原则(OCP: Open Closed Principle):一个软件实体应当对扩展开放,对修改关闭
3. 里氏替换原则(LSP: Liskov Substitution Principle):所有引用基类(父类)的地方必须能透明地使用其子类的对象
4. 依赖倒置原则(DIP: Dependence Inversion Principle):抽象不应该依赖于细节,细节应当依赖于抽象
5. 接口隔离原则(ISP: Interface Segregation Principle):使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口
6. 迪米特原则(LoD: Law of Demeter):一个软件实体应当尽可能少地与其他实体发生相互作用
定义
依赖倒置原则(DIP: Dependence Inversion Principle)是面向对象编程 SOLID 法则中的一条,内容是
- High-level modules should not depend on low-level modules. Both should depend on abstraction.
- Abstractions should not depend on details. Details should depend on abstractions 。
回过头来看看我们上面的例子,注意到虽然我们把 MbizRepository 的生成实例的控制权交给了 RepositoryFactory,但是在 CoupledService 中,我们仍然还是用到了具体的 MbizRepository。因此我们仍然没有摆脱紧耦合的状况。让我们用 DIP 对这个示例进一步改造。
用依赖倒置原则改造示例
首先根据第一点,高层模块(CoupledService)不应该依赖底层模块(MbizRepository),它们都应该依赖抽象(接口或者抽象类)。先实现一个接口:
public interface IMbiz
{
Mbiz GetMBizById(int bizId);
List<Mbiz> GetAllMBizs();
}
然后修改 MbizRepository
public class MbizRepository:IMbiz
{
public List<Mbiz> MockData()
{
return new List<Mbiz>()
{
...
};
}
public Mbiz GetMBizById(int bizId)
{
return MockData().FirstOrDefault(e => e.BizId == bizId);
}
public List<Mbiz> GetAllMBizs()
{
return MockData();
}
}
Service
public class DIPService
{
private **IMbiz **mbizRepository => RepositoryFactory.GetMBizRepository();
public List<Mbiz> GetAllMBizs()
{
return mbizRepository.GetAllMBizs();
}
public Mbiz GetMBizById(int id)
{
return mbizRepository.GetMBizById(id);
}
}
到了这里,我们已经通过 DIP 实现了这样一个例子:高层模块(DIPService)不依赖底层模块(MbizRepository),而是依赖于抽象(IMbiz) ;抽象(IMbiz)不依赖于细节(MbizRepository),即如果细节(MbizRepository)内部发生变化,都不影响(IMbiz);细节(MbizRepository)取决于抽象(IMbiz),即 MbizRepository 的实现应该符合 IMbiz 声明。
上述例子中使用 DIP 的优点在于:
DIPService 只依赖于 IMbiz 接口和 RepositoryFactory,这让 DIPService 和 MbizRepository 的耦合变成了松耦合的状态。我们可以用任何一个实现 IMbiz 的类去替换 MBizRepository。
可是,这个示例仍然存在一个问题:DIPService 依赖于 IMbiz 接口和 RepositoryFactory。想要进一步对这个例子优化的话,就需要依赖注入的帮助了。
2.3 什么是依赖注入
定义
依赖注入(DI)是一种用于实现 IoC 的设计模式。它允许在类之外创建依赖对象,并通过不同的方式将这些对象提供给类。使用 DI,我们可以将依赖对象的创建和绑定移到依赖它们的类之外。
依赖注入模式包含 3 种类型的类:
- Client Class: 依赖于 service class 的类
- Service Class: 提供 service 给 client class 的类
- Injector Class: 注入 service 对象给 client class 的类.
他们的关系如下图:
Injector 类创建了 Service 类的一个对象,并将该对象注入到 Client 对象中。通过这种方式,DI 模式将创建 Service 对象的职责从 Client 类中分离出来。即:由 Injector 类来管理 Service 的创建和注入。
依赖注入的三种类型
- 构造函数注入(Constructor Injection)
- 属性注入(Property Injection)
- 方法注入(Method Injection)
接下来,让我们分别用这三种不同的方式来改造我们的示例:
构造函数注入
当我们通过构造函数提供依赖时,这被称为构造函数注入。
MbizService
public class MbizService
{
IMbiz _mbizRepository;
public MbizService(IMbiz mbizRepository)
{
_mbizRepository = mbizRepository;
}
public List<Mbiz> GetAllMBizs()
{
return _mbizRepository.GetAllMBizs();
}
public Mbiz GetMBizById(int id)
{
return _mbizRepository.GetMBizById(id);
}
}
InjectorService
public class InjectorService
{
MbizService _mbizService;
public InjectorService()
{
_mbizService = new MbizService(new MbizRepository()); //创建并注入依赖
}
public List<Mbiz> GetAllMBizs()
{
return _mbizService.GetAllMBizs();
}
}
上面的示例里, InjectorService
创建并注入 MbizRepository
对象到 MbizService
中。 因此,MbizService
不用 new 或者用 factory 来创建 MbizRepository
。
属性注入
在属性注入中,依赖是通过一个公共属性提供的
同样地,请看代码:
public class MbizService
{
public IMbiz MbizRepository { get; set; }
public List<Mbiz> GetAllMBizs()
{
return MbizRepository.GetAllMBizs();
}
}
public class InjectorService
{
MbizService _mbizService;
public InjectorService()
{
_mbizService = new MbizService();
_mbizService.MbizRepository = new MbizRepository();
}
public List<Mbiz> GetAllMBizs()
{
return _mbizService.GetAllMBizs();
}
}
MbizService
类内声明了一个属性:MbizRepository
, 用来注入实现了 IMbiz
的类 。 而 InjectorService
通过这个 MbizRepository
属性,将依赖注入到 MbizService
中。
方法注入
在方法注入中,依赖是通过方法提供的。此方法可以是类方法或接口方法。
public interface IInjectProvider
{
void SetDependency(IMbiz mbizRepository);
}
public class MbizService : IInjectProvider
{
IMbiz _mbizRepository;
public void SetDependency(IMbiz mbizRepository)
{
_mbizRepository = mbizRepository;
}
public List<Mbiz> GetAllMBizs()
{
return _mbizRepository.GetAllMBizs();
}
}
public class InjectorService
{
MbizService _mbizService;
public InjectorService()
{
_mbizService = new MbizService();
((IInjectProvider)_mbizService).SetDependency(new MbizRepository());
}
public List<Mbiz> GetAllMBizs()
{
return _mbizService.GetAllMBizs();
}
}
MbizService
继承了 IInjectProvider
接口,实了 SetDependency()
方法。InjectorService
使用 SetDependency()
方法将依赖注入 MbizService
中。
三种注入方式的比较
三、IoC 容器
介绍
在实际代码中,我们更多的是使用 IoC 容器来替我们实现依赖注入的过程,也就是这个下图的部分:
IoC Container(又名 DI Container)是一个用于实现自动依赖项注入的框架。它管理对象的创建和生命周期,并向类注入依赖项。
所有的容器都必须为 DI 生命周期提供简单的支持,包括以下部分:
- 注册 Register: 当遇到特定类型时,容器必须知道实例化哪个依赖项。这个过程称为注册。基本上,它必须包含某种注册类型映射的方法。The container must know which dependency to instantiate when it encounters a particular type. This process is called registration. Basically, it must include some way to register type-mapping.
- 解析 Resolve: 在使用 IoC 容器时,我们不需要手动创建对象。容器可以帮我们完成这一个过程。这被称为解析。容器必须包含一些方法来解析指定的类型;容器创建指定类型的对象,如果有必要的依赖项,则注入该对象并返回该对象。When using the IoC container, we don't need to create objects manually. The container does it for us. This is called resolution. The container must include some methods to resolve the specified type; the container creates an object of the specified type, injects the required dependencies if any and returns the object.
- 销毁 Dispose: 容器必须管理依赖对象的生存期。大多数 IoC 容器包括不同的生命周期管理器来管理对象的生命周期并处理它。The container must manage the lifetime of the dependent objects. Most IoC containers include different lifetimemanagers to manage an object's lifecycle and dispose of it.
下面是市场上比较常见的一些开源容器:
Get Started with IoC Container using Autofac
我们就以比较热门的 AutoFac 为例,了解一下怎么使用 IoC 容器。
使用 AutoFac 的过程大概是这样的:
- Add Autofac references.
- Create a ContainerBuilder.
- Register components.
- Build the container and store it for later use.
- During application execution…
- Create a lifetime scope from the container.
- Use the lifetime scope to resolve instances of the components.
再次改造我们之前的例子。现在,它只需要短短几行代码就能完成依赖注入。
public void MbizService()
{
var builder = new ContainerBuilder(); //Create a ContainerBuilder.
builder.RegisterType<MbizRepository>().As<IMbiz>(); //Register components.
IContainer Container = builder.Build(); //Build the container and store it for later use
using (var scope = Container.BeginLifetimeScope()) //Create a sub-lifetime scope from the container
{
var mbiz = scope.Resolve<IMbiz>();
mbiz.GetAllMBizs();
} //Use the lifetime scope to resolve instances of the components.
}
ContainerBuilder Class (autofac.org) 相当于容器的配置
RegistrationExtensions.RegisterType(TImplementer) Method (ContainerBuilder) (autofac.org)
ContainerProvider Constructor (IContainer) (autofac.org)
ContainerProvider Class (autofac.org)
Container.BeginLifetimeScope Method (autofac.org)
组件的生命周期
Understanding the concept of Lifetime scope in Autofac - Stack Overflow
四、总结
最后我们再回顾总结一下上面提及到的内容:
- 这些概念你还记得是什么意思吗:IoC DI DIP IoC Container
- 它们是什么关系:请看下图
- 依赖注入到底是啥
- 有什么方式可以依赖注入
- 动动手尝试使用 AutoFac 来简单进行依赖注入 Getting Started — Autofac 6.0.0 documentation* *
一些感想:
学习了解这些概念不会直接进人你的代码中,而是先进人你的“大脑"中。 —旦你先在脑海中装人了许多关于模式的知识,就能够开始在新设计中采用它们 。
了解了依赖注入后,对.NET Core 的优点有所体会:轻量、可扩展