.NET應用架構設計—從新認識分層架構(現代企業級應用分層架構核心設計要素)

閱讀目錄:前端

  • 1.背景介紹
  • 2.簡要回顧下傳統三層架構
  • 3.企業級應用分層架構(現代分層架構的基本演變過程)
    • 3.1.服務層中應用契約式設計來解決動態條件不匹配錯誤(經過契約式設計模式來將問題在線下暴露出來)
    • 3.2.應用層中的應用控制器模式(經過控制器模式對象化應用層的職責)
    • 3.3.業務層中的命令模式(事務腳本模式的設計模式運用,很好的隔離靜態數據)
  • 4.服務層做爲SOA契約公佈後DTO與業務層的DomainModel共用基本的原子類型
  • 5.兩種獨立業務層職責設計方法(能夠根據具體業務要求來搭配)
    • 5.1.在應用層中的應用控制器中協調數據層與業務層的互動(業務層將絕對的獨立) 
    • 5.2.將業務層直接依賴數據層的關係使用IOC思想改變數據層依賴業務層(業務層將絕對獨立)(比較優雅)
  • 6.總結

1.背景介紹

接觸分層架構有段時間了,從剛開始的朦朦朧朧的理解到如今有必定深度的研究後,以爲有必要將本身的研究成果分享出來給你們,互相學習,也是對本身的一個總結。編程

咱們天天面對的項目結構能夠說幾乎都是分層結構的,或者是基於傳統三層架構演變過來的相似的分層結構,少不了業務層、數據層,這兩個層是比較重要的設計點,看似這兩個層是互相獨立的,可是這兩個層如何設計真的還有不少比較微妙的地方,本文將分享給你們我在工做中包括本身的研究中得出的比較可行的設計方法。後端

2.簡要回顧下傳統三層架構

其實這一節我原本不打算加的,關於傳統三層架構我想你們都應該瞭解或者很熟悉了,可是爲了使得本文的完整性,我仍是簡單的過一下三層架構,由於我以爲它可使得我後面的介紹有連貫性。設計模式

傳統三層架構指將一個系統按照不一樣的職責劃分層三個基本的層來分離關注點,將一個複雜的問題分解成三個互相協做的單元來共同的完成一個大任務。網絡

1.顯示層:用來顯示數據或從UI上獲取數據;該層主要是用來處理數據顯示和特效用的,不包括任何業務邏輯。多線程

2.業務層:業務層包含了系統中全部的核心業務邏輯,不包括任何跟數據顯示、數據存取相關的代碼邏輯。架構

3.數據層:用來提供對具體的數據源引擎的訪問,主要用來直接存取數據,不包括業務邏輯處理。框架

其實用文字描述這三個層的基本職責還非常比較容易的,可是不一樣的人如何理解並設計這三個層就形態萬千了,反正我是看過不少各類各樣的分層結構,各有各的特色,從某個角度講都很不錯,可是都顯得有點亂,由於沒有一個統一的架構模式來支撐,代碼中充滿了對分層架構的理解錯位的地方,好比:常常看見將「事物腳本」模式和「表模塊」模式混搭使用的,致使我最後都不知道把代碼寫在哪裏,提取出來的代碼也不知道該放到哪一個對象裏。dom

層雖簡單可是要想運用的好不容易,畢竟咱們仍是站在一個比較高的層面比較籠統的層面來談論分層結構的,一旦落實到代碼上就徹底不同了,用不用接口來隔離各層,接口放在哪一個層裏,這些都是很微妙的,固然本文不是爲了說明我所介紹的設計是多麼的好,而是給你們一個能夠參考的例子而已。ide

言歸正傳,三個層之間的調用要嚴格按照「上層只能調用直接下層,不可以越權,而下層也不可以調用本身的上層」,這是基本的層結構的調用約束,只有這樣才能保證一個好的代碼結構。顯示層只能調用業務層,業務層也只能調用數據層,其實就是這麼簡單,固然具體的代碼設計也能夠大概概括爲兩種,第一種是實例類或靜態類直接調用;第二種是每一個層之間加上接口來隔離每一個層,使得測試、部署容易點,可是若是用的很差的話效果不大反而會增長複雜度,還不如直接使用靜態類來的直接點,可是用靜態類來設計業務類會使多線程操做很難實施,稍微不注意就會串值或報錯。

3.企業級應用分層架構(現代分層架構的基本演變過程)

上節中咱們基本瞭解了傳統三層架構的類型和職責,本節咱們來簡單介紹一下現代企業應用分層架構的類型和職責。

隨着企業應用的複雜度增長,在原有三層架構上逐漸演化出如今的面向企業級的分層架構,這種架構能很好的支持新的技術和代碼上的最佳實踐。

在傳統的三層結構中的業務層之上多了一個應用層也但是說是服務層,該層是爲了直接隔離顯示層來調用業務層,由於如今的企業應用基本上都在往互聯網方向發展,對業務邏輯的訪問不會在是從進程內訪問了,而是須要跨越網絡來進行。

有了這一層以後會讓本來顯示層調用業務層的過程變得靈活不少,咱們能夠添加不少靈活性在裏面,更爲重要的是顯示層和業務層兩個獨立的層要想徹底獨立是必需要有一個層來輔助和協調他們之間的互動的。在最先的三層架構的書籍中其實也提到了「服務層」來協調的做用,爲何咱們不少的項目都未曾出現過,當我本身看到書上有講解後才恍然大悟。(該部分能夠參考:《企業應用架構模式》【馬丁.福勒】;第二部分,第9章「服務層」)

圖1:(邏輯分層)

應用層中包含了服務的設計部分,應用層的概念稍微大一點,裏面不只不含了服務還包含了不少跟服務不相關的應用邏輯,好比:記錄LOG,協調基礎設施的接入等等,就是將服務層放寬了理解。

圖2:(項目結構分層)

在應用層中包含了咱們上述所說的」服務「,將」服務層「放寬後造成了如今分層架構中相當重要的」應用層「。應用層將負責總體的協調」業務層「和」數據層「及「基礎設施」,固然還會包括系統運行時環境相關的東西。

3.1.服務層中應用契約式設計來解決動態條件不匹配錯誤(經過契約式設計模式來將問題在線下暴露出來)

此設計方法主要是想將動態運行時條件不匹配錯誤在線下自動化迴歸測試時就暴露出來。由於服務層中的契約可能會面臨着被修改的危險性,因此咱們沒法知道咱們本次上線的契約中是否包含了不穩定的條件不匹配的危險。

利用契約式設計模式能夠在調用時自動的執行契約發佈方預先設定的契約檢查器,契約檢查器分爲前置條件檢查器和後置條件檢查器;咱們來看一個簡單的例子;

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.Threading.Tasks; 
 6 
 7 namespace CompanySourceSearch.Service.Contract
 8 {
 9     using CompanySourceSearch.ServiceDto.Request;
10     using CompanySourceSearch.ServiceDto.Response; 
11 
12     public interface ISearchComputer
13     {
14         GetComputerByComputerIdResponse GetComputerByComputerId(GetComputerByComputerIdRequest request);
15     }
16 } 
View Code

在服務契約中我定義了一個用來查詢企業中電腦資源的接口,好的設計原則就是不要直接暴露查詢字段而是要將其封裝起來。

 1 namespace CompanySourceSearch.ServiceDto
 2 {
 3     public abstract class ContractCheckerBase
 4     {
 5         private Func<bool> checkSpecification;
 6         public Func<bool> CheckSpecification
 7         {
 8             get
 9             {
10                 return this.checkSpecification;
11             }
12             private set
13             {
14                 this.checkSpecification = value;
15             }
16         } 
17 
18         public void SetCheckSpecfication(Func<bool> checker)
19         {
20             CheckSpecification = checker;
21         } 
22 
23         public virtual bool RunCheck()
24         {
25             if (CheckSpecification != null)
26                 return CheckSpecification(); 
27 
28             return false;
29         }
30     }
31 } 
View Code

而後定義了一個用來表示契約檢查器的基類,這裏純粹是爲了演示目的,代碼稍微簡單點。服務契約的請求和響應都須要經過繼承這個檢查器類來實現自身的檢查功能。

 1 namespace CompanySourceSearch.ServiceDto.Request
 2 {
 3     public class GetComputerByComputerIdRequest : ContractCheckerBase
 4     {
 5         public long ComputerId { get; set; } 
 6 
 7         public GetComputerByComputerIdRequest()
 8         {
 9             this.SetCheckSpecfication(() => ComputerId > 0/*ComputerId>0的檢查規則*/);
10         }
11     }
12 }
View Code

Request類在構造函數中初始化了檢查條件爲:ComputerId必須大於0。

 1 namespace CompanySourceSearch.ServiceDto.Response
 2 {
 3     using CompanySourceSearch.ServiceDto; 
 4 
 5     public class GetComputerByComputerIdResponse : ContractCheckerBase
 6     {
 7         public List<ComputerDto> ComputerList { get; set; } 
 8 
 9         public GetComputerByComputerIdResponse()
10         {
11             this.SetCheckSpecfication(() => ComputerList != null && ComputerList.Count > 0);
12         }
13     }
14 } 
View Code

一樣Response類也在構造函數中初始化了條件檢查器爲:ComputerList不等於NULL而且Count要大於0。仍是那句話例子是簡單了點,可是設計思想很不錯。

對前置條件檢查器的執行能夠放在客戶端代理中執行,固然你也能夠自行去執行。後置條件檢查器其實在通常狀況下是不須要的,若是你能保證你所測試的數據是正確的,那麼做爲自動化測試是應該須要的,當時維護一個自動化測試環境很不容易,因此若是你用後置條件檢查器來檢查數據動態變化的環境時是不太合適的。

3.2.應用層中的應用控制器模式(經過控制器模式對象化應用層的職責)

應用層設計的時候大部分狀況下咱們都喜歡使用靜態類來處理,靜態類有着良好的代碼簡潔性,並且還能帶來必定的性能提高。可是從長遠來考慮靜態類存在一些潛在的問題,數據不能很好的隔離,重複代碼不太好提取,單元測試不太好寫。

爲了可以在很長的一段時間內似的項目維護性很高的狀況下仍是建議將應用控制器使用實例類設計,這裏我喜歡使用「應用控制器」來設計。它很形象的表達了協調前端和後端的職責,可是具體不處理業務邏輯,與MVC中的控制器很像。

 1 namespace CompanySourceSearch.ApplicationController.Interface
 2 {
 3     using CompanySourceSearch.Service.Contract;
 4     using CompanySourceSearch.ServiceDto.Response;
 5     using CompanySourceSearch.ServiceDto.Request; 
 6 
 7     public interface ISearchComputerApplicationController
 8     {
 9         GetComputerByComputerIdResponse GetComputerByComputerId(GetComputerByComputerIdRequest request);
10     }
11 } 
View Code

在應用控制器中咱們定義了一個用來負責上述查詢Computer資源的的控制器接口。

 1 namespace CompanySourceSearch.ApplicationController
 2 {
 3     using CompanySourceSearch.ApplicationController.Interface;
 4     using CompanySourceSearch.ServiceDto.Request;
 5     using CompanySourceSearch.ServiceDto.Response; 
 6 
 7     public class SearchComputerApplicationController : ISearchComputerApplicationController
 8     {
 9         public GetComputerByComputerIdResponse GetComputerByComputerId(GetComputerByComputerIdRequest request)
10         {
11             throw new NotImplementedException();
12         }
13     }
14 }
View Code

控制器實現類。這樣能夠很清晰的分開各個應用控制器,這樣對服務實現來講是個很不錯的提供者。

 1 namespace CompanySourceSearch.ServiceImplement
 2 {
 3     using CompanySourceSearch.Service.Contract;
 4     using CompanySourceSearch.ServiceDto.Response;
 5     using CompanySourceSearch.ServiceDto.Request;
 6     using CompanySourceSearch.ApplicationController.Interface; 
 7 
 8     public class SearchComputer : ISearchComputer
 9     {
10         private readonly ISearchComputerApplicationController _searchComputerApplicationController; 
11 
12         public SearchComputer(ISearchComputerApplicationController searchComputerApplicationController)
13         {
14             this._searchComputerApplicationController = searchComputerApplicationController;
15         } 
16 
17         public GetComputerByComputerIdResponse GetComputerByComputerId(GetComputerByComputerIdRequest request)
18         {
19             return _searchComputerApplicationController.GetComputerByComputerId(request);
20         }
21     }
22 } 
View Code

服務在使用的時候只須要使用IOC的框架將控制器實現直接注入進來就好了,固然這裏你能夠加上AOP用來記錄各類日誌。

經過將控制器按照這樣的方式進行設計能夠很好的進行單元測試和重構。

3.3.業務層中的命令模式(事務腳本模式的設計模式運用,很好的隔離靜態數據)

在通常的企業應用中大部分的業務層都是使用"事務腳本"模式來設計,因此這裏我以爲有個很不錯的模式能夠借鑑一下。可是不少事務腳本模式都是使用靜態類來處理的,這一點和控制器使用靜態類類似了,代碼比較簡單,使用方便。可是依然有着幾個問題,數據隔離,不便於測試重構。

將事務腳本使用命令模式進行對象化,進行數據隔離,測試重構都很方便,若是你有興趣實施TDD將是一個不錯的結構。

1 namespace CompanySourceSearch.Command.Interface
2 {
3     using CompanySourceSearch.DomainModel; 
4 
5     public interface ISearchComputerTransactionCommand
6     {
7         List<Computer> FilterComputerResource(List<Computer> Computer);
8     }
9 } 
View Code

事務命令控制器接口,定義了一個過濾Computer資源的接口。你可能看見了我使用到了一個DominModel的命名空間,這裏面是一些跟業務相關的且經過不斷重構抽象出來的業務單元(有關業務層的內容後面會講)。

 1 namespace CompanySourceSearch.Command
 2 {
 3     using CompanySourceSearch.Command.Interface; 
 4 
 5     public class SearchComputerTransactionCommand : CommandBase, ISearchComputerTransactionCommand
 6     {
 7         public List<DomainModel.Computer> FilterComputerResource(List<DomainModel.Computer> Computer)
 8         {
 9             throw new NotImplementedException();
10         }
11     }
12 } 
View Code

使用實例類進行業務代碼的組裝將是一個不會後悔的事情,這裏咱們定義了一個CommandBase類來作一些封裝工做。

應用控制器一樣和服務類同樣使用IOC的方式使用業務命令對象。

 1 namespace CompanySourceSearch.ApplicationController
 2 {
 3     using CompanySourceSearch.ApplicationController.Interface;
 4     using CompanySourceSearch.ServiceDto.Request;
 5     using CompanySourceSearch.ServiceDto.Response;
 6     using CompanySourceSearch.Command.Interface; 
 7 
 8     public class SearchComputerApplicationController : ISearchComputerApplicationController
 9     {
10         private readonly ISearchComputerTransactionCommand _searchComputerTransactionCommand;
11         public SearchComputerApplicationController(ISearchComputerTransactionCommand searchComputerTransactionCommand)
12         {
13             this._searchComputerTransactionCommand = searchComputerTransactionCommand;
14         } 
15 
16         public GetComputerByComputerIdResponse GetComputerByComputerId(GetComputerByComputerIdRequest request)
17         {
18             throw new NotImplementedException();
19         }
20     }
21 } 
View Code

到目前爲止每一個層之間堅持使用面向接口編程。

4.服務層做爲SOA契約公佈後DTO與業務層的DomainModel共用基本的原子類型

這裏有個矛盾點須要咱們平衡,當咱們定義服務契約時會定義服務所使用的DTO,而在業務層中爲了很好的凝聚業務模型咱們也定義了部分領域模型或者準確點講,在事務腳本模式的架構中咱們是經過不斷重構出來的領域模型,它封裝了部分領域邏輯。因此當服務中的DTO與領域模型中的實體須要使用相同的原子類型怎麼辦?好比某個類型的狀態等等。

若是純粹的隔離兩個層面,咱們徹底能夠定義兩套如出一轍的原子類型來使用,可是這樣會帶來不少重複代碼,難以維護。若是不定義兩套那麼又將這些共享的類型放在哪裏比較合適,放在DTO中顯示不合適,業務模型是不可能引用外面的東西的,若是放在領域模型中彷佛也有點不妥。

這裏我是採用將原子類型獨立一個項目來處理的,能夠相似於"CompanySourceSearch.DomainModel.ValueType"這樣的一個項目,它只包含須要與DTO進行共享的原子值類型。

5.兩種獨立業務層職責設計方法(能夠根據具體業務要求來搭配)

以前咱們沒有談業務層的設計,這裏咱們重點講一下業務層的設計包括與數據層的互操做。

從應用層開始考慮,當咱們須要處理某個邏輯時從應用控制器開始可能就會認爲直接進入到服務層了,而後服務層再去調用數據層,其實這只是設計的一種方式而已。這樣的設計方式好處就是簡單明瞭,實現起來比較方便。可是這種方法有個問題就是業務層始終仍是依賴數據層的,業務層的變更依然會受到數據層的影響。還有一個問題就是若是這個時候你使用不是「事務腳本」模式來設計業務層的話也會天然而然的寫成過程式代碼,由於你將本來用來協調的應用控制器沒有作到該作的事情,它實際上是用來協調業務層和數據層的,咱們並不必定非要在業務層中去調用數據層,而是能夠將業務層須要的數據從控制器中獲取好而後傳入到業務層中去處理,這和直接在業務層中去調用數據層是差很少的,只不過是寫代碼的時候不能按照過程式的思路來寫了。

無論咱們是使用事務腳本模式仍是表模塊模式或者當下比較流行的領域模型模式,均可以使用這種方法進行設計。

5.1.在應用層中的應用控制器中協調數據層與業務層的互動(業務層將絕對的獨立)

咱們將在應用控制器中去調用數據層的方法拿到數據而後轉換成領域模型進行處理。

namespace CompanySourceSearch.Database.Interface
{
    using CompanySourceSearch.DatasourceDto; 

    public interface IComputerTableModule
    {
        List<ComputerDto> GetComputerById(long cId);
    }
} 
View Code

咱們使用"表入口「數據層模式來定義了一個用來查詢Computer的方法。

 1 namespace CompanySourceSearch.ApplicationController
 2 {
 3     using CompanySourceSearch.ApplicationController.Interface;
 4     using CompanySourceSearch.ServiceDto.Request;
 5     using CompanySourceSearch.ServiceDto.Response;
 6     using CompanySourceSearch.Command.Interface;
 7     using CompanySourceSearch.Database.Interface;
 8     using CompanySourceSearch.DatasourceDto;
 9     using CompanySourceSearch.Application.Common; 
10 
11     public class SearchComputerApplicationController : ISearchComputerApplicationController
12     {
13         private readonly ISearchComputerTransactionCommand _searchComputerTransactionCommand;
14         private readonly IComputerTableModule _computerTableModule;
15         public SearchComputerApplicationController(ISearchComputerTransactionCommand searchComputerTransactionCommand,
16             IComputerTableModule computerTableModule)
17         {
18             this._searchComputerTransactionCommand = searchComputerTransactionCommand;
19             this._computerTableModule = computerTableModule;
20         } 
21 
22         public GetComputerByComputerIdResponse GetComputerByComputerId(GetComputerByComputerIdRequest request)
23         {
24             var result = new GetComputerByComputerIdResponse(); 
25 
26             var dbComputer = this._computerTableModule.GetComputerById(request.ComputerId);//從數據源中獲取Computer集合
27             var dominModel = dbComputer.ConvertToDomainModelFromDatasourceDto();//轉換成DomainModel 
28 
29             var filetedModel = this._searchComputerTransactionCommand.FilterComputerResource(dominModel);//執行業務邏輯過濾 
30 
31             return result; 
32 
33         }
34     }
35 }
View Code

控制器中不直接調用業務層的方法,而是先獲取數據而後執行轉換在進行業務邏輯處理。這裏須要澄清的是,此時我是將讀寫混合在一個邏輯項目裏的,因此大部分的查詢沒有業務邏輯處理,直接轉換成服務DTO返回便可。將讀寫放在一個項目能夠共用一套業務邏輯模型。固然僅是我的見解。

這個是業務層將是徹底獨立的,咱們能夠對其進行充分的單元測試,包括遷移和公用,甚至你能夠想着領域特定框架發展。

5.2.將業務層直接依賴數據層的關係使用IOC思想改變數據層依賴業務層(業務層將絕對獨立)(比較優雅)  

上面那種使用業務層和數據層的方式你也許以爲有點彆扭,那麼就換成使用本節的方式。

以往咱們都是在業務層中調用數據層的接口來獲取數據的,此時咱們將直接依賴數據層,咱們能夠借鑑IOC思想,將業務層依賴數據層進行控制反轉,讓數據層依賴咱們業務層,業務層提供依賴注入接口,讓數據層去實現,而後在業務命令對象初始化的時候在動態的注入數據層實例。

若是你已經習慣了使用事物腳本模式來開發項目,不要緊,你可使用此模式來將數據層完全的隔離出去,你也能夠試着在應用控制器中幫你分擔點事物腳本的外圍功能。

6.總結

文章中分享了本人以爲到目前來講比較可行的企業應用架構設計方法,並不能說徹底符合你的口味,可是能夠是一個不錯的參考,因爲時間關係到此結束,謝謝你們。

 

相關文章
相關標籤/搜索