系列目錄
- 第一章|理論基礎+實戰控制台程式實作AutoFac注入
- 第二章|AutoFac的常見使用套路
- 第三章|實戰Asp.Net Framework Web程式實作AutoFac注入
- 第四章|實戰Asp.Net Core自帶DI實作依賴注入
- 第五章|實戰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這種寬泛的表達。
實戰控制台程式依賴注入
目标很簡單,就是控制台程式啟動後,将學生姓名列印出來。
程式啟動流程是,控制台主程式調用Service層,Service層調用Repository層擷取資料(示例項目的倉儲層沒有連接配接資料庫,隻是直接造個假資料傳回)。
沒有依賴注入的情況下,肯定是主程式會new一個StudentService,StudentService裡會new一個StudentRepository,現在引入依賴注入後,就不應該這麼new出來了,而是通過容器注入,也就是容器會把StudentRepository自動注入到StudentService當中。
AutoFac
Auto是一個開源的輕量級的DI容器,也是.net下最受大家歡迎的實作依賴注入的工具之一,通過AutoFac我們可以很友善的實作一些DI的騷操作。
架構
實體層
學生實體類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當中
經過這幾步,一個簡單的基于依賴注入的程式就完成了。
結果
我們将控制台程式設定為啟動項目,點選運作,如圖調用成功:
如果把調試斷點加在容器初始化函數裡,可以很清晰的看到哪些對象被注冊到了容器裡: