架構原則

提示

本內容節錄自《使用 ASP.NET Core 和 Azure 架構現代化 Web 應用程式》電子書。可以從 .NET Docs 取得,也可以免費下載 PDF 離線閱讀。

Architect Modern Web Applications with ASP.NET Core and Azure eBook cover thumbnail.

「如果營造商要像程式設計人員撰寫程式那樣地蓋房子,那麼第一隻經過的啄木鳥將會摧毀文明。」
- Gerald Weinberg

您在架構與設計軟體解決方案時,應該惦記著可維護性。 本節中所述的準則可協助引導您做出將產生可維護之全新應用程式的架構決策。 一般而言,這些原則會引導您利用與應用程式其他部分未緊密結合,而是透過明確介面或傳訊系統進行通訊的不同元件來建置應用程式。

一般設計原則

關注點分離

開發時的一項指導原則是關注點分離。 這個原則判斷提示軟體應該根據它所執行的工作種類來分離。 比方說,假設應用程式中包含邏輯來識別值得注意的項目,以便顯示給使用者,而且特別格式化此類項目,使其更容易被注意到。 負責選擇要格式化之項目的行為,應該與負責格式化項目的行為分離,因為這些行為牽涉的是其他問題,只是碰巧彼此相關。

在架構上,您可以藉由從基礎結構和使用者介面邏輯,來分離核心商務行為,並以合乎邏輯的方式組建應用程式,以遵循這個原則。 在理想情況下,商務規則和邏輯應該位於個別的專案,且不應依賴應用程式中的其他專案。 此區隔有助於確保商務模型可以輕易進行測試,且不須緊密結合至低階實作詳細資料即可持續改進 (若基礎結構考量相依於定義於商務層中的抽象概念,亦有所幫助)。 關注點分離是在應用程式架構中使用層級背後的一項重要考量。

封裝

應用程式的不同部分應該使用封裝,將它們與應用程式的其他部分隔離。 應用程式元件和層級應該能夠調整內部實作,而且只要不違反外部的合約,便不必中斷其共同作業者。 正確地使用封裝可協助達到應用程式設計中的鬆散結合和模組化,因為物件與套件可以取代為替代的實作,只要維持相同的介面即可。

在類別中,封裝是藉由限制對類別內部狀態的外部存取而達成。 如果外部執行者想要操作物件的狀態,則應透過妥善定義的函式 (或屬性 setter) 來達成,而不是直接存取物件的私用狀態。 同樣地,應用程式元件和應用程式本身應該公開妥善定義的介面,供它們的共同作業者來使用,而不是允許直接修改它們的狀態。 此方法會鬆綁應用程式的內部設計,讓設計能持續改進,且您不必擔心這麼做會中斷共同作業者的工作,只要維護公用合約即可。

可變動的全域狀態是與封裝相對的狀態。 若值是從一個函式的可變動全域狀態擷取,則不能保證此值在另一個函式中也有相同的值 (即使是相同函式,此值也未必相同)。 了解可變動全域狀態的相關問題,是 C# 等程式設計語言支援不同範圍規則的原因之一。這些規則的應用範圍相當廣泛,從陳述式、方法到類別皆適用。 值得注意的是,若依賴中央資料庫在應用程式之內和之間整合的資料驅動架構,本身即等於選擇依賴資料庫所代表的可變動全域狀態。 網域驅動設計和全新架構的一大重要考量事項,即如何封裝對資料的存取,以及如何確保應用程式狀態不因為直接存取行為,而導致其持續性格式失效。

相依性反轉

應用程式內的相依性方向應該是抽象的方向,而不是實作詳細資料。 以大部分的應用程式的撰寫方式而言,其編譯時間相依性都會以執行階段執行的方向流動,因而繪製出直接相依性關係圖。 亦即,如果類別 A 呼叫類別 B 的方法,而類別 B 會呼叫類別 C 的方法,則在編譯時間中,類別 A 將相依於類別 B,而類別 B 將相依於類別 C,如圖 4-1 所示。

Direct dependency graph

圖 4-1. 直接相依性圖形。

套用相依性反轉原則後,會允許 A 呼叫由 B 實作之抽象的方法,使得 A 能夠在執行階段呼叫 B,但在編譯時間內,B 會相依於由 A 控制的介面 (因此反轉一般的編譯時間相依性)。 在執行階段,程式執行流程維持不變,但是介面的引進表示可以輕鬆地插入這些介面的不同實作。

Inverted dependency graph

圖 4-2. 反轉相依性圖形。

相依性反轉是組建鬆散結合應用程式的重要部分,因為這可將實作詳細資料撰寫為相依於較高的抽象層級,並撰寫為實作較高的抽象層級,而不須採用其他方式。 因此,所產生的應用程式會比較可測試、模組化且可維護。 遵循相依性反轉準則,即可達成「相依性插入」

明確相依性

方法和類別應該明確需要正常運作所需的任何共同作業物件。 其稱為明確相依性準則。 類別建構函式會提供一個機會,讓類別能識別它們處於有效狀態並正常運作所需的項目。 如果您定義的類別可以建構和呼叫,但只有在特定全域或基礎結構元件已就緒時,才能正常運作,那麼這些類別對其用戶端便不誠實。 建構函式合約告訴用戶端它只需要指定的項目 (如果類別只使用無參數建構函式則可能沒有任何項目),但在執行階段變成物件確實需要其他項目。

藉由遵循明確的相依性原則,您的類別和方法對其用戶端便會誠實告知他們需要要什麼才能運作。 如遵循以上原則,可讓您的程式碼更充分達到自我記錄,您的程式碼合約也能更易懂易記,因為使用者會相信,只要他們以方法或建構函式參數的形式提供所需項目,他們正在使用的物件便會在執行階段正確運作。

單一責任

單一責任原則適用於物件導向設計,但也可視為類似關注點分離的架構原則。 它指出物件應該只有一項責任,而且應該只有一個變更的原因。 具體而言,物件唯一應該變更的情況是,它執行它的一項責任的方式必須更新時。 遵循此原則有助於產生更鬆散結合且模組化的系統,因為有許多種新行為可實作為新的類別,而不是讓現有類別再承擔更多責任。 新增類別一比變更現有類別安全,因為還沒有程式碼相依於新的類別。

在整合應用程式中,我們可以在高層級套用單一責任原則至應用程式中的層級。 簡報責任應保留在 UI 專案,而資料存取責任則應保留在基礎結構專案中。 商務邏輯應該保留在應用程式核心專案,在這裡它可以輕鬆地測試,且可以獨立於其他責任之外地持續改進。

此原則套用至應用程式架構,並帶到其邏輯端點後,您便會得到微服務。 指定的微服務應該具有單一責任。 如果您需要擴充系統的行為,通常新增其他微服務會比較好,而不要新增責任至現有的微服務。

深入了解微服務架構

不重複原則 (DRY)

應用程式應避免在多個位置指定與特定概念相關的行為,因為此做法正是常見的錯誤來源。 在某些情況下,需求變更後,此行為也必須變更。 可能至少會有一個行為執行個體無法更新,系統的行為也會變得不一致。

請不要複製邏輯,而是要將它封裝在程式設計建構中。 讓此建構成為此行為的單一授權,而且讓應用程式中需要這個行為的任何其他部分都使用新建構。

注意

避免將湊巧重複的行為繫結在一起。 例如,只是兩個不同的常數具有相同的值,並不表示您應該只有一個常數,如果在概念上它們是指不同項目的話。 複製一向偏好結合錯誤的抽象概念。

持續性無知

續性無知 (PI) 指的是需要持續的,但其程式碼不會受到持續性技術選項影響的類型。 這類的類型在 .NET 中有時稱為簡單的 CLR 物件 (POCO),因為它們不需要繼承特定的基底類別或實作特定介面。 持續性無知的價值在於它允許以多種方式保存相同的商務模型,為應用程式提供額外的彈性。 持續性選項可能會隨著時間變更,從一種資料庫技術變為另一種技術,或是除了應用程式一開始的選項之外,可能還需要其他形式的持續性 (例如,除了關聯式資料庫之外,還使用 Redis 快取或 Azure Cosmos DB)。

違反這個原則的一些範例包括:

  • 必要的基底類別。

  • 必要的介面實作。

  • 負責自行儲存的類別 (例如使用中的記錄模式)。

  • 需要無參數建構函式。

  • 需要虛擬關鍵字的屬性。

  • 持續性特定的必要屬性。

類別有任何上述功能或行為的要求,會為要持續保存的類型與持續性技術選擇之間新增結合,使得更難以在未來採用新的資料存取策略。

繫結內容

繫結內容是 Domain-Driven 設計的中心模式。 它們藉由將大型應用程式或組織的複雜性分成不同的概念模組,提供處理複雜性的方法。 每個概念模組都代表與其他內容不同的內容 (因此而繫結),並可獨立持續改進。 每個繫結內容在理想情況下應該能自由選擇自己的概念名稱,而且應該對它自己的持續性存放區具有獨佔存取權。

至少,個別 Web 應用程式應該致力於成為自己的繫結內容,並且具有自己商務模型的持續性存放區,而不與其他應用程式共用一個資料庫。 繫結內容之間的通訊會透過程式設計介面進行,而不是透過共用的資料庫,這樣可讓商務邏輯和事件發生以回應發生的變更。 繫結內容與微服務密切對應,而微服務在理想的情況下也實作為自己的個別繫結內容。

其他資源