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

王清培發表於2014-08-19

閱讀目錄:

  • 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.資料層:用來提供對具體的資料來源引擎的訪問,主要用來直接存取資料,不包括業務邏輯處理。

其實用文字描述這三個層的基本職責還很是比較容易的,但是不同的人如何理解並設計這三個層就形態各異了,反正我是看過很多各種各樣的分層結構,各有各的特點,從某個角度講都很不錯,但是都顯得有點亂,因為沒有一個統一的架構模式來支撐,程式碼中充滿了對分層架構的理解錯位的地方,比如:經常看見將“事物指令碼”模式和“表模組”模式混搭使用的,導致我最後都不知道把程式碼寫在哪裡,提取出來的程式碼也不知道該放到哪個物件裡。

層雖簡單但是要想運用的好不容易,畢竟我們還是站在一個比較高的層面比較籠統的層面來談論分層結構的,一旦落實到程式碼上就完全不一樣了,用不用介面來隔離各層,介面放在哪個層裡,這些都是很微妙的,當然本文不是為了說明我所介紹的設計是多麼的好,而是給大家一個可以參考的例子而已。

言歸正傳,三個層之間的呼叫要嚴格按照“上層只能呼叫直接下層,不能夠越權,而下層也不能夠呼叫自己的上層”,這是基本的層結構的呼叫約束,只有這樣才能保證一個好的程式碼結構。顯示層只能呼叫業務層,業務層也只能呼叫資料層,其實就是這麼簡單,當然具體的程式碼設計也可以大概歸納為兩種,第一種是例項類或靜態類直接呼叫;第二種是每個層之間加上介面來隔離每個層,使得測試、部署容易點,但是如果用的不好的話效果不大反而會增加複雜度,還不如直接使用靜態類來的直接點,但是用靜態類來設計業務類會使多執行緒操作很難實施,稍微不注意就會串值或報錯。

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.總結

文章中分享了本人覺得到目前來說比較可行的企業應用架構設計方法,並不能說完全符合你的口味,但是可以是一個不錯的參考,由於時間關係到此結束,謝謝大家。

 

相關文章