「如果您認為良好的架構很昂貴,不妨試試看拙劣的架構。」 - 布賴恩·福特和約瑟夫·約德
大部分的傳統 .NET 應用程式會部署為對應至可執行檔或單一 WEB 應用程式在單一 IIS 應用程式域內執行的單一單位。 這種方法是最簡單的部署模型,而且非常適用於許多內部和較小的公用應用程式。 不過,即使考慮到這個單一部署單位,大部分非簡單的商務應用程式都會從一些邏輯區隔中的數個層次獲益。
什麼是整合型應用程式?
整合型應用程式是完全獨立的應用程式,就其行為而言。 它可能會在執行作業的過程中與其他服務或數據存放區互動,但其行為的核心會在自己的進程中執行,而且整個應用程式通常會部署為單一單位。 如果這類應用程式需要水平調整,通常整個應用程式會跨多部伺服器或虛擬機重複。
全方位應用程式
應用程式架構的最小可能項目數目是一個。 在此架構中,應用程式的整個邏輯會包含在單一專案中、編譯成單一元件,並部署為單一單位。
新的 ASP.NET Core 專案,無論是在 Visual Studio 中建立,還是從命令行建立,都會以簡單的「一對一」整合型開始。 它包含應用程式的所有行為,包括簡報、商務和數據存取邏輯。 圖 5-1 顯示單一專案應用程式的檔案結構。
圖 5-1。 單一專案的 ASP.NET Core 應用程式。
在單一專案案例中,會透過使用資料夾來區分關注點。 默認範本包含模型、檢視和控制器之MVC模式責任的不同資料夾,以及數據與服務的其他資料夾。 在此安排中,簡報詳細數據應盡可能受限於 Views 資料夾,而數據存取實作詳細資料應限制在 Data 資料夾中保留的類別。 商業規則應該位於 Models 資料夾內的服務和類別中。
雖然簡單,但單一專案整合型解決方案有一些缺點。 隨著專案的大小和複雜度成長,檔案和資料夾的數目也會繼續成長。 使用者介面 (UI) 考慮(模型、檢視、控制器)位於多個資料夾中,這些資料夾不會依字母順序分組。 當其他如篩選器或「ModelBinders」等UI層級結構被添加到各自的資料夾中時,這個問題只會愈來愈嚴重。 商業規則分散在模型和服務資料夾之間,而且沒有清楚指出哪些類別應該相依於哪些資料夾。 此專案層級的組織不足經常會導致 雜亂無章的代碼。
為了解決這些問題,應用程式通常會演變成多項目解決方案,其中每個專案都會被視為位於應用程式的特定 層 。
什麼是圖層?
隨著應用程式的複雜性增加,管理複雜性的一種方法是根據其責任或關注點拆分應用程式。 此方法遵循關注點分離原則,有助於維持日益龐大的程式碼庫的組織,使開發人員能夠輕鬆找到特定功能的實作位置。 不過,分層架構除了程式代碼組織之外,還提供許多優點。
藉由將程式代碼組織成圖層,即可在整個應用程式中重複使用常見的低階功能。 這種重複使用是有益的,因為它表示撰寫的程式代碼較少,而且因為它可以讓應用程式在單一實作上標準化,因為遵循 不要重複您自己 (DRY) 原則。
使用分層架構,應用程式可以強制執行哪些層可以與其他層通訊的限制。 此架構有助於達成封裝。 當圖層變更或取代時,應該只影響與其一起工作的圖層。 藉由限制哪些層相依於其他層,可以減輕變更的影響,讓單一變更不會影響整個應用程式。
圖層(和封裝)可讓您更輕鬆地取代應用程式內的功能。 例如,應用程式一開始可能會使用自己的 SQL Server 資料庫進行持續性,但稍後可以選擇使用雲端式持續性策略,或 Web API 後置的一個。 如果應用程式已在邏輯層中正確封裝其持續性實作,該SQL Server 特定層可以由實作相同公用介面的新層取代。
除了交換實作以回應未來需求變更的可能性外,應用層也可以更輕鬆地交換實作以供測試之用。 這些層不必撰寫針對應用程式實際數據層或 UI 層運作的測試,而是可以在測試時間以假實作取代這些層,以提供對要求的已知回應。 相較於針對應用程式的實際基礎結構執行測試,這種方法通常會讓測試更容易撰寫,而且執行得更快。
邏輯分層是改善企業軟體應用程式中程式代碼組織方式的常見技術,而且有數種方式可將程式代碼組織成圖層。
備註
層次 代表應用程式中的邏輯分隔。 如果應用程式邏輯實際分散到不同的伺服器或進程,則這些個別的實體部署目標稱為 階層。 存在將 N 層架構的應用程式部署到單一層的可能性,且這種情況相當常見。
傳統的「N 層」架構應用程式
最常見的應用程式邏輯組織到層中,如圖 5-2 所示。
圖 5-2。 一般應用層。
這些層通常縮寫為UI、BLL(商業規則層)和 DAL(資料存取層)。 使用此架構,用戶會透過UI層提出要求,該層只會與BLL互動。 接著,BLL 可以呼叫 DAL 以取得數據存取要求。 UI 層不應直接對 DAL 提出任何要求,也不應該透過其他方式直接與持續性互動。 同樣地,BLL 應該只透過 DAL 與資料持久性互動。 如此一來,每一層都有自己的已知責任。
這種傳統分層方法的其中一個缺點是編譯時間相依性會從上到下執行。 也就是說,UI 層相依於 BLL,這取決於 DAL。 這表示 BLL 通常保存應用程式中最重要的邏輯,取決於數據存取實作詳細數據(通常取決於資料庫是否存在)。 在這類架構中測試商業規則通常很困難,需要測試資料庫。 相依性反轉原則可用來解決此問題,如下一節所示。
圖 5-3 顯示範例解決方案,依責任(或層級)將應用程式分成三個專案。
圖 5-3。 具有三個專案的簡單整合型應用程式。
雖然此應用程式會針對組織用途使用數個專案,但它仍會部署為單一單位,且其用戶端會以單一 Web 應用程式的形式與其互動。 這可讓您進行非常簡單的部署過程。 圖 5-4 顯示如何使用 Azure 來裝載這類應用程式。
圖 5-4。 Azure Web 應用程式的簡單部署
隨著應用程式需求的成長,可能需要更複雜的且健全的部署解決方案。 圖 5-5 顯示支援其他功能的較複雜部署計劃範例。
圖 5-5。 將 Web 應用程式部署至 Azure App Service
在內部,此專案按責任劃分為多個子專案,以改善應用程式的可維護性。
此單元可以向上擴展或向外擴展,以利用雲端的隨選擴展能力。 擴展表示將額外的 CPU、記憶體、磁碟空間或其他資源添加到託管您應用程式的伺服器。 水平擴展指的是新增這類伺服器的額外實例,無論是實體伺服器、虛擬機或容器。 當您的應用程式裝載於多個實例時,負載平衡器會用來將要求指派給個別應用程式實例。
在 Azure 中調整 Web 應用程式最簡單的方法是在應用程式的 App Service 方案中手動設定調整。 圖 5-6 顯示適當的 Azure 儀錶板畫面,以設定有多少實例為應用程式提供服務。
圖 5-6。 Azure 中的 App Service 服務方案調整規模。
清晰架構
遵循相依性反向原則以及 Domain-Driven 設計(DDD)原則的應用程式通常會到達類似的架構。 多年來,此架構有許多名稱。 其中一個最早的名字是六邊形架構,接著是埠和配接器。 最近,它被引用為 洋蔥架構 或 乾淨架構。 后一個名稱 Clean Architecture 會作為本電子書中此架構的名稱。
eShopOnWeb 參考應用程式會使用 Clean Architecture 方法,將其程式代碼組織成專案。 您可以在 ardalis/cleanarchitecture GitHub存放庫中,或 從 NuGet 安裝範本,找到解決方案範本,以作為自己 ASP.NET 核心解決方案的起點。
全新架構會將商業規則和應用程式模型放在應用程式的中心。 而非讓業務邏輯依賴於數據存取或其他基礎架構問題,此依賴性被反轉:基礎架構和實作細節依賴於應用核心。 在 Application Core 中定義抽象概念或介面,即可達成這項功能,然後由基礎結構層中定義的類型實作。 可視化此架構的常見方式是使用一系列同心圓,類似於洋蔥。 圖 5-7 顯示此架構表示法樣式的範例。
圖 5-7。 乾淨架構;洋蔥架構
在此圖表中,依賴性會流向最內層的圓形。 Application Core 的名稱來自於它在此圖表核心的位置。 您可以在圖表上看到 Application Core 與其他應用層沒有相依性。 應用程式的實體和介面位於中心。 就在應用程式核心外圍,但仍屬於Application Core的是網域服務。這些服務通常會實作核心內圈中定義的介面。 在 Application Core 外部,UI 和基礎結構層都依賴於 Application Core,但不一定相依於彼此。
圖 5-8 顯示較傳統的水準層圖表,更能反映 UI 與其他圖層之間的相依性。
圖 5-8。 乾淨架構;水平層檢視
請注意,實心箭號代表編譯時間相依性,而虛線箭號則代表僅限運行時間的相依性。 使用乾淨的架構,UI 層會在編譯階段使用 Application Core 中定義的介面,在理想情況下不應該知道基礎結構層中定義的實作類型。 不過,在執行時,應用程式需要這些實作類型才能運行,因此它們必須存在,並透過依賴注入連接到 Application Core 介面。
圖 5-9 顯示遵循這些建議建置時,ASP.NET Core 應用程式架構的更詳細檢視。
圖 5-9。 ASP.NET 乾淨架構之後的核心架構圖表。
由於 Application Core 並不相依於基礎結構,因此很容易撰寫此層的自動化單元測試。 圖 5-10 和 5-11 顯示測試如何融入此架構。
圖 5-10。 隔離地對 Application Core 進行單元測試。
圖 5-11。 整合測試基礎結構實作與外部相依性。
由於UI層不會直接相依於基礎結構專案中定義的類型,因此同樣很容易交換實作,以利測試或回應變更的應用程式需求。 ASP.NET Core 對相依性插入的內建使用和支援,讓此架構成為建構非簡單整合型應用程式的最適當方式。
對於整合型應用程式,應用程式核心、基礎結構和UI專案全都以單一應用程式的形式執行。 運行時間應用程式架構看起來可能類似圖 5-12。
圖 5-12。 ASP.NET Core 應用程式的運行時間架構範例。
在潔淨架構中組織程式碼
在全新架構解決方案中,每個專案都有明確的責任。 因此,某些類型屬於每個專案,而且您經常會在適當的專案中找到對應到這些類型的資料夾。
應用程式核心
Application Core 會保存商務模型,其中包括實體、服務和介面。 這些介面包括將使用基礎結構執行的作業抽象概念,例如數據存取、文件系統存取、網路呼叫等。有時候,在此層定義的服務或介面必須與不相依於UI或基礎結構的非實體類型搭配使用。 這些可以定義為簡單的數據傳輸物件(DTO)。
應用程式核心類型
- 實體類別(持久保存的商業模型類別)
- 匯總 (實體群組)
- 介面
- 域服務
- 規格
- 自定義例外狀況和保護子句
- 網域事件和處理程式
基礎設施
基礎結構專案通常包含數據存取實作。 在典型的 ASP.NET Core Web 應用程式中,這些實作包括 Entity Framework (EF) DbContext、已定義的任何 EF Core Migration
物件,以及數據存取實作類別。 擷取資料存取實作程式代碼的最常見方式是透過使用存放 庫設計模式。
除了數據存取實作之外,基礎結構專案也應該包含必須與基礎結構考慮互動的服務實作。 這些服務應該實作在ApplicationCore中定義的介面,因此基礎結構應該具有ApplicationCore專案的參考。
基礎結構類型
- EF Core 類型 (
DbContext
,Migration
) - 資料存取實作類型 (存放庫)
- 基礎結構特定服務(例如
FileLogger
或SmtpNotifier
)
使用者介面層
ASP.NET Core MVC 應用程式中的使用者介面層是應用程式的進入點。 此項目應該參考 Application Core 專案,而且其類型應該透過 Application Core 中定義的介面,與基礎結構嚴格互動。 UI 層不應允許對基礎結構層類型的直接具現化或靜態呼叫。
UI 層次類型
- 控制器
- 自訂篩選
- 自訂中間件
- 瀏覽次數
- ViewModels
- 新創公司
類別 Startup
或 Program.cs 檔案負責設定應用程式,以及將實作類型連接至介面。 執行此邏輯的位置稱為應用程式的 組合根,使得相依性注入在運行時能正常運作。
備註
為了在應用程式啟動期間設置依賴注入,UI 層專案可能需要參考基礎結構專案。 您可以使用內建支援從元件載入型別的自訂 DI 容器,來消除此相依性。 為了此範例的目的,最簡單的方法是允許UI專案參考基礎結構專案(但開發人員應將基礎結構專案中類型的實際參考限制為應用程式的組合根目錄)。
整合型應用程式和容器
您可以建置單一和整合式部署型 Web 應用程式或服務,並將其部署為容器。 在應用程式中,它可能不是整合型,而是組織成數個連結庫、元件或層。 在外部,它是具有單一進程、單一 Web 應用程式或單一服務的單一容器。
若要管理此模型,您要部署單一容器來代表應用程式。 若要調整規模,只要在前面新增具有負載平衡器的其他複本即可。 透過在單一容器或 VM 中管理單一部署來達到簡化的目的。
您可以在每個容器中包含多個元件/連結庫或內部層,如圖 5-13 所示。 但是,遵循「容器會執行一件事,並在一個進程中執行」容器原則,單體架構可能是衝突。
此方法的缺點在於當應用程式成長並需要擴展時可能會出現問題。 如果整個應用程式具備擴展能力,那就不是問題。 不過,在大部分情況下,應用程式的一些部分是需要調整的窒息點,而其他元件則較少使用。
使用典型的電子商務範例,您可能需要調整的就是產品資訊元件。 有更多顧客瀏覽產品而非購買產品。 客戶使用購物籃比使用付款管線還多。 較少的客戶新增批注或檢視其購買歷程記錄。 而且,在單一區域中,您可能只有少數員工需要管理內容和營銷活動。 藉由擴展單體架構,所有程式代碼都會被多次部署。
除了「調整所有項目」問題之外,單一元件的變更還需要完整重新測試整個應用程式,以及完整重新部署所有實例。
整合型方法很常見,許多組織都使用這個架構方法進行開發。 許多人得到足夠好的結果,而另一些人則遇到瓶頸。 許多都在此模型中設計了其應用程式,因為工具和基礎結構太難建置服務導向架構 (SOA),而且在應用程式成長之前,他們看不到需求。 如果您發現您達到單體式方法的限制,將應用程式細分以便更好地運用容器和微服務,可能是下一個合乎邏輯的步驟。
您可以使用每個實例的專用 VM,在 Microsoft Azure 中部署整合型應用程式。 使用 Azure 虛擬機擴展集,您可以輕鬆地調整 VM。 Azure App Services 可以執行整合型應用程式,並輕鬆地調整實例,而不需要管理 VM。 Azure App Services 也可以執行 Docker 容器的單一實例,以簡化部署。 您可以使用 Docker,將單一 VM 部署為 Docker 主機,並執行多個實例。 使用 Azure 平衡器,如圖 5-14 所示,您可以管理調整。
使用傳統部署技術可以管理對各種主機的部署。 Docker 主機可以使用如 Docker 手動執行的命令 來管理,或透過自動化來管理,例如持續傳遞 (CD) 管線。
部署為容器的整合型應用程式
使用容器來管理整合型應用程式部署的優點。 調整容器的實例比部署額外的虛擬機更快、更容易。 即使使用虛擬機擴展集來擴展 VM 的規模,它們仍然需要花費時間來建立。 部署為應用程式實例時,應用程式的組態會作為 VM 的一部分進行管理。
將更新部署為 Docker 映射的速度要快得多,而且網路效率很高。 Docker 映像通常能在幾秒內啟動,加速部署過程。 卸除 Docker 實例就像發出 docker stop
命令一樣簡單,通常會在不到一秒內完成。
由於容器在設計上本質上是不可變的,所以您永遠不需要擔心損毀的 VM,而更新腳本可能會忘記考慮磁碟上留下的某些特定組態或檔案。
您可以使用 Docker 容器進行更簡單 Web 應用程式的整合式部署。 此方法可改善持續整合和持續部署管線,並協助實現部署對生產的成功。 不再「它在我的計算機上運作,為什麼無法在生產環境中運作?
以微服務為基礎的架構有許多優點,但這些優點代價是複雜度增加。 在某些情況下,成本超過優點,因此在單一容器或只有少數容器中執行的整合型部署應用程式是較佳的選項。
單體式應用程式可能不容易分解成相互獨立的微服務。 微服務應該彼此獨立運作,以提供更具彈性的應用程式。 如果您無法提供應用程式的獨立功能部分,分隔它反而會增加複雜度。
應用程式可能還不需要獨立調整功能。 許多應用程式需要擴充到單一實例之外時,可以透過複製整個實例的相對簡單程式來執行此動作。 將應用程式分隔為離散服務所需的額外工作,當擴展應用程式整個實例既簡單又具成本效益時,所帶來的效益微乎其微。
在開發應用程式初期,您可能沒有清楚瞭解自然功能界限的位置。 當您開發最低可行的產品時,自然分離可能還沒有出現。 其中一些情況可能是暫時性的。 您可以從建立整合型應用程式開始,稍後將一些功能分開,以開發及部署為微服務。 其他條件對應用程式的問題空間可能至關重要,這表示應用程式可能永遠不會分成多個微服務。
將應用程式分成許多離散進程也會導致額外負荷。 將功能分成不同程式時,會比較複雜。 通訊協定變得更加複雜。 您必須在服務之間使用異步通訊,而不是方法呼叫。 當您移至微服務架構時,您必須新增許多在 eShopOnContainers 應用程式的微服務版本中實作的建置組塊:事件總線處理、訊息復原和重試、最終一致性等等。
更簡單的 eShopOnWeb 參考應用程式 支援單容器的單體式運作。 應用程式包含一個 Web 應用程式,其中包含傳統的 MVC 檢視、Web API 和 Razor Pages。 您可以選擇性地執行應用程式的 Blazor 型系統管理員元件,這也需要個別的 API 專案才能執行。
您可以使用 docker-compose build
和 docker-compose up
命令,從解決方案根目錄啟動應用程式。 此命令會使用 Dockerfile
Web 專案根目錄中找到的 來設定 Web 實例的容器,並在指定的埠上執行容器。 您可以從 GitHub 下載此應用程式的來源,並在本機執行。 即使是此整合型應用程式,也受益於在容器環境中部署。
一是容器化部署表示應用程式的每個實例都會在相同的環境中執行。 此方法包括進行早期測試和開發的開發人員環境。 開發小組可以在符合生產環境的容器化環境中執行應用程式。
此外,容器化應用程式能以較低的成本擴展。 使用容器環境使資源共享比傳統虛擬機環境更高。
最後,容器化應用程式會強制將商業規則與記憶體伺服器區隔開。 當應用程式向外延展時,多個容器全都會依賴單一實體儲存媒體。 此儲存媒體通常是執行 SQL Server 資料庫的高可用性伺服器。
Docker 支援
專案會在 eShopOnWeb
.NET 上執行。 因此,它可以在以Linux或 Windows 為基礎的容器中執行。 請注意,針對 Docker 部署,您想要針對 SQL Server 使用相同的主機類型。 以 Linux 為基礎的容器具有較小的占用空間,並且受到偏好。
您可以使用 Visual Studio 2017 或更新版本,以滑鼠右鍵按兩下 方案 總管中的項目並選擇 [ 新增>Docker 支援],將 Docker 支援新增至現有的應用程式。 此步驟會新增所需的檔案,並修改專案以使用它們。 目前的 eShopOnWeb
範例已包含這些檔案。
解決方案層級 docker-compose.yml
檔案包含要建置哪些映像,以及要啟動哪些容器的相關信息。 檔案可讓您使用 docker-compose
命令同時啟動多個應用程式。 在此情況下,它只會啟動 Web 專案。 您也可以使用它來設定相依性,例如個別的資料庫容器。
version: '3'
services:
eshopwebmvc:
image: eshopwebmvc
build:
context: .
dockerfile: src/Web/Dockerfile
environment:
- ASPNETCORE_ENVIRONMENT=Development
ports:
- "5106:5106"
networks:
default:
external:
name: nat
檔案 docker-compose.yml
會參考 Dockerfile
項目中的 Web
。
Dockerfile
用來指定將使用哪個基底容器,以及應用程式在上面設定的方式。
Web
’Dockerfile
:
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /app
COPY *.sln .
COPY . .
WORKDIR /app/src/Web
RUN dotnet restore
RUN dotnet publish -c Release -o out
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app/src/Web/out ./
ENTRYPOINT ["dotnet", "Web.dll"]
針對 Docker 問題進行疑難解答
執行容器化應用程式之後,它會繼續執行,直到停止為止。 您可以使用 docker ps
命令來查看哪些容器正在執行。 您可以使用 docker stop
命令並指定容器識別碼來停止執行中的容器。
請注意,執行 Docker 容器可能會占用您在開發環境中可能想使用的埠。 如果您嘗試使用與執行中 Docker 容器相同的埠執行或偵錯應用程式,您會收到錯誤,指出伺服器無法繫結至該埠。 再次停止容器應該能解決問題。
如果您想要使用 Visual Studio 將 Docker 支援新增至應用程式,請確定 Docker Desktop 正在執行。 當您啟動精靈時,如果 Docker Desktop 未執行,精靈將無法正確執行。 此外,精靈會檢查您目前的容器選擇,以新增正確的 Docker 支援。 如果您想要新增支援 Windows 容器,您需要在 Docker Desktop 開啟並已設定 Windows 容器的情況下執行安裝程式。 如果您想要添加對 Linux 容器的支援,請在 Docker 已執行且已配置 Linux 容器時啟動精靈。
其他 Web 應用程式架構樣式
- Web-Queue-Worker:此架構的核心元件包括一個處理用戶端請求的 Web 前端,以及一個執行資源密集型工作、長時間運行的工作流程或批次作業的工作員。 Web 前端透過訊息佇列與工作元件進行通訊。
- 多層式:多層式架構會將應用程式分成邏輯層和實體層。
- 微服務:微服務架構是由小型自主服務的集合所組成。 每個服務都是獨立的,而且應該在限定的內容內實作單一商務功能。
參考 – 常見的 Web 架構
-
潔淨架構
https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html -
Onion 架構
https://jeffreypalermo.com/blog/the-onion-architecture-part-1/ -
存放庫模式
https://deviq.com/repository-pattern/ -
清理架構解決方案範本
https://github.com/ardalis/cleanarchitecture -
建構微服務電子書
https://aka.ms/MicroservicesEbook -
DDD (Domain-Driven 設計)
https://learn.microsoft.com/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/