1. 程式人生 > >【半小時大話.net依賴注入】(一)理論基礎+實戰控制檯程式實現AutoFac注入

【半小時大話.net依賴注入】(一)理論基礎+實戰控制檯程式實現AutoFac注入

系列目錄

  1. 第一章|理論基礎+實戰控制檯程式實現AutoFac注入

  2. 第二章|AutoFac的常見使用套路

  3. 第三章|實戰Asp.Net Framework Web程式實現AutoFac注入

  4. 第四章|實戰Asp.Net Core自帶DI實現依賴注入

  5. 第五章|實戰Asp.Net Core引入AutoFac的兩種方式

說明

簡介

該系列共5篇文章,旨在以實戰模式,在.net下的

  • 控制檯程式

  • Framework Mvc程式

  • Framework WebApi程式

  • Core Api程式

分別實現依賴注入。

其中.Net Framework框架主要以如何引入AutoFac作為容器以及如何運用AuotoFac為主,.Net Core框架除了研究引入AutoFac的兩種方式,同時也運用反射技巧對其自帶的DI框架進行了初步封裝,實現了相同的依賴注入效果。

專案架構如下圖:

專案 名稱 型別 框架
Ray.EssayNotes.AutoFac.Infrastructure.CoreIoc Core容器 類庫 .NET Core 2.2
Ray.EssayNotes.AutoFac.Infrastructure.Ioc Framework容器 類庫 .NET Framework 4.5
Ray.EssayNotes.AutoFac.Model 實體層 類庫 .NET Framework 4.5
Ray.EssayNotes.AutoFac.Repository 倉儲層 類庫 .NET Framework 4.5
Ray.EssayNotes.AutoFac.Service 業務邏輯層 類庫 .NET Framework 4.5
Ray.EssayNotes.AutoFac.ConsoleApp 控制檯主程式 控制檯專案 .NET Framework 4.5
Ray.EssayNotes.AutoFac.CoreApi Core WebApi主程式 Core Api專案 .NET Core 2.2
Ray.EssayNotes.AutoFac.NetFrameworkApi Framework WebApi主程式 Framework WebApi專案 .NET Framework 4.5
Ray.EssayNotes.AutoFac.NetFrameworkMvc Framework MVC主程式 Framework MVC專案 .NET Framework 4.5

GitHub原始碼地址:https://github.com/WangRui321/Ray.EssayNotes.AutoFac

Welcome to fork me~(歡迎來叉我~)

適用物件

該專案主要實戰為主,理論部分我會結合例子和程式碼,深入淺出地闡述,如果你是:

  • 從來沒聽過IoC、DI這些勞什子
  • 瞭解一些依賴注入的理論知識但是缺乏實戰
  • 在.Net Framework下已熟練運用依賴注入,但在.Net Core還比較陌生

只要你花上半個小時認真讀完每一句話,我有信心這篇文章一定會對你有所幫助。

如果你是:

  • 髮量比我還少的秒天秒地的大牛

那麼也歡迎閱讀,雖然可能對你幫助並不大,但是歡迎提供寶貴的意見,有寫的不好的地方可以互相交流~

下面開始第一章《理論知識+實戰控制檯程式實現AutoFac注入》


理論基礎

依賴

依賴,簡單說就是,當一個類需要另一個類協作來完成工作的時候就產生了依賴。這也是耦合的一種形式。

舉個例子,比如標準的三層架構模式

名稱 職責 舉例
介面層(UI) 負責展示資料 StudentController
業務邏輯層(BLL) 負責業務邏輯運算 StudentService
資料訪問層(DAL) 負責提供資料 StudentRepository

資料訪問層(DAL)程式碼:

    /// <summary>
    /// 學生倉儲
    /// </summary>
    public class StudentRepository
    {
        public string GetName(long id)
        {
            return "學生張三";//造個假資料返回
        }
    }

業務層(BLL)程式碼:

    /// <summary>
    /// 學生邏輯處理
    /// </summary>
    public class StudentService
    {
        private readonly StudentRepository _studentRepository;

        public StudentService()
        {
            _studentRepository = new StudentRepository();
        }

        public string GetStuName(long id)
        {
            var stu = _studentRepository.Get(id);
            return stu.Name;
        }
    }

其中,StudentService的實現,就必須要依賴於StudentRepository。而且這是一種緊耦合,一旦StudentRepository有任何更改,必然導致StudentService的程式碼同樣也需要更改,這種情況是程式設計師們不願意看到的。

介面驅動

介面驅動是為了實現一個設計原則:要依賴於抽象,而不是具體的實現。
還拿上面的例子說明,現在我新增一個DAL的介面層,IStudentRepository,抽象出所需方法:

    /// <summary>
    /// 學生倉儲interface
    /// </summary>
    public interface IStudentRepository
    {
        string GetName(long id);
    }

然後讓StudentRepository去實現這個介面:

    /// <summary>
    /// 學生倉儲
    /// </summary>
    public class StudentRepository : IStudentRepository
    {
        public string GetName(long id)
        {
            return "學生張三";//造個假資料返回
        }
    }

然後在StudentService裡只依賴於IStudentRepository,以後的增刪改查都通過IStudentRepository這個抽象來做:

    /// <summary>
    /// 學生邏輯處理
    /// </summary>
    public class StudentService
    {
        private readonly IStudentRepository _studentRepository;

        public StudentService()
        {
            _studentRepository = new StudentRepository();
        }

        public string GetStuName(long id)
        {
            var stu = _studentRepository.Get(id);
            return stu.Name;
        }
    }

這樣做的好處有兩個,一個是低耦合,一個是職責清晰。如果對此還有懷疑的話,我們可以想象一個情景,就是負責寫StudentService的是程式設計師A,負責寫StudentRepository的是另一個程式設計師B,那麼:

  • 針對程式設計師A

我(程式設計師A)只需要關注業務邏輯層面,如果我需要從倉儲層拿資料庫的資料,比如我需要根據Id獲取學生實體,那麼我只需要去IStudentRepository找Get(long id)函式就可以了,至於實現它的倉儲怎麼實現這個方法我完全不用管,你怎麼從資料庫拿資料不是我該關心的事情。

  • 針對程式設計師B

我(程式設計師B)的工作就是實現IStudentRepository介面的所有方法就行了,簡單而明確,至於誰來呼叫我,我不用管。IStudentRepository裡有根據Id獲取學生姓名的方法,我實現了就行,至於業務邏輯層拿這個名字幹啥,那不是我要關心的事情。

這樣看的話是不是彼此的職責就清晰多了,更進一步再舉個極端的例子:
比如程式設計師B是個實習生,整天划水摸魚,技術停留在上個世紀,結果他寫的倉儲層讀取資料庫全部用的手寫sql語句的方式,極難維護,後來被領導發現領了盒飯,公司安排了另一個程式設計師C來重寫倉儲層,C這時不需要動其他程式碼,只需要新建一個倉儲StudentNewRepository,然後實現之前的IStudentRepository,C使用Dapper或者EF,寫完新的倉儲層之後,剩下的只需要在StudentService裡改一個地方就行了:

        public StudentService()
        {
            _studentRepository = new StudentNewRepository();
        }

是不是很清晰,耦合不會像以前那麼重。
其實對於這個小例子來說,介面驅動的優勢還不太明顯,但是在系統層面優勢就會被放大。比如上面換倉儲的例子,雖然職責是清晰了,但是專案裡有幾個Service就需要改幾個地方,還是很麻煩。原因就是上面講的,這是一種依賴關係,Service要依賴Repository,有沒有一種方法可以讓這種控制關係反轉過來呢?當Service需要使用Repository,有沒有辦法讓我需要的Repository自己注入到我這裡來?
當然有,這就是我們將要實現的依賴注入。使用依賴注入後你會發現,當C寫完新的倉儲後,業務邏輯層(StudentService)是不需要改任何程式碼的,所有的Service都不需要一個一個去改,直接在注入的時候修改規則,不要注入以前老的直接注入新的倉儲就可以了。

面向介面後的架構:

名稱 職責 舉例
介面層(UI) 負責展示資料 StudentController
業務邏輯抽象層(InterfaceBLL) 業務邏輯運算抽象介面 IStudentService
業務邏輯層(BLL) 負責業務邏輯運算 StudentService
資料訪問抽象層(InterfaceDAL) 資料訪問抽象介面 IStudentRepository
資料訪問層(DAL) 負責提供資料 StudentRepository

什麼是IoC

IoC,全稱Inversion of Control,即“控制反轉”,是一種設計原則,最早由Martin Fowler提出,因為其理論提出時間和成熟時間相對較晚,所以並沒有被包含在GoF的《設計模式》中。

什麼是DI

DI,全稱Dependency Injection,即依賴注入,是實現IoC的其中一種設計方法。
其特徵是通過一些技巧,將依賴的物件注入到呼叫者當中。(比如把Repository注入到Service當中)
這裡說的技巧目前主要指的就是引入容器,先把所有會產生依賴的物件統一新增到容器當中,比如StudentRepository和StudentService,把分配許可權交給容器,當StudentService內部需要使用StudentRepository時,這時不應該讓它自己new出來一個,而是通過容器,把StudentRepository注入到StudentService當中。
這就是名稱“依賴注入”的由來。

DI和IoC有什麼區別

這是個老生常談的問題了,而且這兩個名字經常在各種大牛和偽大牛的吹逼現場頻繁出現 ,聽的新手雲裡霧裡,莫名感到神聖不可侵犯。那麼DI和IoC是同一個東西嗎?如果不是,它們又有什麼區別呢?
回答很簡單:不是一個東西。
區別也很簡單,一句話概括就是:IoC是一種很寬泛的理念,DI是實現了IoC的其中一種方法。
說到這裡我已經感覺到屏幕後的你性感地添了一下嘴脣,囤積好口水,準備開始噴我了。
先別慌,我有證據,我們先來看下微軟怎麼說:

ASP.NET Core supports the dependency injection (DI) software design pattern, which is a technique for achieving Inversion of Control (IoC) between classes and their dependencies.

地址:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.2

翻譯過來就是“ASP.NET Core支援依賴注入(DI)的軟體設計模式,該模式是一種在類和它依賴的物件之間實現了控制反轉(IoC)的技術”。

如果有人覺得辣雞微軟不夠權威,那我們去看下IoC以及DI這兩個概念的發明人——Martin Fowler怎麼說:

幾位輕量級容器的作者曾驕傲地對我說:這些容器非常有用,因為它們實現了控制反轉。這樣的說辭讓我深感迷惑:控制反轉是框架所共有的特徵,如果僅僅因為使用了控制反轉就認為這些輕量級容器與眾不同,就好象在說我的轎車是與眾不同的,因為它有四個輪子。
因此,我想我們需要給這個模式起一個更能說明其特點的名字——”控制反轉”這個名字太泛了,常常讓人有些迷惑。經與多位IoC 愛好者討論之後,我們決定將這個模式叫做”依賴注入”(Dependency Injection)。

地址:http://insights.thoughtworkers.org/injection/

Martin Fowler說的比較委婉,其實說白了就是建議我們,不要亂用IoC裝逼,IoC是一種設計理念,很寬泛,你把程式裡的一個寫死的變數改成從配置檔案裡讀取也是一種控制反轉(由程式控制反轉為由框架控制),你把這個配置改成使用者UI介面的一個輸入文字框由使用者輸入也是一種控制反轉(由框架控制反轉為由使用者自己控制)
所以,如果確定討論的模式是DI,那麼就表述為DI,還是儘量少用IoC這種寬泛的表達。

AutoFac

AutoFac是一個開源的輕量級的DI容器,也是.net下最受大家歡迎的實現依賴注入的工具之一,通過AutoFac我們可以很方便的實現一些DI的騷操作。

實戰控制檯程式依賴注入

目標很簡單,就是控制檯程式啟動後,將學生姓名打印出來。
程式啟動流程是,控制檯主程式呼叫Service層,Service層呼叫Repository層獲取資料(示例專案的倉儲層沒有連線資料庫,只是直接造個假資料返回)。
沒有依賴注入的情況下,肯定是主程式會new一個StudentService,StudentService裡會new一個StudentRepository,現在引入依賴注入後,就不應該這麼new出來了,而是通過容器注入,也就是容器會把StudentRepository自動注入到StudentService當中。

架構

實體層

學生實體類StudentEntity:

namespace Ray.EssayNotes.AutoFac.Model
{
    /// <summary>學生實體</summary>
    public class StudentEntity
    {
        /// <summary>唯一標識</summary>
        public long Id { get; set; }
        /// <summary>姓名</summary>
        public string Name { get; set; }
        /// <summary>成績</summary>
        public int Grade { get; set; }
    }
}

倉儲層

IStudentRepository介面:

using Ray.EssayNotes.AutoFac.Model;

namespace Ray.EssayNotes.AutoFac.Repository.IRepository
{
    /// <summary>學生倉儲interface</summary>
    public interface IStudentRepository
    {
        string GetName(long id);
    }
}

StudentRepository倉儲類:

using Ray.EssayNotes.AutoFac.Model;
using Ray.EssayNotes.AutoFac.Repository.IRepository;

namespace Ray.EssayNotes.AutoFac.Repository.Repository
{
    /// <summary>
    /// 學生倉儲
    /// </summary>
    public class StudentRepository : IStudentRepository
    {
        public string GetName(long id)
        {
            return "學生張三";//造個假資料返回
        }
    }
}

Service層

IStudentService介面

namespace Ray.EssayNotes.AutoFac.Service.IService
{
    /// <summary>
    /// 學生邏輯處理interface
    /// </summary>
    public interface IStudentService
    {
        string GetStuName(long id);
    }
}

StudentService類:

using Ray.EssayNotes.AutoFac.Repository.IRepository;
using Ray.EssayNotes.AutoFac.Repository.Repository;
using Ray.EssayNotes.AutoFac.Service.IService;

namespace Ray.EssayNotes.AutoFac.Service.Service
{
    /// <summary>
    /// 學生邏輯處理
    /// </summary>
    public class StudentService : IStudentService
    {
        private readonly IStudentRepository _studentRepository;
        /// <summary>
        /// 構造注入
        /// </summary>
        /// <param name="studentRepository"></param>
        public StudentService(IStudentRepository studentRepository)
        {
            _studentRepository = studentRepository;
        }

        public string GetStuName(long id)
        {
            var stu = _studentRepository.Get(id);
            return stu.Name;
        }
    }
}

其中建構函式是一個有參的函式,引數是學生倉儲,這個後面依賴注入時會用。

AutoFac容器

需要先通過Nuget匯入Autofac包:

using System;
using System.Reflection;
//
using Autofac;
using Autofac.Core;
//
using Ray.EssayNotes.AutoFac.Repository.IRepository;
using Ray.EssayNotes.AutoFac.Repository.Repository;
using Ray.EssayNotes.AutoFac.Service.IService;
using Ray.EssayNotes.AutoFac.Service.Service;

namespace Ray.EssayNotes.AutoFac.Infrastructure.Ioc
{
    /// <summary>
    /// 控制檯程式容器
    /// </summary>
    public static class Container
    {
        /// <summary>
        /// 容器
        /// </summary>
        public static IContainer Instance;

        /// <summary>
        /// 初始化容器
        /// </summary>
        /// <returns></returns>
        public static void Init()
        {
            //新建容器構建器,用於註冊元件和服務
            var builder = new ContainerBuilder();
            //自定義註冊
            MyBuild(builder);
            //利用構建器建立容器
            Instance = builder.Build();
        }

        /// <summary>
        /// 自定義註冊
        /// </summary>
        /// <param name="builder"></param>
        public static void MyBuild(ContainerBuilder builder)
        {
            builder.RegisterType<StudentRepository>().As<IStudentRepository>();
            builder.RegisterType<StudentService>().As<IStudentService>();
        }
    }
}

其中:

  • public static IContainer Instance
    為單例容器
  • Init()方法
    用於初始化容器,即往容器中新增物件,我們把這個新增的過程稱為註冊(Register)。
    ContainerBuilder為AutoFac定義的容器構造器,我們通過使用它往容器內註冊物件。
  • MyBuild(ContainerBuilder builder)方法
    我們具體註冊的實現函式。RegisterType是AutoFac封裝的一種最基本的註冊方法,傳入的泛型(StudentService)就是我們欲新增到容器的物件;As函式負責繫結註冊物件的暴露型別,一般是以其實現的介面型別暴露,這個暴露型別是我們後面去容器內查詢物件時使用的搜尋標識,我們從容器外部只有通過暴露型別才能找到容器內的物件。

    主程式

需要先Nuget匯入AutoFac程式包:

using System;
//
using Autofac;
//
using Ray.EssayNotes.AutoFac.Infrastructure.Ioc;
using Ray.EssayNotes.AutoFac.Service.IService;


namespace Ray.EssayNotes.AutoFac.ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Container.Init();//初始化容器,將需要用到的元件新增到容器中

            PrintStudentName(10001);

            Console.ReadKey();
        }

        /// <summary>
        /// 輸出學生姓名
        /// </summary>
        /// <param name="id"></param>
        public static void PrintStudentName(long id)
        {
            //從容器中解析出物件
            IStudentService stuService = Container.Instance.Resolve<IStudentService>();
            string name = stuService.GetStuName(id);
            Console.WriteLine(name);
        }
     }
 }

進入Main函式,先呼叫容器的初始化函式,該函式執行成功後,StudentRepository和StudentService就被註冊到容器中了。
然後呼叫列印學生姓名的函式,其中Resolve()方法是AutoFac封裝的容器的解析方法,傳入的泛型就是之前註冊時的暴露型別,下面可以詳細看下這一步到底發生了哪些事情:

  • 容器根據暴露型別解析物件

也就是容器會根據暴露型別IStudentService去容器內部找到其對應類(即StudentService),找到後會試圖例項化一個物件出來。

  • 例項化StudentService

AutoFac容器在解析StudentService的時候,會呼叫StudentService的建構函式進行例項化。

  • 構造注入

AutoFac容器發現StudentService的建構函式需要一個IStudnetRepository型別的引數,於是會自動去容器內尋找,根據這個暴露型別找到對應的StudnetRepository後,自動將其注入到了StudentService當中

經過這幾步,一個簡單的基於依賴注入的程式就完成了。

結果

我們將控制檯程式設定為啟動專案,點選執行,如圖呼叫成功:

如果把除錯斷點加在容器初始化函式裡,可以很清晰的看到哪些物件被註冊到了容器裡:

補充

使用控制檯程式本來是為了突出容器的概念,但是容易造成一些誤解,DI的最終形態可以參考原始碼裡的Api專案和MVC專案,本來想循序漸進,先第一章控制檯引入容器的概念,然後第二章講批量註冊、注入泛型、生命週期域管理,第三章講Api和MVC專案,最後兩章講下.net core的DI,但是這裡還是先說下吧:

  • 誤解1:每次新增Service和Repository都要去註冊,不是更麻煩?

其實是不需要一個一個註冊的,運用批量註冊後容器內部的程式碼是這樣的,可以直接批量註冊所有的:

    /// <summary>
    /// .net framework MVC程式容器
    /// </summary>
    public static class MvcContainer
    {
        public static IContainer Instance;

        /// <summary>
        /// 初始化容器
        /// </summary>
        /// <param name="func"></param>
        /// <returns></returns>
        public static void Init(Func<ContainerBuilder, ContainerBuilder> func = null)
        {
            //新建容器構建器,用於註冊元件和服務
            var builder = new ContainerBuilder();
            //註冊元件
            MyBuild(builder); 
            func?.Invoke(builder);
            //利用構建器建立容器
            Instance = builder.Build();

            //將AutoFac設定為系統DI解析器
            System.Web.Mvc.DependencyResolver.SetResolver(new AutofacDependencyResolver(Instance));
        }

        public static void MyBuild(ContainerBuilder builder)
        {
            Assembly[] assemblies = Helpers.ReflectionHelper.GetAllAssembliesWeb();

            //批量註冊所有倉儲 && Service
            builder.RegisterAssemblyTypes(assemblies)//程式集內所有具象類(concrete classes)
                .Where(cc => cc.Name.EndsWith("Repository") |//篩選
                             cc.Name.EndsWith("Service"))
                .PublicOnly()//只要public訪問許可權的
                .Where(cc => cc.IsClass)//只要class型(主要為了排除值和interface型別)
                .AsImplementedInterfaces();//自動以其實現的所有介面型別暴露(包括IDisposable介面)

            //註冊泛型倉儲
            builder.RegisterGeneric(typeof(BaseRepository<>)).As(typeof(IBaseRepository<>));

            //註冊Controller
            Assembly mvcAssembly = assemblies.FirstOrDefault(x => x.FullName.Contains(".NetFrameworkMvc"));
            builder.RegisterControllers(mvcAssembly);
        }
    }

誤解2:每次使用都要解析下,還不如直接new
好吧,其實也是不需要自己去解析的,最終形態的Controller入口是這樣的,直接在建構函式裡寫就行了:

    public class StudentController : Controller
    {
        private readonly IStudentService _studentService;
        public StudentController(IStudentService studentService)
        {
            _studentService = studentService;
        }

        /// <summary>
        /// 獲取學生姓名
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public string GetStuNameById(long id)
        {
            return _studentService.GetStuName(id);
        }
    }

就是直接在建構函式裡注入就可以了。

  • 誤解3:依賴注入是不是過度設計?

首先DI是一個設計模式(design pattern),其本身完全不存在過不過度的問題,這完全取決於用的人和怎麼用。
另外,在.NET Core中,DI被提到了一個很重要的地位,如果想要了解.NET Core,理解DI是必不可少的。