.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的使用


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM