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

系列目錄

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

  2. 第二章|AutoFac的使用技巧git

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

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

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

說明

簡介

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

  • 控制檯程序設計模式

  • Framework Mvc程序架構

  • Framework WebApi程序mvc

  • Core Api程序app

分別實現依賴注入。

其中.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

源碼是一個虛構的項目框架,相似於樣例性質的代碼或者測試程序,裏面不少註釋,對理解DI,或怎麼在MVC、WebApi和Core Api分別實現依賴注入有很好的幫助效果。
因此,如下內容,配合源碼食用效果更佳~

適用對象

該項目主要實戰爲主,理論部分我會結合例子和代碼,深刻淺出地闡述,若是你是:

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

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

若是你是:

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

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

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


DI理論基礎

依賴

依賴,簡單說就是,當一個類須要另外一個類協做來完成工做的時候就產生了依賴。

這也是耦合的一種形式,可是是不可避免的。

咱們能作的不是消滅依賴,而是讓依賴關係更清晰、更易於控制。

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

名稱 職責 舉例
界面層(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
我只須要關注業務邏輯層面,
若是我須要從倉儲層拿數據庫的數據,
好比我須要根據Id獲取學生實體,
那麼我只須要去IStudentRepository找Get(long id)函數就能夠了,
至於實現它的倉儲怎麼實現這個方法我徹底不用管,
你怎麼從數據庫拿數據不是我該關心的事情。
  • 針對程序員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是必不可少的。

相關文章
相關標籤/搜索