
什么是依赖注入?
依赖注入
控制反转
控制反转(Inversion of Control):IoC不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序。它把传统上由程序代码直接操控的对象的调用权交给容器,通过容器来实现对象组件的装配和管理。所谓的“控制反转”概念就是对组件对象控制权的转移,从程序代码本身转移到了外部容器。
控制反转(IoC)的作用
- 降低耦合度:组件之间的依赖关系不再是硬编码的,因此可以更容易地更换组件实现,提高系统的灵活性和可维护性。
- 提高模块化:各个组件可以独立开发和测试,因为它们不依赖于具体的实现,而是依赖于抽象的接口或抽象类。
- 便于测试:由于组件之间的依赖被外部化,可以更容易地使用mock对象或测试桩来模拟依赖进行单元测试。
IoC容器
当应用程序中有多处需要用到依赖注入时,就需要一个专门的类来负责管理创建所需要的类并创建它所有可能要用到的依赖,这个类就是依赖注入容器(Dependency Injection Container),也可以称之为控制反转容器(Inversion of Control Container,IoC容器)
定义:IOC容器是一个用于管理对象生命周期和依赖关系的框架。它根据配置(如XML文件、注解或代码配置)自动创建对象,并将依赖关系注入到这些对象中。
我们可以把Ioc容器看作一个用于创建对象的工厂,它负责向外提供被请求要创建的对象,当创建这个对象时,如果它又依赖了其他对象或服务,那么容器会负责在其内部查找需要的依赖,并创建这些依赖,直至所有的依赖项都创建完成后,最终返回被请求的对象。除了创建对象和它们的依赖之外,容器也负责管理所有创建对象的生命周期。常见的依赖注入容器有Autofac和SimpleInjector等。
作用:
- 对象创建:根据配置自动创建对象实例。
- 依赖注入:将依赖关系注入到对象中,实现控制反转。
- 生命周期管理:管理对象的生命周期,包括创建、销毁和回收资源。
依赖注入
简介
依赖注入(Dependency Injection):依赖注入是实现控制反转的一种具体方式。它涉及将依赖关系(服务或对象)传递到类中,而不是让类自己创建它们。
通畅情况下,应用程序是由多个组件构成,而组件与组件之间往往存在依赖关系。依赖关系是指两个不同组件之间的引用关系,当其中一方不存在时,另一方就不能正常工作。例如,要在界面上显示一些结果,就需要调用类似如下的获取数据的组件:
public class DataService
{
public List<Book> GetAllBooks()
{
return data;
}
}
对于上述场景,通常的做法是,在需要显示数据的地方(也就是依赖这个类的位置)将DataService实例化,然后调用GetAllBooks方法获取数据,并最终显示结果。然而,这种依赖方式会增加调用方和被调用方之间的耦合,这种耦合又会增加应用程序的维护成本及灵活性。
要解决这一问题,就需要用到依赖倒置原则(Dependency Inversion Principle),这个原则指明:高层不应直接依赖低层,两者均依赖抽象(或接口)
这样我们就可以根据实际的需要去实现对应的接口,而不是写死的类。
因此,对于依赖DataService服务的地方,如果将它对DataService的依赖,替换为对接口(IDataService)的依赖,则高层(即要使用DataService服务的地方)不再直接依赖低层,而是依赖接口。此时,高层只需要关心接口,而不再需要关心具体的实现,并且高层可以根据自身的需要来设计接口,并由低层实现该接口,因此形成了“依赖倒置”。
基于此,定义一个名为IDataService的接口,并使用DataService实现此接口:
public interface IDataService
{
List<Book> GetAllBooks();
}
public class DataService:IDataService
{
public List<Book> GetAllBooks()
{
return data;
}
}
提取接口快捷键:Ctrl+R,Ctrl+I
对于调用DataService的地方,可以这样修改:
public class DisplayDataService
{
private readonly IDataService _dataService;
public DisplayDataService(IDataService dataService)
{
//_dataService一旦在构造函数中被赋值后,就不能被修改
this._dataService = dataService;
}
public void ShowData()
{
var data = _dataService.GetAllBooks();
}
}
接下来,只需要在实例化DisplayDataService类时,在它的构造函数中传入一个IDataService接口的具体实现即可
IDataService dataService = new DataService();
DisplayDataService displayDataService = new DisplayDataService(dataService);
在上述例子中,通过构造函数向DataDisplayService注入了它所需要的依赖(IDataService接口),这种注入依赖的方式称为构造函数注入,它也是最常见的方法。通过构造函数获取所需要的依赖后,就可以将它保存为类级别的全局变量,也就可以在整个类中使用。
实现方式:
- 构造函数注入:通过类的构造函数提供依赖关系。这是C#中最常见和推荐的DI形式。
- 属性注入:通过类的公共属性分配依赖关系。这种方法提供了灵活性,但可能暴露内部状态,减少封装性。
- 方法注入:通过方法参数传递依赖关系。适用于仅对特定方法需要的依赖关系进行注入。
注意:如果注入服务在类中多个方法都有用到,就用构造函数注入,如果注入服务只在一个方法中使用到,就用方法注入比较简洁。
ASP.NET Core 内置的依赖注入
在ASP.NET Core中,所有被放入依赖注入容器的类型或组件称为服务
,为了能够在程序中使用服务,首先需要向容器中添加服务,然后通过构造函数以注入的方式注入所需要的类中。
//ServiceCollection是一个集合,用于存储服务描述符,这些描述符定义了应用程序中可以被注入的服务。
IServiceCollection services = new ServiceCollection();
//添加一个ServiceDescriptor实例到服务集合中,并传递了三个参数:
services.Add(new ServiceDescriptor(typeof(IBook),typeof(Book),ServiceLifetime.Scoped));
typeof(IBook)
:这是服务的接口类型,表示这个服务描述符是关于IBook
接口的。IBook
是一个定义了书籍相关操作的接口。typeof(Book)
:这是服务的实现类型,表示IBook
接口将由Book
类来实现。Book
类实现了IBook
接口的具体类。ServiceLifetime.Scoped
:这是服务的生命周期。Scoped
生命周期意味着每个请求或每个使用该服务的客户端将获得同一个服务实例,但在不同的请求或客户端之间,服务实例是不同的。这通常用于Web应用程序中,其中每个HTTP请求需要一个服务实例,但在同一请求内重复使用相同的实例可以提高效率。
生命周期
在ASP.NET Core内置的依赖注入容器中,服务的生命周期有如下3种类型:
- Singleton(单例):整个应用程序生命周期内只创建一个实例,并且每次请求都会返回这个相同的实例。
- Transient(瞬态):每次请求都会创建一个全新的服务实例。这意味着即使在同一请求中多次请求同一个服务,也会得到不同的实例。
- Scoped(范围):范围(或称为作用域),即在某个范围(或作用域内)内,获取的始终是同一个服务实例,而不同范围(或作用域)间获取的是不同的服务实例。对于Web应用,每个请求为一个范围(或作用域)。
ServiceDescriptor
除了上述构造函数以外,还有一下两种形式:
public ServiceDescriptor(Type serviceType, object instance);
public ServiceDescriptor(Type serviceType, Func<IServiceProvider,object> factory, ServiceLifetime lifetime);
其中,第一种形式可以直接指定一个实例化的对象,使用这种方式,服务的生命周期将是Singleton,而第二种形式则是以工厂的方式来创建实例,以满足更复杂的创建要求。
除了直接调用Add方法外,IServiceCollection还提供了分别对应以上3种类型生命周期的扩展方法:AddSingleton()、AddTransient()、AddScoped()
因此,上述添加服务可以修改为这种方法:
services.AddScoped(typeof(IBook), typeof(Book));
//更简洁的方式
services.AddScoped<IBook,Book>();
依赖注入步骤
//1.创建服务集合
//存储服务描述符的集合,表示应用程序中可以被注入的服务。
ServiceCollection services = new ServiceCollection();
//2.注册容器里的服务信息
//这些方法分别对应单例(Singleton)、作用域(Scoped)和瞬态(Transient)生命周期。
services.AddSingleton<DisplayDataService>();
services.AddTransient<IDataService,DataService>();
services.AddScoped<IDataService,DataService>();
//3.构建服务提供者
//注册完服务后,你需要构建一个IServiceProvider实例,这个实例用于解析服务。
IServiceProvider serviceProvider = services.BuildServiceProvider();
//4.通过容器进行服务调用
//GetService<IDataService>() 方法会在内部查找 IServiceCollection 中注册的服务描述符,并找到匹配 IDataService 接口的第一个服务描述符。然后,根据该服务描述符中的信息(例如生命周期和工厂方法),服务容器会创建或返回一个实现了 IDataService 接口的对象。如果没有找到匹配的服务描述符,它将返回 null。
DataService dataservice= serviceProvider.GetService<IDataService>();
IServiceProvider的服务定位器方法:
- GetService
():当容器中不存在指定类型服务时,则返回null - GetRequiredService
():当容器中不存在指定类型服务时,则抛出异常 - IEnumerable
GetServices ():使用于可能有多种满足条件的服务
注意:
- 服务注册:在ASP.NET Core应用程序的启动过程中,你会通过
IServiceCollection
注册服务,每个服务注册都会创建一个ServiceDescriptor
实例,并添加到IServiceCollection
中。这些ServiceDescriptor
实例描述了服务的类型、实现和生命周期。 - 服务解析:当应用程序需要某个服务时,它会通过
IServiceProvider
接口来请求该服务的实例。IServiceProvider
会遍历IServiceCollection
中的ServiceDescriptor
实例,找到匹配的服务描述符,并根据描述符中的生命周期和工厂方法来创建服务实例。
案例理解
using System;
using Microsoft.Extensions.DependencyInjection;
namespace DIConsole
{
class Program
{
static void Main(string[] args)
{
//使用依赖注入
//创建服务集合容器
ServiceCollection services = new ServiceCollection();
//注册容器里的服务信息
services.AddScoped<Controller> ();
services.AddScoped<ILog, LogImpl>();
services.AddScoped<IConfig, ConfigImpl>();
services.AddScoped<IStorage, StorageImpl>();
//构建一个IServiceProvider实例,这个实例用于解析服务。
IServiceProvider serviceProvider = services.BuildServiceProvider ();
//通过容器进行服务调用
Controller controller = serviceProvider.GetRequiredService<Controller> ();
controller.Test();
//不使用依赖注入
/*
ILog log =new LogImpl();
IConfig config = new ConfigImpl();
IStorage storage = new StorageImpl(config);
Controller controller = new Controller(log,storage);
controller.Test();
*/
}
}
class Controller
{
//Controller类依赖于ILog和IStorage服务
private readonly ILog _log;
private readonly IStorage _storage;
public Controller(ILog log, IStorage storage)
{
_log = log;
_storage = storage;
}
public void Test()
{
_log.Log("开始上传");
_storage.Save("liuliu", "1.txt");
_log.Log("上传完毕");
}
}
//日志
interface ILog
{
void Log(string message);
}
class LogImpl:ILog
{
public void Log(string message)
{
Console.WriteLine("日志:"+message);
}
}
//配置
interface IConfig
{
string GetValue(string name);
}
class ConfigImpl:IConfig
{
public string GetValue(string name)
{
return "hello";
}
}
//云存储
interface IStorage
{
void Save(string content, string name);
}
class StorageImpl : IStorage
{
//依赖于IConfig接口来获取服务器信息
private readonly IConfig _config;
//通过构造函数注入
public StorageImpl(IConfig config)
{
_config = config;
}
public void Save(string content, string name)
{
string server= _config.GetValue("server");
Console.WriteLine($"向服务器{server}的文件名为{name}上传{content}");
}
}
}
使用依赖注入和直接使用 new
的区别
- 控制反转:
- 依赖注入:控制权从对象转移到了容器,容器负责创建和维护对象的生命周期。
- 直接 new:对象自己控制其依赖关系的创建。
- 解耦合:
- 依赖注入:对象不直接依赖于具体的实现,而是依赖于接口或抽象类,这降低了组件之间的耦合度。
- 直接 new:对象直接依赖于具体的实现,这增加了组件之间的耦合度。
- 可测试性:
- 依赖注入:可以轻松地替换依赖项为模拟对象,便于单元测试。
- 直接 new:替换依赖项较为困难,不利于单元测试。
- 生命周期管理:
- 依赖注入:容器管理对象的生命周期,可以灵活地控制对象的创建和销毁。
- 直接 new:对象的生命周期由创建它们的代码控制,通常在方法执行完毕后就会被垃圾回收。
- 代码可读性和可维护性:
- 依赖注入:代码更加简洁,依赖关系明确,易于理解和维护。
- 直接 new:代码可能更加复杂,依赖关系不明显,难以理解和维护。
分享Bromon的blog上对IoC与DI浅显易懂的讲解
1. IoC(控制反转)
首先想说说IoC(Inversion of Control,控制反转)。这是spring的核心,贯穿始终。所谓IoC,对于spring框架来说,就是由spring来负责控制对象的生命周期和对象间的关系。这是什么意思呢,举个简单的例子,我们是如何找女朋友的?常见的情况是,我们到处去看哪里有长得漂亮身材又好的mm,然后打听她们的兴趣爱好、qq号、电话号、ip号、iq号………,想办法认识她们,投其所好送其所要,然后嘿嘿……这个过程是复杂深奥的,我们必须自己设计和面对每个环节。传统的程序开发也是如此,在一个对象中,如果要使用另外的对象,就必须得到它(自己new一个,或者从JNDI中查询一个),使用完之后还要将对象销毁(比如Connection等),对象始终会和其他的接口或类藕合起来。
那么IoC是如何做的呢?有点像通过婚介找女朋友,在我和女朋友之间引入了一个第三者:婚姻介绍所。婚介管理了很多男男女女的资料,我可以向婚介提出一个列表,告诉它我想找个什么样的女朋友,比如长得像李嘉欣,身材像林熙雷,唱歌像周杰伦,速度像卡洛斯,技术像齐达内之类的,然后婚介就会按照我们的要求,提供一个mm,我们只需要去和她谈恋爱、结婚就行了。简单明了,如果婚介给我们的人选不符合要求,我们就会抛出异常。整个过程不再由我自己控制,而是有婚介这样一个类似容器的机构来控制。Spring所倡导的开发方式就是如此,所有的类都会在spring容器中登记,告诉spring你是个什么东西,你需要什么东西,然后spring会在系统运行到适当的时候,把你要的东西主动给你,同时也把你交给其他需要你的东西。所有的类的创建、销毁都由 spring来控制,也就是说控制对象生存周期的不再是引用它的对象,而是spring。对于某个具体的对象而言,以前是它控制其他对象,现在是所有对象都被spring控制,所以这叫控制反转。
2. DI(依赖注入)
IoC的一个重点是在系统运行中,动态的向某个对象提供它所需要的其他对象。这一点是通过DI(Dependency Injection,依赖注入)来实现的。比如对象A需要操作数据库,以前我们总是要在A中自己编写代码来获得一个Connection对象,有了 spring我们就只需要告诉spring,A中需要一个Connection,至于这个Connection怎么构造,何时构造,A不需要知道。在系统运行时,spring会在适当的时候制造一个Connection,然后像打针一样,注射到A当中,这样就完成了对各个对象之间关系的控制。A需要依赖 Connection才能正常运行,而这个Connection是由spring注入到A中的,依赖注入的名字就这么来的。那么DI是如何实现的呢? Java 1.3之后一个重要特征是反射(reflection),它允许程序在运行的时候动态的生成对象、执行对象的方法、改变对象的属性,spring就是通过反射来实现注入的。
- 感谢你赐予我前进的力量