P3-1 依赖注入

依赖注入


什么是控制反转、服务定位器和依赖注入

控制反转

  • 生活中的“控制反转”:自己发电和用电网的电。
  • 依赖注入(Dependency Injection,DI)是控制反转(Inversion of Control,IOC)思想的实现方式。
  • 依赖注入简化模块的组装过程,降低模块之间的耦合度
1
2
3
4
5
6
7
自己发电的代码

var connSettings = ConfigurationManager.ConnectionStrings["connStr1"];
string connStr = connSettings.ConnectionString;
SqlConnection conn = new SqlConnection(connStr);
缺点是?
//缺点是:你需要先去配置文件Web.config读取数据库连接字符串,然后创建新对象去接收这个值,然后去连接数据库

代码控制反转的目的

“怎样创建XX对象”→“我要XX对象”

两种实现方式:

  1. 服务定位器(ServiceLocator)
    1. 需要先找服务定位器去要,然后它给你,你在拿去使用(半自动化)
  2. 推荐:依赖注入(Dependency Injection,DI) ⭐️⭐️⭐️
    1. 自己声明一下,编译器会自动的给你声明的属性赋值(自动化)
1
2
3
4
5
6
7
8
9
10
11
12
13
//畅想Demo 下面都是自己想象的,不是.NET 有的
服务定位器
IDbConnection conn = ServiceLocator.GetService<IDbConnection>();

依赖注入
class Demo
{
public IDbConnection Conn { get; set; }
public void InsertDB()
{
IDbCommand cmd = Conn.CreateCommand();
}
}

.NET Core依赖注入的基本使用

DI的几个概念

  • 服务(srevice):对象
    • 当你向框架要一个对象的时候,它就会给你提供一个服务
  • 注册服务
    • 把服务注册进去,你才能拿到这个服务
  • 服务容器:负责管理注册的服务
  • 查询服务:创建对象及关联对象
  • 对象的生命周期

    获取服务的时候是创建一个新对象还是用之前的对象

    • 瞬态(Transient)
      • 每次被请求的时候都会创建一个新对象
    • 范围(Scoped)
      • 在给定的范围内,可以多次请求同一个服务对象,服务每次返回的对象都是同一个
    • 单例(Singleton)
      • 全局共享同一个服务对象

.NET 中使用DI

1、测试代码见备注
2、根据类型来获取和注册服务。
可以分别指定服务类型(service type)和实现类型(implementation type)。这两者可能相同,也可能不同。服务类型可以是类,也可以是接口,建议面向接口编程,更灵活。
3、.NET控制反转组件取名为DependencyInjection,但它包含ServiceLocator的功能。

.NET 中使用DI 2

  • 1.通过NuGet安装对应开发包
    • Install-Package Microsoft.Extensions.DependencyInjection
  • 2.using Microsoft.Extensions.DependencyInjection
  • 3.ServiceCollection用来构造容器对象IServiceProvider 。调用ServiceCollection的BuildServiceProvider()创建的ServiceProvider,可以用来获取BuildServiceProvider()之前ServiceCollection中的对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Program
{
static void Main(string[] args)
{
ServiceCollection services = new ServiceCollection();
services.AddTransient<TestServiceImpl>();//瞬态服务
using(ServiceProvider sp =services.BuildServiceProvider())
//ServiceProvider == 服务定位器
{
TestServiceImpl testService = sp.GetRequiredService<TestServiceImpl>();
testService.Name = "Tom";
testService.SayHi();
}
}
}
public interface ITestService//接口
{
public string Name { get; set; }
public void SayHi();

}
public class TestServiceImpl:ITestService//实现类
{
public string Name { get; set; }
public void SayHi()
{
Console.WriteLine($"Hi,I'm {Name}");
}

}

生命周期

  • 1、给类构造函数中打印,看看不同生命周期的对象创建,使用serviceProvider.CreateScope()创建Scope

  • 2、如果一个类实现了IDisposable接口,则离开作用域之后容器会自动调用对象的Dispose方法

    • Disposable
  • 3、不要在长生命周期的对象中引用比它短的生命周期的对象。在ASP.NET Core中,这样做的话默认会抛异常

  • 生命周期的选择

    • 如果类无状态,建议为Singleton
      • 无状态
        • 类中没有成员变量和属性
    • 如果类有状态,且有Scope控制,建议为Scoped,因为通常这种Scope控制下的代码都是运行在同一个线程中的,没有并发修改的问题
    • 在使用Transient的时候要谨慎(Transient使用一般很少,因为很容易用了出现内存泄漏)
  • 5、.NET注册服务的重载方法很多,看着文档琢磨吧

  • 比较

    • 瞬态
      • 瞬态
    • 范围
      • scope
      • 不同范围的scope
    • 单例
      • 单例

其他注册方法

  • 服务类型和实现类型不一致的注册
    • 服务类型:接口
    • 实现类型:实现类
  • 其他的Add方法
1
2
3
4
//services.AddScoped<ITestService, TestServiceImpl>();
//services.AddScoped(typeof (ITestService), typeof (TestServiceImpl));
//services.AddSingleton<ITestService, TestServiceImpl>();
services.AddSingleton(typeof(ITestService), new TestServiceImpl());

服务定位器

IServiceProvider的服务定位器方法:

  • T GetService() 如果获取不到对象,则返回null。
    • object GetService(Type serviceType) —非泛型方法
  • T GetRequiredService()如果获取不到对象,则抛异常
    • object GetRequiredService(Type serviceType) —非泛型方法
  • IEnumerable GetServices()适用于可能有很多满足条件的服务
    • IEnumerable GetServices(Type serviceType) —非泛型方法
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      //T GetService<T>() 如果获取不到对象,则返回null。
      ITestService ts1 = sp.GetService<ITestService>();
      //ITestService ts1 = (ITestService)sp.GetService(typeof(ITestService));//非泛型

      //T GetRequiredService<T>()如果获取不到对象,则抛异常
      //Required:必须的。如果找不到,直接抛异常
      //类似于显示类型转换和as
      ITestService ts2 = sp.GetRequiredService<ITestService>();
      ts1.Name = "zane";
      ts1.SayHi();
      Console.WriteLine(ts1.GetType());


      //IEnumerable<T> GetServices<T>()适用于可能有很多满足条件的服务
      IEnumerable<ITestService> tests = sp.GetServices<ITestService>();
      foreach (ITestService t in tests)
      {
      Console.WriteLine(t.GetType());
      }

      依赖注入的魅力所在

      • 1、依赖注入是有“传染性”的,如果一个类的对象是通过DI创建的,那么这个类的构造方法中声明的所有服务类型的参数都会被DI赋值;
        • 但是如果一个对象是由开发人员手动创建的,那么这个对象就和DI没有关系,它的构造方法中声明的服务类型参数就不会被自动赋值。
        • 一旦使用了DI,就要尽量避免直接通过new关键字来创建对象
        • 好处:业务逻辑层的开发人员不用管数据访问类的代码,只需要注册服务去使用即可。不需要知道数据访问类的逻辑与代码。数据访问类就是框架,自动根据你声明的类型去赋值
      • 2、.NET的DI默认是构造函数注入。
      • 3、案例:编写一个类,连接数据库做插入操作,记录日志(模拟的输出),把Dao、日志都放入单独的服务类
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      namespace DI依赖注入1
      {
      class Program
      {
      static void Main(string[] args)
      {
      ServiceCollection services = new ServiceCollection();
      services.AddScoped<Controller>();
      services.AddScoped<ILog,LogImpl>();
      services.AddScoped<IStorage,StorageImpl>();
      //services.AddScoped<IConfig,ConfigImpl>();//注册服务
      services.AddScoped<IConfig,DBConfigImpl>();
      //所有用到的服务都写到services.BuildServiceProvider()之前
      using (var sp =services.BuildServiceProvider())
      {
      var c = sp.GetRequiredService<Controller>();
      c.Test();
      }
      Console.ReadKey();
      }
      }
      class Controller
      {
      private readonly ILog log;
      private readonly IStorage storage;
      public Controller(ILog log, IStorage storage)
      {
      this.log = log;
      this.storage = storage;
      }
      public void Test()
      {
      this.log.Log("开始上传");
      this.storage.Sava("asdasdas", "1.txt");
      this.log.Log("上传完毕");
      }
      }
      interface ILog
      {
      public void Log(string msg);
      }
      class LogImpl : ILog
      {
      public void Log(string msg)
      {
      Console.WriteLine($"日志:{msg}");
      }
      }
      interface IConfig
      {
      public string GetValue(string name);
      }
      class ConfigImpl : IConfig
      {
      public string GetValue(string name)
      {
      return "Hello";
      }
      }

      class DBConfigImpl : IConfig
      {
      public string GetValue(string name)
      {
      Console.WriteLine("从数据库中读取");
      return "Hello db";
      }
      }
      interface IStorage
      {
      public void Sava(string content, string name);
      }
      class StorageImpl : IStorage
      {
      //DI:降低模块之间的耦合
      private readonly IConfig config;//取配置文件中的数据
      public StorageImpl(IConfig config)//注入:IConfig config | StorageImpl==构造函数
      {
      this.config = config;
      }
      public void Sava(string content, string name)
      {
      string server = config.GetValue("server");//去配置文件夹中的服务器
      Console.WriteLine($"向{server}服务器上传文件名为{name},文件内容为:{content}");
      }
      }


      }

      案例

      需求说明

      1、目的:演示DI的能力;
      2、有配置服务、日志服务,然后再开发一个邮件发送器服务。可以通过配置服务来从文件、环境变量、数据库等地方读取配置,可以通过日志服务来将程序运行过程中的日志信息写入文件、控制台、数据库等。
      3、说明:案例中开发了自己的日志、配置等接口,这只是在揭示原理,.NET有现成的,后面讲。

      实现1

      • 1、创建四个.NET Core类库项目,ConfigServices是配置服务的项目,LogServices是日志服务的项目,MailServices是邮件发送器的项目,然后再建一个.NET Core控制台项目MailServicesConsole来调用MailServices。MailServices项目引用ConfigServices项目和LogServices项目,而MailServicesConsole项目引用MailServices项目。
      • 2、编写类库项目LogServices,创建ILogProvider接口。编写实现类ConsoleLogProvider。编写一个ConsoleLogProviderExtensions定义扩展方法AddConsoleLog,namespace和IServiceCollection 一致。

        代码写在了“ConfigServices”这个项目里

      实现2

      • 1、编写配置服务的类库项目ConfigServices。接口IConfigProvider,方法:string GetValue(string name)。

      • 2、环境变量读取配置类EnvVarConfigProvider:Environment.GetEnvironmentVariable(name);编写一个类带扩展方法:AddEnvVarConfig

      • 3、编写从ini文件中读取配置的类ConfigServices。

      • 注意点

        • ①读取本地的配置文件,需要修改为“如果较新则复制”
          • 配置文件
        • ②简化注册服务的方法
          • 扩展方法
            • 需求
          • 1.名称空间名称修改为“Microsoft.Extensions.DependencyInjection”, 这样可以免去using引用
          • 2.方法参数中需要加 this,要不然“.”调用时出不来
          • 3.扩展方法后,其实有的实现类就可以不写public了
            • 注意点
            • 扩展

      实现3

      • 1、“可覆盖的配置读取器”。配置中心服务器。可以本地的覆盖配置服务器的,或者配置文件覆盖环境变量的。
        例如,按照“配置中心服务器”、“本地环境变量”、“本地配置文件”的顺序添加了三个配置提供者,在“配置中心服务器”中提供了“a=1;b=2;c=3”这三个配置项,在“本地环境变量”中配置了“a=10;b=20;”,在“本地配置文件”中配置了“b=200”,那么最终我们读取的时候读到的就是“a=10;b=200;c=3;”
      • 2、定义一个从各个ConfigProvider中读取项的IConfigReader接口。编写实现类LayeredConfigReader 。

      实现4

      • 1、编写发送邮件的服务。IMailSender接口,实现类DefaultMailSender。注入IConfigReader ,ILogProvider ,不真的发邮件,想真的发邮件用MailKit。
      • 2、整合、调试。

      总结

      • 关注于接口,而不是关注于实现,各个服务可以更弱耦合的协同工作。在编写代码的时候,我们甚至都不知道具体的服务是什么。

      • 第三方DI容器:Autofac等。Autofac优点:支持属性注入、基于名字注入、基于约定的注入等。

      • 奥卡姆剃刀原理:如无必要,勿增实体

        • 图片1

      P3-1 依赖注入
      http://example.com/2024/09/10/Net Core2022教程/第3章:.NET Core核心基础组件/P3-1 依赖注入/
      Author
      John Doe
      Posted on
      September 10, 2024
      Licensed under