【DNT精英論壇回顧】.NET依賴注入在區塊鏈項目aelf中的實踐

2019年9月8日,DNT精英論壇(暨.NET北京俱樂部)第2期在北京微軟大廈成功舉辦。論壇由資深.NET專家和社區活躍用戶發起,以「分享、成長、合做、雙贏」爲原則,旨在打造一個領先的技術分享平臺和成長交流生態。html

本次活動邀請到了紅帽開放創新實驗室高級諮詢顧問陳計節、52ABP.COM 站長梁桐銘、aelf高級技術經理趙奕旗,三人分別就《.NET Core 雲原生和 DevOps 實踐》、《ASP.NET Core 和EF Core 3.0中的亮點和變化》、《.NET依賴注入在區塊鏈項目aelf中的實踐》三個話題進行了分享。git

aelf高級技術經理趙奕旗結合aelf的設計理念和BCL應用DI的實例,從什麼是依賴注入、設計模式五大原則(SOLID)、DI,IoC,DIP的區別、DI的三個維度這四個方面對.NET依賴注入在aelf中的實踐進行了詳細的分享和解讀。github

如下爲趙奕旗分享內容回顧:面試

什麼是依賴注入

什麼是依賴注入?做爲開發者,咱們對依賴這個名詞都有概念,當咱們手頭作的程序須要用到一個第三方類庫時,咱們就能夠把這個類庫稱爲依賴。可是注入呢?spring

常言道,看一我的的身價,要看他的對手。依賴注入的對手是誰?依賴查找,Dependency Lookup。依賴注入和依賴查找都是實現IoC,也就是控制反轉的方式之一。編程

估計不少人跟我剛開始同樣不理解控制反轉這個名詞,我說一點本身的想法,這個控制指的是對依賴的控制,自己好比我在寫一段代碼,寫着寫着突然發現一個功能的實現依賴於其餘的模塊或者類庫,基於面向對象的角度考慮,可能我想立馬new一個對象出來,在哪裏跌倒,就在哪裏爬起來,在哪裏碰到阻礙,就在哪裏new出來一個依賴,開始藉助這個依賴繼續實現業務邏輯。可是這樣很差,專業點講這樣會提高代碼的耦合,致使代碼難以維護。就是new這個操做很差,由於一旦當場建立實例,就會在如今寫代碼的這個類裏產生一個易變的依賴。萬一之後這個實例換了一個實現,就得返回來改代碼,去new另外一個實現,以後我會說一些依賴注入的反模式。因此根據前輩的經驗,最科學的作法應該是在哪裏跌倒,就在哪裏趴下,想辦法不經過new來獲取這個依賴。設計模式

前面說的兩個方式,依賴注入,依賴查找,就是當你趴在原地找依賴的時候,能夠用的手段。當你選擇趴下的時候,獲取依賴的控制權就反轉了。安全

插一句,用過spring框架的確定據說過服務定位器,如今有不少前輩認爲Service Locator,是一種反模式。其實Service Locator就是依賴查找的一個具體實現。雖說服務定位器和依賴注入自己的思想截然相反,MS.DI在實現依賴注入框架的時候,仍是用到了服務定位器。以後會看到。框架

若是沒用過spring也沒事。函數式編程

咱們嘗試一下釐清兩者的關係。顯然在依賴注入裏,注入這個詞是有重要地位的,Stack Overflow上有一段關於依賴注入的回答頗有意思,問題是如何向一個五歲的孩子解釋依賴注入:

When you go and get things out of the refrigerator for yourself, you can cause problems. You might leave the door open, you might get something Mommy or Daddy doesn't want you to have. You might even be looking for something we don't even have or which has expired.

What you should be doing is stating a need, "I need something to drink with lunch," and then we will make sure you have something when you sit down to eat.

你想象你家的冰箱塞滿了東西,而後一個五歲的小朋友去你家冰箱裏翻吃的,你慌不慌?他可能最後忘了把冰箱門合上,可能找到一些不適合本身吃的東西,甚至會試圖在冰箱裏尋找xbox,那固然一無所得,你家的冰箱若是有意識,可能會想拋出一個空引用異常。

因此合適的作法是什麼?去問一問小朋友須要什麼,而後咱們來從冰箱裏找東西,找出來交給他。

作個類比,小朋友從冰箱翻東西就相似於依賴查找,咱們做爲成年人把東西提供給小朋友,就是依賴注入。

在展開依賴注入以前,我認爲咱們有必要溫習一下設計模式的五大原則(有些地方提了六大甚至七大,可是意思多少有重疊,這裏就說五大原則),順便看一下這五大原則和依賴注入的關係。 我估計不少人被SOLID面試題也折磨地不清,這裏我分享下本身的見解,也算給之後羣裏討論提供一些話題了。

SOLID

Single Responsibility Principle (SRP,單一職責原則)

There should never be more than one reason for a class to change.

你們可能據說過一個段子。

  • 你寫過沒有bug的代碼嗎?

  • 寫過啊,Hello World。

這個段子告訴咱們一個道理:代碼越簡單,越不容易出bug。

怎麼簡單呢?讓一段代碼只作一件事情就行了。在實踐中,咱們讓一個類只作一件事情就行了。並且若是咱們可以給這個類起一個親切又直觀的名字,代碼自己的自述性就會很好,我記得阿里寫Java的規範就對命名有要求,好比若是某個類使用了某種設計模式,就要求把設計模式的名字體如今名字,這個時候註釋都顯得多餘,並且容易讓人類讀懂的代碼更好維護。

回到單一職責原則的描述:只能存在一個致使類變動的緣由。這個表達挺抽象的。碰到這種抽象的句子,我就會試圖仿寫例句。好比只能存在一個致使我變動的緣由。那可能我是一個沒有感情的復讀機,只有別人發的東西變了,我才能換一句復讀。或者我是一個購物車,只有添加購物車的這我的發工資了,我這個購物車纔會清一點東西。不知道你們能不能get個人意思。

還有一個判斷一個類是否符合單一職責原則的標準,就是要看這個類裏面元素之間的關聯性,你不能一個實現復讀機的類裏出現一個寫做文的功能,想都不要想,他最多關心一下剪貼板大小。話說回來,剪貼板自己也不該該包含在復讀機這個類裏,而是單獨做爲一個服務存在,能夠考慮用倉儲(Repository)模式。

關聯性高意味着高內聚,在功能上粒度更小。

這裏說的類,指的能夠是實體,也能夠是服務。領域驅動設計這本書中,Eric把應用裏的對象分紅三種,值,實體,服務。能夠認爲實體就是一些值的組合,服務是包含業務邏輯的代碼。

Open Closed Principle (OCP,開閉原則)

Software entities like classes, modules and functions should be open for extension but closed for modifications.

說到開閉原則,對我而言,結合一個設計模式確定特別容易理解,裝飾器模式。咱們設想一個場景。如今在作超市的收銀系統,或者是什麼電商網站,反正有一個商品的實體,商品實體有一個GetPrice方法,如今要給商品加一個打折的功能。假設商品這個歌類知足單一職責原則,如今有三種方案:

  1. 給接口加一個方法,專門用來提供如今須要的功能

  2. 修改類的實現,用之前的方法簽名提供新的功能

  3. 從新建立一個類,繼承以前的實現類,對之前的方法包裝一層,提供新的實現

開閉原則但願咱們選第三種解決方案。簡單說一下緣由:第一個,修改接口意味着其餘實現了這個接口的類須要新增一個實現,這個新增的實現頗有多是沒必要要的,萬一哪天全部的商品都不打折了,這個方法就廢掉了,因此pass;第二個簡直就是一個危險操做,必定會影響單元測試不說(若是有的話),很大機率會給依賴原來實現的模塊引入bug,pass。第三個方案最安全。咱們能夠爲之前的實現提供一個裝飾器,而後調用裝飾器來提供新的功能。以前隨便舉的例子,之前的實現就是返回商品原價,裝飾器能夠叫什麼「打折裝飾器」,而後調用一樣的方法返回的是打折後的價格,之前的實現要經過構造方法參數注入進裝飾器實現裏。實際使用可能沒這麼粗暴,不過大意就是這樣。

另外,開閉原則和另一個「三大編程原則」版本中的Don't Repeat Yourself原則很類似,只不過角度不一樣於上面使用裝飾器模型的場景了。《重構》中提到了Rule of three,一樣的代碼在第三次出現前,考慮一下怎麼抽象它。

以後講依賴注入的組合根的時候會再次提到開閉原則。

Liskov Substitution Principle (LSP,里氏替換原則)

Functions that use use pointers or references to base classes must be able to use objects of derived classes without knowing it.

這個估計是平時最容易忽視的一個原則。

使用基類的地方,替換成它的子類,程序還能夠正常運行。這個主要是讓咱們繼承一個父類的時候,不要隨便重寫父類的方法,重寫以前要思考一下。其實這個原則的提出應該起源於1988年一我的提出的Design by Contract(契約式設計)理論,這我的我也不認識,契約式設計咱們如今也不必深究,不過咱們剛開始學接口可能也聽過,一個接口方法,就是一個契約,契約式設計把一個契約的實現分紅三部分,如今只要理解前兩個就能夠了:

  1. 前置條件檢查,就是驗證校驗參數是否合法。

  2. 後置條件檢查,就是驗證方法的執行結果是否符合契約,是否誠實,熟悉函數式編程的朋友可能知道FP中的一個原則要編寫誠實的函數,FP裏的誠實是說一個函數要作到言出必行,履行本身的函數簽名,不能說讓它返回一個商品打折後的價格,它拋了一個異常,由於這個異常沒有在它的簽名裏獲得體現,簽名裏說傳入一個string,返回一個int,既然指明瞭這個映射,那返回值就得是int這個集合裏的。這裏說的後置條件驗證是否遵照契約,我就是這麼理解的。

  3. 不變式檢查,對象檢查自身的狀態,確保本身的本質不變,這個不在SOLID原則裏,要實現的話也跟AOP有關。

總之,里氏替換原則是說,若是你要在子類裏重寫父類的方法,前置條件的檢查要麼和父類相同,要麼更寬鬆;後置條件的檢查要麼相同,要麼更嚴格。由於原本在子類裏重寫父類方法的動機就是想對父類作一個擴展,這麼說來里氏替換原則原則仍是很符合直覺的。

我以前犯過一個錯誤,就是有一個方法我設想子類不提供,就在這個子類重寫了父類的方法,而後直接throw new NotImplementedException();,這就嚴重違反了里氏替換原則。這時候應該把這一個方法單獨提出來,作一個接口,我正在寫的這個子類不要繼承這個接口就行了。

Interface Segregation Principle (ISP,接口隔離原則)

Clients should not be forced to depend upon interfaces that they don`t use.

The dependency of one class to another one should depend on the smallest possible.

咱們aelf的智能合約執行框架是經過grpc和.NET的反射實現的。咱們有一個跨合約調用的功能,在定義咱們鏈上須要的智能合約的時候,定義了一些智能合約須要實現的接口,咱們稱之爲:ACS,即AElf Contract Standard。每個ACS定義了一些接口,相似於咱們平時在開發中定義interface的場景,開發者在作跨合約調用的時候,能夠不用關心某些ACS中定義的接口具體是哪一個合約實現的,只須要提供實現了這個ACS的某一個合約的地址就能夠了。另外一方面,每個合約能夠實現多個ACS,被其餘合約作跨合約調用時,可能只對它實現的某一個ACS感興趣,那麼就能夠僅僅依賴於這一個ACS,不會把這個合約的其餘方法暴露給跨合約調用,這即是對接口隔離原則的最好闡述。

在aelf的代碼實現過程當中,咱們還會在鏈上對一些接口提供支持,好比說共識合約。由於aelf的設計理念就是但願區塊鏈的共識是能夠被替換的。目前咱們採用的共識是DPOS,可是理論上,也能夠支持其餘的共識合約如POW或者POC。所以,咱們在代碼實現的時候,把咱們認爲一個共識合約應該提供的服務抽象了出來:好比確認如今能不能出塊?由於POW模式下是每一個人均可以出塊的,可是若是是POS或者DPOS的話,你須要通過一系列選舉投票成爲記帳節點或者見證人才能進行出塊;若是能出塊的話,節點須要多長時間出一個塊;出塊的時候應當如何組織這個塊的數據,同時這個共識合約還應該告訴你當你收到一個塊以後,應當怎樣去驗證它等等。這個高度抽象概括後的共識合約接口標準,咱們將其定義爲ACS4。

Dependency Inversion Principle (DIP,依賴倒置原則)

High level modules should not depend upon low level modules.

Both should depend upon abstractions.  Abstractions should not depend upon details. Details should depend upon abstractions.

高層模塊和底層模塊都應該依賴抽象。很直白。一句話:面向接口編程。咱們能夠把接口放在比較底層的模塊中,而後在高層的模塊中提供對這個接口的實現。不一樣的高層模塊之間的交流,也是經過底層模塊裏的接口來進行的。就好比咱們把日誌看成一個高層模塊,底層模塊中只有一個ILogger接口,提供一些打印不一樣等級的日誌的方法,具體打印的實現放在日誌模塊裏。其餘模塊可能都依賴日誌模塊才能打出日誌,能夠用NLog,也能夠用log4net,日誌模塊就變成了一個依賴項,具體用哪一個,要看咱們怎麼手動注入這個依賴。這裏終於談到了依賴注入。因此怎麼注入?很快咱們會說到組合根(Composition Root)。

DI,IoC,DIP的區別

這裏的DI是說DI技術,不是DI容器,咱們開發時用到的依賴注入框架,好比Autofac,Ninject,等等,都屬於DI容器。

DI實際上是一系列設計模式的組合。好比有一個很是重要的設計模式:Composition Root(組合根)。這個原本是接下來說的,咱們提早說一下,否則很差說明白單純的DI技術是怎麼用的。 組合根是一種設計模式,使用這個設計模式的重點在於咱們要找到放置組合根的位置,好在前人給了咱們經驗:儘量地靠近應用程序地入口點。那麼組合根是用來作什麼的?簡而言之,配置——若是簡單說配置可能會使人誤解組合根是一個DI容器——換個說法,設定,依賴關係。也就是這個應用程序中,哪一個抽象類型對應哪一個具體類型,都須要在組合根中經過某種方式進行設定。這裏說的抽象類型在C#中能夠是接口,也能夠是抽象類,事實上在應用DI的實踐中用那種方式進行抽象根本不重要,咱們只須要在組合根中設定好抽象類型對應具體類型的依賴關係就能夠了。這個設定在DI容器中能夠直接調用Register或者AddSingleton之類的方法,然而應用DI技術並不必定要用DI容器,咱們接下來會給一個不實用DI容器,可是使用了DI的技術或者說思想的最簡單的例子。

咱們從「以終爲始」的角度從新理解一下組合根這個概念:能夠想象,一個完整的能夠用於生產的應用,必定包含大量的服務,而若是咱們想讓這個應用程序的實現是鬆耦合的,就會用到接口隔離原則,也就是說,當咱們在某個服務中須要用到其餘服務時,由於寫代碼的時候不知道其餘服務會經過什麼實現,因此在代碼中,咱們直接調用一個抽象。而應用程序運行起來以後,在針對抽象的調用實際發生以前,就必定有一個裝配的過程,所謂的裝配就是根據代碼裏的抽象類型,在運行時給出一個具體的實現,這就要求咱們手動提供一個抽象類型和具體類型的映射關係。組合根就是手動提供這個關係的地方。最簡單的,控制檯應用,要跑來的話須要各個組件配合,高層組件對底層組件的依賴注入有不少種,目前看最經常使用的是構造方法注入,後面會說其餘的注入方式,這裏的代碼適用構造方法注入,你不能把一個接口丟進去,確定要本身手動new出來一個實現放進去,這個操做就是設定了抽象類型和具體類型的依賴關係。

這裏插一句,咱們回過頭看一下開閉原則,若是從組合根的角度看,當某一個功能須要修改的時候,咱們能夠不改變原有的實現,而是用裝飾模式從新提供一個實現,而後在組合根中將對應的依賴設定成新的實現,只須要添加代碼、改一下組合根就能夠了,新的實現有問題還能夠隨時撤回來,一直到肯定舊的實現不須要了,再消除這個冗餘的關係,這一步就是重構了。重構的時候如何肯定已經實現的feature不被影響?人老是不靠譜的,特別是把重構這個活交給別人的話。因此咱們要在重構前補充上足夠的單元測試,甚至一開始實現前就寫好單元測試。測試驅動設計(Test Driven Design)要求先寫單元測試,單元測試會fail,而後提供實現,直到測試經過,可是若是軟件開發到此爲止,只能稱做測試優先設計(Test First Design),測試驅動設計的核心是重構,解耦到滿意的地步,測試用例就是重構不會破壞功能的重要保證。

IoC,控制反轉,剛開始的時候就說過,DI實際上是IoC的一種實現,可是IoC這個概念從它提出的角度而言,更偏向於方向。可能有人聽過好萊塢原則:don't call me, I'll call you,怎麼理解呢,就是一個具體類型做爲一個依賴模塊,不須要關心本身什麼時候會被調用,怎麼調用,只須要等着本身被調用就好了,just do it。爲何叫好萊塢模式,由於若是在好萊塢,一個演員很火,很被市場須要,就像有一個模塊很被咱們正在寫的代碼須要同樣,那這個演員什麼都不須要關心,導演會拿着劇本去找他,或者找他的經紀人,演員只須要投入工做就能夠了。 最後說SOLID裏的DIP,它指明的是高層模塊不該該依賴底層模塊,它們應該都依賴抽象,從這裏看,DIP更加關注抽象的程度,也就是在具體實現上,代碼的形態。

可是咱們能夠粗略地認爲,這三個概念表達的是一個思想,若是有什麼東西必須提煉的話,那就是面向接口編程。並且要把接口放在底層,實現放在高層。

DI的三個維度

接下來咱們簡單說一下依賴注入這個技術的三個維度,它們是相輔相成的,

對象組合

也就是組合根實際上作的事情,即配置依賴關係。

生命週期管理

前面只說組合根是設定依賴關係的地方,其實在設定依賴關係的同時,依賴注入還要求管理每個對象的生命週期。

咱們已經知道,應用DI時,依賴都是在組合根中設定的,咱們也能夠把組合對象的對象或方法稱爲組合器(Composer),組合器是一個統一術語,就指組裝依賴的對象或者方法。一般而言,DI容器就是一個組合器。

因爲組合器的存在,對象註定管不了它的依賴的建立過程。那依賴的銷燬呢?若是依賴不及時銷燬,就會有內存泄露的風險。

用過.NET的都知道,GC會自動地回收不會被使用的對象,除非這個對象咱們實現了IDisposable接口,咱們才能夠本身親手銷燬對象。

在DI中,對象的生命週期是由組合器管理的。組合器能夠決定某個依賴對象是否在其餘不一樣的對象中共享,也能夠決定釋放對象的時機:是超出某一個消費者的做用域就釋放,仍是超出全部消費者的做用於才釋放。

對象生命週期的管理應該是依賴注入中最複雜的問題之一,時間關係就不展開講了,稍後咱們看一下微軟一個叫Extensions的repo裏爲依賴注入的實現提供支持的代碼,這個項目爲全部對象準備了三種lifestyle,定義在ServiceLifetime中。這個單詞自己的意思是生活方式或工做方式,這裏彷佛翻譯成生命週期類型比較好。這三種生命週期類型放在DI容器的角度可能更好理解,不過管理對象生命週期這個命題在不使用DI容器的時候也是存在的,這三種分類依然適用:

  • Singleton。整個應用程序中會一直共享某個抽象對應的同一個實例。

  • Scoped。在一個給定的做用於中使用單例,等同於Singleton,不一樣做用域就提供不一樣的實例。

  • Transient。每次請求都會返回一個剛new出來的實例。

攔截

理論上來講,若是代碼實現時遵照了SOLID原則,就能夠經過裝飾器模式來實現面向切面(AOP)編程。大體作法就是針對之前的XXService,建立其子類命名爲XXServiceDecorator,把XXService做爲構造方法參數注入XXServiceDecorator,在XXServiceDecorator從新實現XXService中的方法作攔截,通常而言就是直接調用,先後加條件。最後把在組合根設定上XXServiceDecorator而非XXService就好了。

不過實現AOP還有另外兩種方法,動態攔截(Dynamic Interception)(也叫動態代理)和編譯時織入(Compile-Time Weaving)(也叫IL編織)。

Demo

github.com/EanCuznaivy…

參考資料

stackoverflow.com/questions/1…

softwareengineering.stackexchange.com/questions/2…

martinfowler.com/articles/di…

相關文章
相關標籤/搜索