【Python 】在 Python 中使用架構模式管理複雜性

你的原始碼是不是感覺像一個大泥球?依賴項是否在您的程式碼庫中交織在一起,以至於改變感覺很危險或不可能?

隨著業務的增長和領域模型(您在應用程式中解決的業務問題)變得更加複雜,我們如何在不從頭開始重新編寫所有內容的情況下解開我們建立的混亂?更好的是,我們如何避免一開始就陷入混亂?

鳥瞰圖

以下是 Python 架構模式中介紹的技術的簡要總結:

分層架構

單一職責

檢視 vs 服務 vs 儲存庫 vs ORM vs 域

依賴倒置

高階與低階模組

抽象

領域驅動設計

先說“業務上下文”

領域建模(事件風暴等)

實體 vs ValueObjects vs 域服務

資料類

測試驅動開發

什麼是TDD

在服務層進行高速測試

在域中進行低速測試

設計模式

儲存庫模式

服務層模式

工作單元模式

聚合模式

事件驅動架構

活動

訊息匯流排

事件處理程式作為服務層

時間解耦

佇列和代理

冪等性、故障和監控

命令

CQRS

簡單讀取與複雜命令

非規範化、快取和最終一致性

我將簡要介紹這些主題中的每一個,但我不會在這篇博文中重新列印這本書。這些將是我自己的話和我的解釋,所以如果你想要“真正的交易”,我建議你去源頭找一本這本書:)

分層架構

【Python 】在 Python 中使用架構模式管理複雜性

A simplified summary of Layered Architecture

SOLID 原則大量存在於良好的設計中。簡而言之,如果您不知道,我將解釋這些是什麼。S,Single Responsibility,意味著程式碼應該有一個改變的理由,而且只有一個理由。O,對於 Open-Closed,意味著您的程式碼應該對擴充套件開放但對修改關閉。L,對於 Liskov Substitution,意味著子類的例項可以在不改變行為的情況下替換其父類的用法。我,對於介面隔離,意味著你的程式碼不應該被迫實現它不使用的行為。最後,D,表示依賴倒置,意味著一種鬆散耦合。

單一職責是分層架構背後的動機。也就是說,您的 Django 檢視負責處理 HTTP 事務——獲取輸入、傳送輸出和狀態碼。這些檢視應該委託給編排業務邏輯的服務。服務實現用例並且應該依賴於圍繞低階細節的抽象,這些抽象可以包括儲存庫(用於儲存抽象)和工作單元(用於事務或原子操作管理)。

這些層(檢視、服務、儲存庫/UoW)從您的業務與特定用例/端點/網頁相關的高層開始。然後他們使用抽象層直到我們寫入資料庫(在儲存庫中)或與其他系統通訊等的低階操作。這就是依賴倒置的原理。

依賴倒置原則有兩個部分。首先,高階模組不應該依賴於低階模組,兩者都應該依賴於抽象。其次,抽象不應該依賴於細節,而細節應該依賴於抽象。因為這是一個如此複雜的話題,我不會詳述它,如果你有興趣,我建議你在這裡、這裡、甚至在本書中找到更好的閱讀材料!

領域驅動設計

【Python 】在 Python 中使用架構模式管理複雜性

也稱為 DDD。成為您領域的主人!什麼是域?好吧,實際上,這取決於您要解決的業務問題!不,我不是在開玩笑。這實際上取決於 - 域的定義是您要解決的業務問題!

也就是說,如果您在一家航運公司工作,那麼當您為您的域建模時,您會發現您可能有“ShippingContainers”和“Ships”或“Trucks”等。您可能有“SalesReports”和“PackingManifests” ”。但是,如果你要為一家軟體公司工作,那麼這些域物件就沒有多大意義,你將擁有一個完全不同的域模型。

找出你的領域模型的過程被稱為……“領域建模”。您可以為此使用幾種不同的技術,我最喜歡的技術之一是“事件風暴”(https://eventstorming。com/)。不過,基本上,TLDR 是您需要與利益相關者(需要解決問題的人)坐下來弄清楚他們使用的語言。寫下名詞和動詞,將它們連線在一起,並弄清楚你的領域是如何工作的。做對了,它會使剩下的過程變得更容易。

然後,您需要將此域模型轉換為實際程式碼。出於我們的目的,我們專注於“實體”和“值物件”——區別在於實體具有永久身份(例如 ID 欄位),而值物件根據其……嗯……值……來改變身份。例如,“使用者”將有一個 ID 欄位,您可以在不更改實際使用者的情況下更改使用者的電子郵件。然而,ValueObject 類似於地址。如果你改變地址的值,你就有了一個新的地址!看看它是如何工作的?

你可以很簡單地使用“@dataclass”在 python 中表示你的域模型,它為你設定了你的建構函式和其他一些簡潔的東西。這可以為您提供一個非常簡單的物件,該物件僅用於儲存特定屬性(例如,城市、州、zip 或名字、姓氏等)。然後您可以從您的儲存庫中返回這些物件,並且您將有一個一致的結構來傳遞您的應用程式。讓您的領域模型透過 ID 相互引用並根據需要進行水合,可選擇儲存在快取中,然後您就可以參加比賽了。

測試驅動開發

【Python 】在 Python 中使用架構模式管理複雜性

The “Testing Pyramid” with explanations

對於某些人來說,TDD 是一個有爭議的話題。如果你不熟悉,TDD 的基本前提是隻有三個規則:

除非您的測試失敗,否則您不得編寫任何程式碼。

你一次只能寫一個測試用例,它應該開始失敗。

一旦你有一個失敗的測試,你應該只寫足夠的程式碼來使測試透過。

而已。然後你重複。人們說這個完整的迴圈是一個 30 秒的過程——我懷疑他們練習它的時間比我多一點。這也被稱為“進攻性”測試,而不是我們都習慣的“防禦性”測試——也就是說,防禦性測試是在事後編寫測試以“保護”自己。防禦性測試可以為您提供一些保護,但要獲得高覆蓋率要困難得多。進攻性測試為您提供 100% 的覆蓋率,並*迫使*您使用抽象等編寫可測試的程式碼。

也就是說,TDD 不是靈丹妙藥。它不是一種宗教。有(很少)TDD 不起作用的情況。TDD 也不會阻止您編寫錯誤或編寫糟糕的程式碼(您仍然也可以編寫糟糕的測試)。考慮到這一點,重要的是透過在可能時以“高速檔”進行測試並在必要時以“低速檔”進行測試,從而最大限度地提高測試的價值。

高速檔與低速檔測試是本書中討論的一個概念。總而言之,“高階”是指您在服務層或使用其他高階模組編寫測試(參見上面的“分層架構”)。它們往往涵蓋更多程式碼,並且最適合新增新功能或修復簡單錯誤。“低檔”測試是在域級別和其他低級別模組。當面臨特別困難的錯誤或進行非常大的重構時,低檔是最好的。

設計模式

【Python 】在 Python 中使用架構模式管理複雜性

A simple layout of the design patterns we talk about

有很多設計模式值得了解。其他一些書籍,如“設計模式:可重用面向物件軟體的元素”涵蓋了其中的幾本。Python 中的架構模式特別關注四種模式:儲存庫模式、服務層模式、工作單元模式和聚合模式。

儲存庫是圍繞您的儲存機制的抽象。您可以為 Redis、CSV 檔案、資料庫等建立一個儲存庫。它們都可以滿足一個通用介面,如果您真的願意,您可以將一個交換為另一個。目標是抽象出低階細節,以便您的高階模組不依賴於低階細節。這對於分層架構很重要,這也是本書廣泛使用儲存庫模式的原因。

服務層只是您的業務邏輯的編排。當您第一次開始編寫 API 端點時,傾向於將所有業務邏輯放在一個處理 API 請求的函式中。這違反了單一職責原則,因為 API 端點處理程式現在負責管理 HTTP 輸入、響應以及業務邏輯的所有各個方面,如建立使用者、驗證輸入、登入等。這些較低級別(儘管不是最低級別)任務可以委託給每個用例都有方法的服務。也就是說,該服務將具有註冊使用者、登入使用者等的方法。這些方法將呼叫儲存庫並接收回域物件。

工作單元用於原子操作。想想“資料庫事務”和“鎖”,通常封裝相關的操作。如果您需要“預訂酒店房間”,那麼您可以有一個包含此邏輯的“工作單元”。如果在查詢可用房間並將房間分配給某人並處理此人的付款資訊期間發生某種錯誤,那麼工作單元將很好地為您回滾所有這些邏輯。您可以依賴低級別的資料庫事務(並且您的工作單元可能在後臺執行此操作),但是在您的服務函式中內聯該邏輯開始混淆您的程式碼。使用工作單元來處理這些原子操作提供了一個乾淨的介面,可以利用 Python 強大的“with”語句並根據需要在您之後自動清理。

聚合是具有共同一致性邊界的領域物件的集合。購物車之類的東西可以是一個聚合體——購物車內有幾個領域物件,甚至購物車內可能還有其他聚合體。但是,在結賬時,將購物車視為一個單元是很有用的。您可以將聚合視為物件樹,並且可以透過根來引用聚合。

關於聚合的另一個注意事項是每個儲存庫應該有一個聚合。換句話說,您不應該擁有不是聚合的域物件的儲存庫。這樣,聚合就形成了領域模型的“公共”API。

事件驅動架構

【Python 】在 Python 中使用架構模式管理複雜性

Simplified overview of Event Driven Architecture and CQRS

簡而言之,EDA 就是您使用“事件”作為系統的輸入。事件(或領域事件)是一個 ValueObject,您可以有內部和外部事件。內部事件永遠不會離開您的系統,通常由訊息匯流排(將事件對映到事件處理程式的簡單路由器)之類的東西處理。外部事件被髮送到其他系統並且非常適合“時間解耦”——您可以向訊息代理發出事件,該訊息代理非同步管理一系列佇列工作程式。

所有事件都可能失敗,我們如何處理失敗很重要。我們需要監控以瞭解事件何時失敗以及哪些事件失敗。我們還需要我們的事件處理程式是冪等的,所以當我們重試事件時,不會發生任何意外。常規事件可以在不影響整體操作的情況下安全地失敗,這是事件和命令之間的重要區別。

命令是一種特殊型別的事件。一個常規事件可以有多個處理程式,而一個命令只有一個處理程式。一個命令,當它失敗時,應該將異常重新丟擲堆疊,而當一個事件失敗時,應該有一些優雅的異常處理。命令通常會修改資料並觸發副作用,將其與“返回資料”操作分開是 CQRS(Command/Query Responsibility Segregation)的目標。

CQRS 背後的主要動機是命令昂貴且複雜,通常需要一定程度的原子性以及即時一致性。另一方面,查詢是簡單的讀取操作。查詢通常不依賴於域(業務邏輯),而命令通常依賴於域。可以針對只讀副本執行查詢,其中命令通常最好針對主資料儲存執行。查詢還可以利用非規範化資料和最終一致性。這很好,因為查詢通常比命令多幾個數量級,這有助於系統更好地擴充套件。

應用所有這些

總而言之,重要的是逐個進行。您無需一次完成所有這些操作。如果您對嘗試工作單元猶豫不決,或者您沒有立即使用聚合,或者您甚至沒有領域模型,那沒關係!您可以從使用分層架構開始的最簡單和最有效的事情之一 - 看看您是否可以使用服務將較低級別的模組與較高級別的模組解耦。看看您是否可以將您的儲存邏輯隔離到您的服務使用的儲存庫中。如果您還可以繪製一些簡單的資料類來表示您的域物件並讓您的 ORM 依賴於這些,那就更好了。

如果您將依賴於自身的邏輯組合在一起,並使用抽象將模組分開,那麼您將成為其中的一部分。檢視接縫的位置並開始將程式碼拆分為可測試的塊。有關這方面的一些優秀示例,請檢視“有效地使用遺留程式碼”,這本書既是一本好書,又被“Python 中的架構模式”引用。

哦,如果您還沒有閱讀“Python 中的架構模式”,請特別注意結尾部分!這將為您提供更多關於我上面提到的所有內容的背景資訊。我用大約 5 頁總結了一本 300 多頁的書,所以肯定有一些東西我遺漏了 :)

TAG: 儲存測試事件架構可以