閱讀目錄:
- 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 }
在服務契約中我定義了一個用來查詢企業中電腦資源的接口,好的設計原則就是不要直接暴露查詢字段而是要將其封裝起來。

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 }
然后定義了一個用來表示契約檢查器的基類,這里純粹是為了演示目的,代碼稍微簡單點。服務契約的請求和響應都需要通過繼承這個檢查器類來實現自身的檢查功能。

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 }
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 }
同樣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 }
在應用控制器中我們定義了一個用來負責上述查詢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 }
控制器實現類。這樣可以很清晰的分開各個應用控制器,這樣對服務實現來說是個很不錯的提供者。

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 }
服務在使用的時候只需要使用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 }
事務命令控制器接口,定義了一個過濾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 }
使用實例類進行業務代碼的組裝將是一個不會后悔的事情,這里我們定義了一個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 }
到目前為止每個層之間堅持使用面向接口編程。
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); } }
我們使用"表入口“數據層模式來定義了一個用來查詢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 }
控制器中不直接調用業務層的方法,而是先獲取數據然后執行轉換在進行業務邏輯處理。這里需要澄清的是,此時我是將讀寫混合在一個邏輯項目里的,所以大部分的查詢沒有業務邏輯處理,直接轉換成服務DTO返回即可。將讀寫放在一個項目可以共用一套業務邏輯模型。當然僅是個人看法。
這個是業務層將是完全獨立的,我們可以對其進行充分的單元測試,包括遷移和公用,甚至你可以想着領域特定框架發展。
5.2.將業務層直接依賴數據層的關系使用IOC思想改變數據層依賴業務層(業務層將絕對獨立)(比較優雅)
上面那種使用業務層和數據層的方式你也許覺得有點別扭,那么就換成使用本節的方式。
以往我們都是在業務層中調用數據層的接口來獲取數據的,此時我們將直接依賴數據層,我們可以借鑒IOC思想,將業務層依賴數據層進行控制反轉,讓數據層依賴我們業務層,業務層提供依賴注入接口,讓數據層去實現,然后在業務命令對象初始化的時候在動態的注入數據層實例。
如果你已經習慣了使用事物腳本模式來開發項目,沒關系,你可以使用此模式來將數據層徹底的隔離出去,你也可以試着在應用控制器中幫你分擔點事物腳本的外圍功能。
6.總結
文章中分享了本人覺得到目前來說比較可行的企業應用架構設計方法,並不能說完全符合你的口味,但是可以是一個不錯的參考,由於時間關系到此結束,謝謝大家。