控制反转、服务定位器、依赖注入


  控制反转:使用对象或者服务的时候,不需要自己去创建/new服务,而是在使用的时候直接声明,容器会自动分配一个服务实例。相当于自己用发电机发电使用和利用电网公司的电的区别,自己发电,我需要一台发电机,安装发电机,自己设置电压,频率等等,而使用电网公司的只需要花钱,就能使用;(自己创建和问别人要的区别)

  • 依赖注入(Dependency Injection,DI)是控制反转(Inversion Of Control,IOC)思想的一种实现方式
  • 依赖注入简化模块的组装过程,降低模块之间的耦合度
  • 实现控制反转的两种方式:1.服务定位器,2.依赖注入
//1.自己发电的方式
var connSettings = ConfigurationManager.ConnectionStrings["connStr"];
string connStr = connSettings.ConnectionString;
SqlConnection conn = new SqlConnection(connStr);

//2.服务定位器实现控制反转(伪代码)
IDbConnection conn = ServiceLocator.GetService<IDbConnection>();

//3.依赖注入实现控制反转(伪代码)
class Demo
{
    //只需要声明,表示我需要一个数据库连接的实例对象
    public IDbConnection Conn {get;set;}
    public void InsertDB()
    {
        IDbCommand command = Conn.CreateCommand();
    }
}
  • Dependency Injection的几个概念
  1. 服务(service):对象,用来使用的
  2. 注册服务:将服务注册到相应的容器中
  3. 服务容器:负责管理注册的服务
  4. 查询服务:创建对象及关联对象
  5. 对象生命周期:Transient(瞬态)、Scoped(范围)、Singletion(单例)
namespace DependencyInjectionDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            //新建服务容器
            ServiceCollection services = new ServiceCollection();
            //将服务注册到容器中
            services.AddTransient<TestServiceImpl1>();
            //再从服务容器中获取指定的服务来使用
            using (ServiceProvider provider= services.BuildServiceProvider())
            {
                TestServiceImpl1 serviceImpl1 = provider.GetService<TestServiceImpl1>();
                serviceImpl1.Name = "Tim Duncan";
                serviceImpl1.sayHi();
            }
        }
    }



    interface ITestService {
        public string Name { get; set; }

        public void sayHi();
        
    }

    class TestServiceImpl1:ITestService
    {
        public string Name { get; set; }
        public void sayHi()
        {
            Console.WriteLine("hello,my name is "+ Name);
        }
    }

    class TestServiceImpl2 : ITestService
    {
        public string Name { get; set; }
        public void sayHi()
        {
            Console.WriteLine("你好,我的名字是 " + Name);
        }
    }
}
  • 服务的生命周期
  • 1.Service.AddTransient<T>,创建一个瞬时的对象,每次创建都是一个新的对象服务;
        static void Main(string[] args)
        {
            //新建服务容器
            ServiceCollection services = new ServiceCollection();
            //将服务注册到容器中(瞬态注册)
            services.AddTransient<TestServiceImpl1>();
            //再从服务容器中获取指定的服务来使用
            using (ServiceProvider provider= services.BuildServiceProvider())
            {

                TestServiceImpl1 serviceImpl1 = provider.GetService<TestServiceImpl1>();
                serviceImpl1.Name = "james";
                serviceImpl1.sayHi();

                TestServiceImpl1 serviceImpl2 = provider.GetService<TestServiceImpl1>();
                serviceImpl2.Name = "kobe";
                serviceImpl2.sayHi();
                //比较两个对象是否相同
                Console.WriteLine(object.ReferenceEquals(serviceImpl1, serviceImpl2));
                //再次输出serviceImpl1
                serviceImpl1.sayHi();
            }
        }

         可以看到,两个服务的是不相同的,并且再次调用第一个服务的方法,服务里面的值也不会改变,这个就是瞬时获取,每个服务实例都不一样;

          2.Service.Singletion<T>,单例模式,每次拿到都都是同一个对象实例
        static void Main(string[] args)
        {
            //新建服务容器
            ServiceCollection services = new ServiceCollection();
            //将服务注册到容器中(单例模式注册)
            services.AddSingleton<TestServiceImpl1>();
            //再从服务容器中获取指定的服务来使用
            using (ServiceProvider provider= services.BuildServiceProvider())
            {

                TestServiceImpl1 serviceImpl1 = provider.GetService<TestServiceImpl1>();
                serviceImpl1.Name = "james";
                serviceImpl1.sayHi();

                TestServiceImpl1 serviceImpl2 = provider.GetService<TestServiceImpl1>();
                serviceImpl2.Name = "kobe";
                serviceImpl2.sayHi();
                //比较两个对象是否相同
                Console.WriteLine(object.ReferenceEquals(serviceImpl1, serviceImpl2));
                //再次输出serviceImpl1
                serviceImpl1.sayHi();
            }
        }

        可以看到,这个时候两个对象是一样的,并且第一个对象经过第二个对象赋值之后,再重新打印,属性值已经变为第二次赋值的kobe了,这就是单例模式,永远都是一个对象,每次赋值都会把之前的覆盖掉,这种适合于创建不需要状态的服务对象,比如一些辅助帮助类等。

  • 3.AddScoped<T>,是指对一定范围内的代码有作用,出了这个范围,服务自动被释放,调用对象的IDispose方法
        static void Main(string[] args)
        {
            //新建服务容器
            ServiceCollection services = new ServiceCollection();
            //将服务注册到容器中(范围模式注册)
            services.AddScoped<TestServiceImpl1>();
            //再从服务容器中获取指定的服务来使用
            using (ServiceProvider provider= services.BuildServiceProvider())
            {
                //作用域范围
                using (IServiceScope scope1 = provider.CreateScope())
                {
                    var serviceImpl1 =  scope1.ServiceProvider.GetService<TestServiceImpl1>();
                    serviceImpl1.Name = "james";
                    serviceImpl1.sayHi();

                    var serviceImpl2 = scope1.ServiceProvider.GetService<TestServiceImpl1>();
                    serviceImpl1.Name = "kobe";
                    serviceImpl1.sayHi();
                    //比较两个对象是否相同
                    Console.WriteLine(object.ReferenceEquals(serviceImpl1, serviceImpl2));
                    //再次输出serviceImpl1
                    serviceImpl1.sayHi();
                }
            }
        }

可以看到,在同一个using范围内,Scope模式获取的两个服务对象是一样的 ,类似于上面单例模式

下面看另外一种情况,在不同作用域中定义的对象进行比较

        static void Main(string[] args)
        {
            //新建服务容器
            ServiceCollection services = new ServiceCollection();
            //将服务注册到容器中(作用域范围模式注册)
            services.AddScoped<TestServiceImpl1>();
            //再从服务容器中获取指定的服务来使用
            using (ServiceProvider provider= services.BuildServiceProvider())
            { 
                TestServiceImpl1 outSideService;
                //作用域范围1
                using (IServiceScope scope1 = provider.CreateScope())
                {
                    var serviceImpl1 =  scope1.ServiceProvider.GetService<TestServiceImpl1>();
                    serviceImpl1.Name = "james";
                    serviceImpl1.sayHi();
                    outSideService = serviceImpl1;
                }
                //作用域范围2
                using (IServiceScope scope2 = provider.CreateScope())
                {
                    var serviceImpl1 = scope2.ServiceProvider.GetService<TestServiceImpl1>();
                    serviceImpl1.Name = "james";
                    serviceImpl1.sayHi();
                    //比较外部服务对象和内部服务对象
                    Console.WriteLine(object.ReferenceEquals(outSideService, serviceImpl1));
                }
            }
        }

 

  • 4.需要注意的点
  1. 不要再长生命周期的对象中引用比它短的生命周期的对象,这样会默认抛异常
  2. 生命周期的选择:如果类无状态,没有成员属性变量,就是简单的一些处理逻辑返回值等,定义为Singletion;如果有状态,且有Scope控制,建议为Scoped,因为通常这种Scope控制下代码都是运行在同一个线程中的,并没有并发修改的问题;在使用Transient的时候需要谨慎
其他注册方法

  • 我们一般把服务定义为接口,要用的对象为接口的实现类,如下:TestServiceImpl2为接口ITestService的实现类
{
    class Program
    {
        static void Main(string[] args)
        {
            ServiceCollection services = new ServiceCollection();
            services.AddScoped<ITestService, TestServiceImpl2>();

            using (ServiceProvider sp = services.BuildServiceProvider())
            {
                ITestService implService = sp.GetService<ITestService>();
                implService.Name = "John";
                implService.sayHi();
                Console.WriteLine(implService.GetType());
            }
        }
    }
    interface ITestService {
        public string Name { get; set; }

        public void sayHi();
        
    }

    class TestServiceImpl1:ITestService
    {
        public string Name { get; set; }
        public void sayHi()
        {
            Console.WriteLine("hello,my name is "+ Name);
        }
    }

    class TestServiceImpl2 : ITestService
    {
        public string Name { get; set; }
        public void sayHi()
        {
            Console.WriteLine("你好,我的名字是 " + Name);
        }
    }
}

运行结果为:

  • 在provider.GetService<T>的方法里我们也可以使用typeof(T)的方式来获取服务对象:如下:
        static void Main(string[] args)
        {
            ServiceCollection services = new ServiceCollection();
            services.AddScoped<ITestService, TestServiceImpl2>();
            using (ServiceProvider sp = services.BuildServiceProvider())
            {
                ITestService implService =(ITestService) sp.GetService(typeof(ITestService));
                implService.Name = "John";
                implService.sayHi();
                Console.WriteLine(implService.GetType());
            }
        }

运行结果也是一样的,如下:

  • 还有可以使用provider.GetRequiredService<T>()方法,这种方式,如果泛型T不是注册时的服务类型,则会抛出异常,所以,在获取服务对象时的类型必须与注册时候的类型一致:

 

  • provider.GetServices(),获取该注册服务的多个实现类集合。如果注册服务是一个接口,此接口下面有多个实现类的话,那么使用GetServices方法可以遍历出所有的实现类(前提是这个实现类必须跟随接口一起注册在服务容器中):
        static void Main(string[] args)
        {
            ServiceCollection services = new ServiceCollection();
            services.AddScoped<ITestService, TestServiceImpl1>();
            services.AddScoped<ITestService, TestServiceImpl2>();
            using (ServiceProvider sp = services.BuildServiceProvider())
            {
                var implServices = sp.GetServices<ITestService>();
                foreach (var item in implServices)
                {
                    Console.WriteLine(item.GetType()); 
                }
            }
        }

运行结果如下:两个实现类都打印出来了

  • 注意,如果一个服务接口注册了多个实现类,此时使用GetService<T>方法获取的是最后一个注册到容器中的子类类型,如下:

 

打印结果如下:

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐