【閱讀筆記】軟體構築美學(08)應用程式的重新分層

應用程式的分層。重構分層。各層之間的討論。

書名:軟體構築美學

作者: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 來幫助,讓我們可以專注在分層上,也可以互相討論自己是如何設計接合口的

也可以先開個會議討論,分享彼此的看法,也是有幫助的。

 

了解何時停止

應該定期地瞭解一下手邊重構的任務,並問問自己是否有繼續下去的價值,或是有其他更重要的工作等著我們

 

總結

透過分層的方式,可以讓我們應用程式更接近物件導向原則,這對於棕地專案是一個重要的議題,

因為它本身不具分層、零散的應用或是定義不明確。

定義一個良好的契約,以及幫助隔離外部程式碼變動的反腐敗層。

說明了垂直層以及簡單了解剖面導向程式設計,它提供了一個橫切關注點的機制,例如:記錄或是效能監控。

以領域為中心的方式重構,並且說明他的好處,也解釋如何進行重構,如何衡量進展,以及停止重構的時機點。