應用程式的分層。重構分層。各層之間的討論。
書名:軟體構築美學
作者:Kyle Baley,Donald Belcham
譯者:蔡煥麟,張簡才祿
發行公司:悅知文化,精誠資訊股份有限公司
程式碼該如何分層
- 封裝(encapsulation)
- 關注點分離(separation of concerns)
分層(layer)
- 資料存取層(data access layer):應用程式與資料儲存容器之間的溝通
- 單獨的組件(assembly):明確界定層次的實體邊界
問題描述
當系統某一個地方改變時,此時受影響的程式碼卻是散落在各地。
若將程式碼的設計混雜在一起,或是未善用關注點分離的技巧,將會導致開發團隊遇到許多問題。
邏輯層與實體層
實體層(Tier)
典型的三層式架構
- 使用者介面(user interface)
- 商業邏輯(business logic)
- 資料儲存容器(data storage repository)
多層架構,可以讓不同應用程式重複利用
- 資料儲存層可以同時讓多個應用系統使用
- 建立多種不同的 UI 來儲存商業邏輯層
在實體層下,我們需要在程式碼的顆粒度上進行更細化的動作
對於實體層來說,它無法更細的對應到應用程式的每一層,因此就有了邏輯層的概念
邏輯層(Layer)
邏輯層本身不會扯到硬體的部分,它會偏向於軟體的設計,或是根據不同的任務來切分。
邏輯層與實體層之間的區別,往往在於實體層包含一到多個邏輯層。
- 使用者介面:View、控制器
- 商業邏輯:服務層、領域模型、資料容器層,其他
一個邏輯層可以跨越在多個實體層,比如說 log 紀錄以及例外處理,會透過公用函式庫的專案來達成。
邏輯層的好處
- 可以跟著不同的關注點來定義程式碼要擺放的位置
- 明確的分離與相鄰邏輯層之間的互動關係,最好的方式透過 interface 介面來設計。
使用介面的方式來隔離
介面讓不同類別透過已定義的方法、屬性或是共通的約定,讓彼此之間可以互動。
透過介面建立好相關的合約(contract),每一個類別(包含生產或消費)就可以各自獨立實作,
這是因為介面已經清楚的定義這些類別的權責,以及要傳入的參數和回傳值,剩下的就是如何讓類別來實作這些介面。
可以有效地幫助我們定義邏輯層的部分,並且在變更應用程式的功能時,可以幫助我們隔離程式碼,而不需相互牽連修改。
可以將邏輯層想像成是第三方的元件,並且裡面包含一些可以執行的程式碼,而透過介面所設計的合約,
事實上,這也定義邏輯層的一種方式,因此透過外部建立的一個函式庫,介面可以幫助我們清楚的切分。
防止邏輯層腐敗
反腐敗層(auto-corruption layer)
一個明確定義好的合約,客戶端程式可以不必因為邏輯層的修改,而受影響波及。
在這樣的環境下修改程式碼,並不會影響到呼叫的程式碼。
接合口(seam)
是一種介面,並且提供邏輯層彼此之間溝通的橋樑。
主要是為了降低程式碼之間的耦合,若應用程式能夠遵循以介面為基礎的設計方式,在這些邏輯層之間的銜接,就可以透過接合口來降低彼此之間的耦合度。
邏輯層、接合口,以及基於介面設計的方式
在程式的設計過程中,若結合了邏輯層、接合口,以及基於介面設計的方式,可以將不同區塊的程式碼,讓它們彼此之間是完整的獨立開來,
這種程式碼隔離的做法,讓修改現有的程式變得更加容易,並可以大大的降低程式碼之間的影響。
垂直層(vertical layer)
垂直層所關注的地方會是在如何跨越多個實體層,log 紀錄就是一個很好的例子,其他的可能包含安全檢查、錯誤檢查、效能監控、以及交易等。
這些都很適合用在跨越不同層上的使用。
但系統內直接呼叫垂直層,這樣的做法會增加程式碼的干擾與混亂度。
剖面導向程式設計(Aspect-oriented programming)
運用關注點分的方式,讓程式碼需透過傳統的方式來隔離。它讓我們可以正確使用垂直層。
它能夠將外來的基礎功能,獨立成一個基礎的類別,使用自訂屬性(custom attribute)
ActionFiler 的用法
建立一個 LoggerAttribute.cs 類別,繼承 ActionFilterAttribute ,override OnActionExecuting / OnActionExecuted 使用 Trace
public class LoggerAttribute : ActionFilterAttribute{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
Trace.TraceInformation("OnActionExecuting Log Begin");
base.OnActionExecuting(filterContext);
}
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
Trace.TraceInformation("OnActionExecuted Log End");
base.OnActionExecuted(filterContext);
}
}
只要在 Action or Controller 加上自訂屬性標籤,達成垂直層的套用。
[Logger]
public class HomeController : BaseController
大多數的 AOP 框架,它們會去攔截呼叫的方法,並且在方法執行的前後,會先去呼叫垂直層自定義的執行動作。
攔截者(interceptor)的程式碼大多會封裝在垂直層中,這裡的紀錄就是一個例子。
現在程式碼的所有記錄功能,都透過一個集中的地方來處理,並且類別與方法不再受其他程式碼的干擾。
以領域為中心的方法(domain-centric)
傳統資料請求的 N 層設計,雖然我們將領域部分顯示在這裡,不過他通常不會存在於個別的邏輯層上。
以領域為中心的方法,並且會封裝商業邏輯與領域物件
以領域為中心的架構,它凸顯出領域的重要,就如應用程式的中心一樣
重構分層
- 選擇一個起始點
- 進行逐步改善
- 重構另一個畫面
- 確認持續進展
- 了解何時停止
選擇一個起始點
出發點應該是為了能夠測試,只有足夠的測試才能確保運作的正確性
選擇向上來重構(從下到 UI 層)或是向下分割(從 UI 端往下)
建議是向下分割,從 UI 端開始往下進行,可以減少每一層的責任(會比較清楚目前只要用到哪些功能)
這也是為了方便程式碼的重構。
進行逐步改善
通常會從小步驟開始進行,並且讓它逐漸擴散到其餘的地方
例如:從變數重新命名到資料存取方式的大翻修。
我們強調要早點檢查並且經常檢查,同樣的,重構也需要用這種方式進行
以更小的方式進行:可逆的步驟、支援單元測試,以及整合測試
千萬別立刻重構程式碼,而是以單一的接合口開始進行
當我們建立好介面(程式碼上的介面)的同時,就會知道該如何實作這些接合口
然而這一整塊的程式碼,只要將它移至各自的類別上,並且每個類別會實作特定的介面
之後 UI 畫面就可以透過這些介面來操作
重構另一個畫面
當完成第一個畫面後,會有以下兩個好處
- 這些邏輯層將被定義好
- 經歷過一次練習後,或許你會想到更好的主意
一旦邏輯層被定義好,我們的框架也將慢慢成形,不過不同的畫面不一定會重複用到相同的邏輯層
但這樣的過程有助於我們去思考如何建構一個良好的邏輯層
確認持續進展
在重構的過程中,很容易陷入一種乏味的狀況。
可以藉由非正式的 code review 來幫助,讓我們可以專注在分層上,也可以互相討論自己是如何設計接合口的
也可以先開個會議討論,分享彼此的看法,也是有幫助的。
了解何時停止
應該定期地瞭解一下手邊重構的任務,並問問自己是否有繼續下去的價值,或是有其他更重要的工作等著我們
總結
透過分層的方式,可以讓我們應用程式更接近物件導向原則,這對於棕地專案是一個重要的議題,
因為它本身不具分層、零散的應用或是定義不明確。
定義一個良好的契約,以及幫助隔離外部程式碼變動的反腐敗層。
說明了垂直層以及簡單了解剖面導向程式設計,它提供了一個橫切關注點的機制,例如:記錄或是效能監控。
以領域為中心的方式重構,並且說明他的好處,也解釋如何進行重構,如何衡量進展,以及停止重構的時機點。