.NET中的控制反转及AutoFac的简单说明

shanzm-2020年3月16日 02:17:35

1.控制反转


1.1 什么是依赖?

依赖是面向对象中用来描述类与类之间一种关系的概念。两个相对独立的对象,当一个对象负责构造另一个对象的实例,或者依赖另一个对象的服务,这样的两个对象之间主要体现为依赖关系


1.2 什么是控制反转?

说反转则要先说“正转”,传统中,在程序中使用new关键字配合构造函数去创建一个对象,这就是程序主动的创建其所依赖对象,这就是“正转”。

调用者不自己创建被调用者对象,而交由第三方(容器)进行创建被调用者对象,这个过程称为控制反转(inversion of control,IOC)。

为什么要控制反转?控制反转是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度,便于扩展和后期维护。


1.3 什么是依赖注入?

实现控制反转的主要方式是依赖注入。(当然不止依赖注入这一种方法,还有依赖查找(Dependency Lookup,DL)。二者区别可参考:维基:控制反转)

依赖注入具体是指:调用类 不主动创建依赖对象,而是使用容器来帮忙创建及注入依赖对象,这个过程就称为依赖注入(Dependency Injection,DI

从代码层面说,依赖注入提供一种机制,将依赖对象的引用传递给被依赖对象

具体的说:Class A(调用类)中用到 Class B 类型的对象(依赖对象),通常情况下,我们在 Class A 中使用new关键字配合构造函数创建一个 Class B 的对象

但是,采用依赖注入技术之后, Class A只需要定义一个Class B类型的属性,不需要直接new来获得这个对象,而是通过IOC容器 将Class B类型的对象在外部new出来并注入到Class A里的引用中,从而实现Class A和Class B解耦


1.4 简单总结

明白了上述几个概念,那么就可以理解这样一句话“模块间的依赖关系从程序内部提到外部来实例化管理称之为控制反转,这个实例化的过程就叫做依赖注入。”



2.控制反转容器


2.1 IOC容器说明

在说到控制反转时提到“使用IOC容器在 调用类 外部创建 依赖对象 并注入到 调用类”,其中IOC容器是什么?

IOC容器就是具有依赖注入功能的容器,IOC容器负责实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。从而,应用程序无需直接在代码中new相关的对象,应用程序由IOC容器进行组装。

简而言之,IOC容器主要就两个作用:1、绑定服务与实例之间的关系。2、对实例进行创建和销毁

在.NET程序中IOC容器有许多,如Unity、AutoFac、Spring.net等等。

据说AutoFac是关于.NET的最流行的IOC容器,本文简单的介绍一下AutoFac的使用方式。


2.2 IOC容器的原理

IOC容器是怎么实现的呢?

可以参考 手写一个简单的IOC容器


2.3 使用AutoFac的简介示例

使用AutoFac容器一般是面向接口编程。所以这里使用一个分层项目来演示AutoFac的使用,即使用AutoFac创建接口实现类的对象

完整Demo下载

①新建一个类库项目TestIBLL

用于定义一些接口

public interface IUserBll
{
    //检查登录信息
    bool Login(string userName, string pwd);
    //添加新用户
    void AddNew(string userName, string pwd);
}

②新建一个类库项目TestBLLImpl

添加对TestIBLL项目的引用,用于定义接口的实现类

 public class UserBll : IUserBll
{
    //实现接口
    public void AddNew(string userName, string pwd)
    {
        Console.WriteLine($"新增了一个用户:{userName}");//为了演示,简单模拟
    }
    public bool Login(string userName, string pwd)
    {
        Console.WriteLine($"登录用户是:{userName}");//为了演示,简单模拟
        return true;
    }
}

【说明】

这里定义了UserBll类,实现了IUserBll接口,

按照AutoFac中的术语,有如下称呼:

  • UserBll类称为组件(Component)

  • IUserBll接口称为服务(Service)

③新建一个控制台项目TestUI

用于模拟UI层

添加对TestIBLL和TestBLLImpl项目的引用

安装AutoFac:PM>Install-Package Autofac

static void Main(string[] args)
{
    //创建容器构造者
    ContainerBuilder builder = new ContainerBuilder();
    //注册组件UserBll类,并把服务IUserBll接口暴露给该组件
    //把服务(IUserBll)暴露给组件(UserBll)
    builder.RegisterType<UserBll>().As<IUserBll>();
    //创建容器
    IContainer container = builder.Build();
    //使用容器解析服务,创建实例(不推荐,见下面说明):IUserBll userBll = container.Resolve<IUserBll>();
    //使用生命周期解析服务,创建实例
    using (ILifetimeScope scope = container.BeginLifetimeScope())
    {
        IUserBll userBll = scope.Resolve<IUserBll>();
        userBll.Login("shanzm", "123456");
    }

}    

【说明】

  • 其中关于AutoFac的术语:

    • 容器(Container) :用于管理程序中所有的组件的结构(简单的说就是管理所有接口的实现类)
    • 生命周期(Lifetime): 实例 的从 创建 到 释放 的持续时间
    • 注册(Registration): 添加和配置 组件 到 容器 的行为
    • 作用域(Scope): 一个特定的 上下文 , 在其中 组件 的 实例 将会被其他 组件 依据它们的 服务 所共享
    • 解析服务: 相当于给服务实例化对象
    • 注册:容器中添加一个实现了服务(接口)的组件(实现类)的操作
  • 通过创建 ContainerBuilder 来注册组件

    ContainerBuilder有一系列的注册方法,这里使用的是通过类型注册:RegisterType()

    任何通过 RegisterType() 注册的组件必须是个具体的类型, 后面解析服务的时候,Autofac就会创建了一个你注册对象的实例

  • 每个组件暴露一个或多个服务(简单地说就是一个类(组件)可能实现一个或多个接口(服务)) ,他们使用 ContainerBuilder 上的 As() 方法连接起来.

  • 解析服务,即创建一个服务的提供对象(简单的说就是为接口创建一个注册的实现类)

    不推荐使用使用容器直接解析服务:IUserBll userBll = container.Resolve<IUserBll>();

    我看到一些文章和视频中使用容器去解析服务,但是我看了官方文档,其中是不推荐这么使用的,因为可能造成内存的泄露。

    推荐你总是从生命周期中解析服务(即我的示例中的方式), 以确保服务实例被妥善地释放和垃圾回收

    AutoFac文档中:“永远从一个生命周期作用域而不是从根容器中解析服务!

    后续为了示例代码的简洁,我还是直接使用容器解析服务,周知!



3 使用AutoFac的一些细节

下面演示一下AutoFac最基本的一些API,具体细节和其他的功能,可以参考AutoFac文档,其文档非常详细且有中文版本(AutoFac文档

3.1 准备工作

接着上面的示例,在类库项目TestIBLL中添加以下接口:

创建IAnimalBll.cs

//IAnimalBll接口
public interface IAnimalBll
{
    void Cry();//动物都有叫的动作
}

创建IMasterBll.cs

//IMasterBll接口
public interface IMasterBll
{
    void Walk();
}

在类库项目TestBLLImpl中分别实现上述接口

创建DogBll.cs

//DogBll类实现IAnimalBll接口
public class DogBll : IAnimalBll
{
    public void Cry()
    {
        Console.WriteLine("汪汪汪!");
    }
}

创建CatBll.cs

//CatBll类实现IAnimalBll接口
public class CatBll : IAnimalBll
{
    public void Cry()
    {
        Console.WriteLine("喵喵喵!");
    }
}

创建MasterBll.cs

//MasterBll类,实现了IMasterBll接口和IUserBll接口
public class MasterBll : IMasterBll,IUserBll
{
    //注意这里,MasterBll是接口的实现类,这个类还有一个接口类型的属性
    public IAnimalBll dogBll { get; set; }

    public void AddNew(string userName, string pwd)
    {
        Console.WriteLine($"新增了一个Master用户:{userName}");
    }
    public bool Login(string userName, string pwd)
    {
        Console.WriteLine($"登录用户是Master:{userName}");
        return true;
    }
    public void Walk()
    {
        Console.WriteLine("带着狗散步!");
        dogBll.Cry();//在调用中,使用.PropertiesAutowired()方法给dogBll注册其实现类
    }
}

3.2 注册整个程序集中的所有实现类

项目中,其实我们可以使用.RegisterAssemblyTypes(),一次性把程序集(类库项目)中的的所有接口实现类都注册给相应的接口

static void Main(string[] args)
{
    ContainerBuilder builder = new ContainerBuilder();//创建容器构造者

    Assembly asm = Assembly.Load(" TestBLLImpl");//获取指定的程序集
    
    builder.RegisterAssemblyTypes(asm).AsImplementedInterfaces();//注册指定程序集中的所有接口实现类

    IContainer container = builder.Build();//创建容器

    IUserBll userBll = container.Resolve<IUserBll>();//解析服务,创建实例

    userBll.Login("shanzm", "123456");//使用服务提供者
}

【说明】

  • 关于使用.RegisterAssemblyTypes()对指定的程序集扫描注册,可以使用Where()Except()对类型进行过滤
    具体的使用方式可以,查看文档:程序集扫描

  • .AsImplementedInterfaces():将程序集中的实现类注册给它所实现的所有接口。


3.3 注入接口实现类中的接口类型的属性

对实现类中的属性也是可以使用AutoFac注入的,

对于接口的实现类中若是有某个接口类型的属性,我们可以使用.PropertiesAutowired()在注册该实现类的同时,把该属性同时注册,即实现属性的自动装配,即属性注入

static void Mian(string[] args)
{
    
    ContainerBuilder builder = new ContainerBuilder();

    Assembly asm = Assembly.Load("TestBLLImpl");

    //在这里通过.PropertiesAutowired(),给接口实现类中的接口属性也注册一个该类型的接口的实现类,即实现属性自装配
    builder.RegisterAssemblyTypes(asm).AsImplementedInterfaces().PropertiesAutowired();
    builder.RegisterType<DogBll>().As<IAnimalBll>();

    IContainer container = builder.Build();

    IMasterBll masterBll = container.Resolve<IMasterBll>();
    masterBll.Walk();//打印:带着狗散步!汪汪汪!
}
  • 注意这里的一个细节,在MasterBll类中有一个IAnimalBll类型的属性dogBll,我们使用PropertiesAutowired()方法实现属性的自动装配,

    但是呀,IAnimalBll接口在TestBLLImpl程序集中有两个实现类,而自动装配按顺序给dogBll属性注册的是CatBll类型的对象

    而我的期望是注册DogBll类型的对象给IAnimalBll类型的属性

    所以这里还要显示的把DogBll类注册给IAnimalBll接口

  • 如果你预先知道属性的名字和值,你可以使用WithProperty("PropertyName", propertyValue)

    所以示例中可以这样写:

    builder.RegisterType<MasterBll>().As<IMasterBll>().WithProperty("dogBll",new DogBll());


3.4 关于一个接口有多个不同的实现类

为已给接口注册实现类的时候,可能该接口有多个实现类,则我们可以为每一个注册提供已给命名
builder.RegisterType<Object>().Named<IObject>(string name)
在解析服务的时候,可以通过名称指定创建哪个服务提供者(实现类)
IContainer.ResolveNamed<IObject>(string name)

static void Main(string[] args)
{
    ContainerBuilder builder = new ContainerBuilder();

    //builder.RegisterType<DogBll>().As<IAnimalBll>();//这样写,下面注册服务的时候,你只能给IAnimalBll对象创建一个DogBll类型的实例
    builder.RegisterType<DogBll>().Named<IAnimalBll>("Dog");
    builder.RegisterType<CatBll>().Named<IAnimalBll>("Cat");

    IContainer container = builder.Build();

    using (ILifetimeScope scope = container.BeginLifetimeScope())
    {
        IAnimalBll dogBll = scope.ResolveNamed<IAnimalBll>("Dog");
        IAnimalBll catBll = scope.ResolveNamed<IAnimalBll>("Cat");
        dogBll.Cry();
        catBll.Cry();
    }
}

但是我们在注册整个程序集中的实现类的时候,该怎么注册已给接口的不同的实现类呢?

使用IEnumerable<IObject> objects =container.Resolve<IEnumerable<IObject>>()

static void Main(string[] args)
{
    ContainerBuilder builder = new ContainerBuilder();

    Assembly asm = Assembly.Load(" TestBLLImpl");
    builder.RegisterAssemblyTypes(asm).AsImplementedInterfaces();
    IContainer container = builder.Build();
    
    //解析服务,将所有实现了IAnimalBll接口的类都注册,结果存放在集合中
    IEnumerable<IAnimalBll> animalBlls = container.Resolve<IEnumerable<IAnimalBll>>();

    foreach (var bll in animalBlls)
    {
        Console.WriteLine(bll.GetType());
        bll.Cry();
    }
    //选取指定的实现类
    IAnimalBll dogBll = animalBlls.Where(t => t.GetType() == typeof(DogBll)).First();
    dogBll.Cry();
}

3.5 关于一个实现类实现了多个接口

之前我们说了使用在注册组件时,一个组件暴露多个服务的时候,可以连续使用 .As()方法

使用.AsImplementedInterfaces()可以达到同样的效果

MasterBll类实现了多个接口,我们可以把该类注册给他所有实现的接口
换言之,只要是MasterBll实现的接口,我们都注册给他一个MasterBll类型的对象
但是注意,这个MasterBll对象只包含当前接口中的方法

static void Mian(string[] args)
{
     ContainerBuilder builder = new ContainerBuilder();

    //builder.RegisterType<MasterBll>().As<IUserBll>().As<IMasterBll>(); 
    //即一个组件暴露了多个服务,这里就等价于:
    builder.RegisterType<MasterBll>().AsImplementedInterfaces();//把MasterBll类注册给所有他实现的接口
   
    
    IContainer container = builder.Build();

    //解析IUserBll服务
    //其实这里的userBll是MasterBll类型的对象,但是这个MasterBll类型的对象只具有IUserBll接口中的方法,不具有IMasterBll接口中的方法
    IUserBll userBll = container.Resolve<IUserBll>();

    userBll.Login("shanzm", "11111");//打印:登录用户是Master:shanzm
    Console.WriteLine(userBll.GetType());//打印:TestBLLImpl.MasterBll
    //userBll.Walk();//注意虽然是MasterBll类型对象,但是只具有当前解析的IUserBll接口中的方法
}

【说明】:

  • 在注册组件(实现类)的时候连续使用As()方法,可以暴露所有其实现的接口(服务),比较麻烦,所以使用.AsImplementedInterfaces()方法,将实现类的注册给其所实现的所有接口

  • 该实现类实现了多个接口,但是在解析服务的时候,只具有当前解析的接口中的方法。


3.6 关于实例作用域

在使用AutoFac的时候,最后解析服务,创建提供服务的实例对象

这个对象的在程序中存在时长,也就是从实例化到最后释放的时间,称之为服务的生命周期

这个对象在应用中能共享给其他组件并被消费的作用域,称之为服务的作用域

在理解了以上的概念后,我们才能解释什么是实例作用域

1.一个依赖一个实例(Instance Per Dependency)

当我们调用 Resolve() 解析服务的时候返回一个实例,每次请求都返回一个唯一的实例,如无说明,默认就是这种作用域!

static void Mian(string[] args)
{
    ContainerBuilder builder = new ContainerBuilder();
    //注册实例
    //builder.RegisterType<UserBll>().As<IUserBll>().InstancePerDependency(); //默认就是这种
    builder.RegisterType<UserBll>().As<IUserBll>();
    //创建容器
    IContainer container = builder.Build();
 
   //解析服务
    using (var scope = container.BeginLifetimeScope())
    {
        IUserBll userBll1 = scope.Resolve<IUserBll>();
        userBll1.Login("shanzm", "1111");//打印:登录用户是普通用户:shanzm
        IUserBll userBll2 = scope.Resolve<IUserBll>();
        userBll2.Login("shanzm", "2222");//打印:登录用户是普通用户:shanzm
        
        Console.WriteLine(ReferenceEquals(userBll1, userBll2));//打印结果:false
    }
}
//说明:根据调试,结果就可以看出,每次在解析服务,创建的服务提供者都是新的。
//你要注意,我们上面的示例代码在同一个生命周期中注册的两个IUserBll接口的实例,但是它们依旧是两个不同的实例

2.单一实例(Single Instance)

它也被称为 '单例.' 使用单一实例作用域, 在根容器和所有嵌套作用域内所有的请求都将会返回同一个实例.

建议在面向接口的编程中,实例作用域采用:单一实例。防止出现并发操作,造成脏数据!

static void Mian(string[] args)
{
    ContainerBuilder builder = new ContainerBuilder();
    //注册实例
    builder.RegisterType<UserBll>().As<IUserBll>().SingleInstance();//设置为单一实例
    //创建容器
    IContainer container = builder.Build();
 
    //解析服务
    using (var scope1 = container.BeginLifetimeScope())
    {
        IUserBll userBll1 = scope1.Resolve<IUserBll>();
        userBll1.Login("shanzm", "1111");
        using (var scope2 = container.BeginLifetimeScope())
        {
            IUserBll userBll2 = scope2.Resolve<IUserBll>();
            userBll2.Login("shanzm", "2222");
            Console.WriteLine(ReferenceEquals(userBll1, userBll2));
            //因为是单一实例,所以就是在不同的生命周期中,也是同一个实例,打印结果:true
        }
    }
}
//说明:最终的打印结果:true 。即使在不同的生命周期中每次在解析服务,创建的服务提供者都是同一个!

其他的实例作用域,详细可以参考文档:实例作用域

  1. 每个生命周期作用域一个实例(Instance Per Lifetime Scope)
  2. 每个匹配的生命周期作用域一个实例(Instance Per Matching Lifetime Scope)
  3. 每个请求一个实例(Instance Per Request)
  4. 每次被拥有一个实例(Instance Per Owned)
  5. 线程作用域(Thread Scope)


4.在MVC中使用AutoFac

AutoFac在 ASP .NET MVC中使用更加的方便,主要需要注意的地方就是在Global.asax.cs文件中对AutoFac配置

做一个简单示例:(点击下载完整的Demo源码)

①创建名为TestIService的类库项目,定义所有接口

创建IUserService.cs 文件

public interface IUserService
{
    bool CheckLogin(string userName, string pwd);
    bool CheckUserNameExists(string userName);
}

创建INewsService.cs 文件

public interface INewsService
{
    string AddNews(string title, string body);
}

②创建名为TestServiceImpl的类库项目,定义接口的实现类

首先,添加对TestIService项目的引用

创建UserService.cs 文件

public class UserService : IUserService
{
    //注意接口的实现类是可以有接口类型的属性,该属性也会被注册一个实现对应类型接口的类的对象
    public INewsService newsService { get; set; }
    public bool CheckLogin(string userName, string pwd)
    {
        return true;
    }
    public string UserAction(string userName)
    {
        string result = newsService.AddNews("2020年3月16日-新冠病毒", "中国境内的新冠病毒被有效遏制");
        return userName+"  添加新闻  :"+result;
    }
}

创建NewsService.cs 文件

public class NewsService : INewsService
{
    public string AddNews(string title, string body)
    {
        return ($"Title:{title},Content:{body}");
    }
}

③创建名为TestMVC 的Web MVC项目

首先,添加对TestIService项目和TestServiceImpl项目的引用

接着安装AutoFac在MVC中插件:PM> Install-Package AutoFac.Mvc5

在Global.asax.cs中添加对AutoFac的配置:

using Autofac.Integration.Mvc;

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    RouteConfig.RegisterRoutes(RouteTable.Routes);

    /*------------------------------AutoFac配置--开始--------------*/

    ContainerBuilder builder = new ContainerBuilder();
    //此处需要:using Autofac.Integration.Mvc;
    //把当前程序集中的所有Controllerr类中的接口类型的属性注册
    builder.RegisterControllers(typeof(MvcApplication).Assembly).PropertiesAutowired();

    Assembly asmSevice = Assembly.Load("TestServiceImpl");
    builder.RegisterAssemblyTypes(asmSevice)
        .Where(type => !type.IsAbstract)//除去抽象类,抽象类不可以实例化(其实这一句也可以不写)
        .AsImplementedInterfaces()//将实现类注册给其实现的所有接口
        .PropertiesAutowired();//接口实现类中接口类型的属性也注册
    IContainer container = builder.Build();
    //MVC中的所有Controller类都是由AutoFac帮我们创建对象
    DependencyResolver.SetResolver(new AutofacDependencyResolver(container));

    /*------------------------------AutoFac配置--结束--------------*/
}

创建HomeController.cs 控制器

public class HomeController : Controller
{
    public IUserService userService { get; set; }//通过AutoFac自动为我们赋值一个IUserService接口实现对象


    public ActionResult CheckLogin()
    {
        bool b = userService.CheckLogin("shanzm", "123456");
        return Content(b.ToString());//结果:页面显示true
    }

    public ActionResult UserAddNews()
    {
        string result = userService.UserAction("shanzm");
        return Content(result);//结果:页面显示:shanzm 添加新闻 :Title:2020年3月16日-新冠病毒,Content:中境内的新冠病毒被有效遏制
    }
}

在浏览器中分别请求HomeController中的两个Action,即可以看到我们使用AutoFac给userService属性注入相应的实例成功了!



5.参考及示例源码下载

全文示例的源代码下载

文档:AutoFac官方中文文档

博客园:全面理解 ASP.NET Core 依赖注入

博客园:ASP.NET MVC IOC 之AutoFac攻略

博客园:Autofac 组件、服务、自动装配 《第二篇》

简书:AutoFac的使用

posted @ 2020-03-16 02:21  shanzm  阅读(1637)  评论(6编辑  收藏  举报
TOP