Cookie Cutter架構-Janos Pasztor
在業務應用程序方面,您需要一個可以很好地擴展的體系結構。這是我的看法,基於Uncle Bobs EBI。
儘管大多數人都認為我是DevOps人,但我經常在諮詢項目期間使用業務應用程序,甚至在為DevOps企業編寫管理軟體時也是如此。在我這麼多年的時間裡,我意識到我編寫代碼的方式並不是非常有效。
首先,我開始使用一個框架,例如Symfony(拒絕抨擊Symfony),我會以Symfony文檔中舉例說明的方式編寫代碼。但是,Symfony文檔包含有關如何執行操作而不是黃金標準的簡化示例。例如,它將直接在控制器中具有資料庫(Doctrine)查詢。
許多人試圖將其理性化為簡單,並且Doctrine是MVC中的模型,但是當我開始學習時,擁有扁平的架構並不能很好地擴展。隨著需求的增長,控制器將變得越來越臃腫,並且不會分離出常見的代碼部分。這是一個問題,但我知道沒有解決方案。
幾年前,我遇到過Uncle Bobs談話架構 - 失落的歲月,但這太過於學術化,太過於理論化。儘管他提出的設置稱為Entity-Boundary-Interactor有相當數量的文檔,但我發現它太簡單了。
然後,大約兩年前,我從PHP切換到Java。不是因為我討厭PHP,遠非如此。我只是希望深入 靜態類型,而PHP(現在仍然)缺乏這種類型。我第一次切換到Hacklang,這很棒,但當時缺乏任何合理的IDE支持。最後,我放棄並將所有代碼移植到Java。
看看Java世界,我不太喜歡它。作為語言的新手,在我看來,我不喜歡古老的Servlet API,而且錯過了PSR-7中提出的現代不可變HTTP表示形式 。
由於我沒有時間壓力,我做了一個商業環境中沒有理智的人會做的事情,並且我自己也做了。我將PSR-7移植到Java,並為servlet API編寫了一個映射器。我構建了一個圍繞Jetty的抽象來充當嵌入式Web伺服器,因此我擺脫了通常的Java架構的限制,可以自由地構建和試驗我喜歡的任何系統。
去年夏天,另一個概念開始湧入我的觀點,這嚴重影響了我構建系統的方式:單頁應用程序。討厭或喜歡React和它的好友,我開始考慮我的應用程序更像是一個API,而不是那些在會話中處理狀態,存儲表單數據和不存在的東西。
我還在很大程度上建立了依賴注入的概念,使用我自己的 依賴注入器。但最重要的是,我做了很多關於如何構建一個可以很好地維護的應用程序的思考。
架構
通過我的實驗,我提出了一個我想出的架構。重點不在於編寫最少量的代碼。這也不是最快的寫入代碼。重點是可預測性。換句話說,我討厭意外。當需求發生微小變化時,它不應該波及整個應用程序,搞亂一切。應該在一個地方捕獲第三方API更改或錯誤,並且不應導致級聯故障。
為了實現這一點,我的應用程序分為三層:API,業務邏輯和後端/存儲。這些層中的每一層僅負責一件事,並且它們在它們之間傳遞實體。(基本上,半啞semi-dumb數據傳輸對象DTO,banq註:如果作者懂DDD,用領域模型替代DTO就跟棒了!)。
API
該API負責處理與輸出介質。例如,如果API需要返回博客帖子,但又想獲取所述博客帖子的作者,則需要調用相應的業務邏輯類來執行該操作。例如:
class BlogPostGetApi {
private final BlogPostGetBusinessLogic blogPostGetBusinessLogic;
private final AuthorGetBusinessLogic authorGetBusinessLogic;
@Inject
public BlogPostGetApi(
BlogPostGetBusinessLogic blogPostGetBusinessLogic,
AuthorGetBusinessLogic authorGetBusinessLogic
) {
this.blogPostGetBusinessLogic = blogPostGetBusinessLogic;
this.authorGetBusinessLogic = authorGetBusinessLogic;
}
@Route(
method = "GET"
path = "/blog/:id"
)
public Response get(String id) throws BlogPostNotFoundException {
BlogPost blogPost = blogPostGetBusinessLogic.getById(id);
Author author = authorGetBusinessLogic.getById(blogPost.getAuthorId());
return new Response (
blogPost,
author
)
}
//Inner class that contains a structured response
public class Response {
//...
}
}
正如您所看到的,業務邏輯層不必處理它要獲取對象有關的複雜性,這是API層要處理的複雜性。API處理任何潛在的許可權問題也很重要,例如:
if (!blogPost.isPublished()) {
throw new BlogPostNotFoundException();
}
它變得更複雜,但你更容易都懂代碼且明白。
業務邏輯
業務邏輯是負責怎麼做的問題,所以你可以有像這樣的類 UserCreateBusinessLogic。此類僅負責導致用戶創建的業務流程。因任何原因需要創建用戶的API都可以依賴於UserCreateBusinessLogic確保用戶創建僅在一個地方完成。
毋庸置疑,業務流程可能變得複雜,因此他們當然可以相互調用。例如,如果您有一個創建組織對象的業務流程,並且有一個用戶,那麼您可以擁有一個UserOrganizationRegisterBusinessLogic,它將同時調用UserCreateBusinessLogic和OrganizationCreateBusinessLogic。也許還有付款創建和其他幾個。
後端/存儲
我們應用程序中的最後一層負責處理我們應用程序之外的任何討厭部分,例如第三方API,資料庫以及我們認為不可靠的所有其他內容。
等一下......我剛才說資料庫不可靠嗎?Yepp,我剛才是這麼說。資料庫在網路上,並且開發人員希望它與其他人一樣,而網路是不可靠的(banq註:FLP定理)。他們可以損壞,他們可能很慢,他們可以丟包。因此,我對資料庫的處理與處理第三方API的方式相同。
回到API ......通常我們必須處理第三方API,而我們並不是很清楚這些API內部。要麼它沒有被文檔描述,要麼它只是有一些我們還沒有遇到過的怪癖。這些將不可避免地導致我們的系統遲早要處理的問題,例如捕獲錯誤,例如,讓業務邏輯現在我無法做到這一點。
例如,數據結構可能很奇怪而且不符合我們的喜好,在這種情況下,後端層作業將其轉換為我們可以工作的對象。
實體
注意:本文不區分實體和DTO。出於本文的目的,實體是您希望在應用程序的各個部分之間傳遞的結構化數據集。如果您想根據目的或用途拆分它們,請選擇它。
正如我提到的,不同的層使用實體進行通信。這些不是您期望從ORM系統獲得的實體。它們不包含載入子對象的魔術函數,例如blogPost.getAuthor()。這些是啞數據傳輸對象(banq註:可以用DDD實體或值對象實現),例如:
class BlogPost {
private final String id;
private final String authorId;
private final String title;
public BlogPost(
String id,
String authorId,
String title
) {
this.id = id;
this.authorId = authorId;
this.title = title;
}
public String getId() {
return this.id;
}
//...
}
想要獲取屬於此博客帖子的作者?自己做。在我看來,它應該在您的業務邏輯中明確。可讀代碼,用於記錄發生的情況,而不是依賴於ORM的內部行為。
您可能還注意到上面的實體是不可變的。如果要修改標題,則必須在副本中執行此操作。為此,實體可以包含輔助函數:
public BlogPost withTitle(
String title
) {
return new BlogPost(
this.id,
this.authorId,
title
);
}
就是這樣!除了可能驗證之外,實體中沒有更多內容。畢竟,甚至不應該創建具有無效數據的實體,應該儘早發生故障。
處理一致性
還記得我們上面的組織和用戶註冊示例嗎?一個註冊過程涉及創建多個對象,而這些對象又使用多個存儲類將數據保存到資料庫。
你如何確保一致性?換句話說,您如何確保創建全部或全部?
這就是Java真正開始閃耀的地方。有一種稱為Java Transaction API的東西,它允許創建分散式事務,甚至可以跨多個資料庫。
我只是Transaction在執行需要它的操作時在我的API層中請求一個對象,然後將它通過我的應用程序傳遞到存儲層。然後,存儲層可以使用它來確保一致性,甚至可以跨多個對象創建/更新。
處理許可權檢查
我很難在很長一段時間內實施更複雜的許可權檢查。API層本身不適合實現廣泛的許可權檢查,因為可能需要跨多個API重用這些許可權。
讓我們舉一個非常簡單的例子:登錄的每個用戶都獲得一個訪問令牌,然後你在API中為每個需要許可權的請求請求訪問令牌,如下所示:
public Response update(
@RequestHeader(name = "Authorization", prefix = "Bearer")
String accessToken,
String blogPostId,
String title,
//...
) {
//...
}
在此示例中,單頁應用程序將在Authorization標頭中發送訪問令牌,如下所示:
Authorization: Bearer your-access-token-here
但是,您的API需要確定使用所述訪問令牌登錄的用戶是否有權更新此博客帖子。我們可以在這裡使用一個小技巧:我們在業務邏輯之前添加一個額外的安全層,例如:
public Response update(
@RequestHeader(name = "Authorization", prefix = "Bearer")
String accessToken,
String blogPostId,
String title,
//...
) throws AccessDeniedException, BlogPostNotFoundException {
newBlogPost = blogPostUpdateSecurity.update(
blogPostId,
accessToken,
title,
//...
);
return new Response(
newBlogPost
);
}
安全層檢查用戶是否具有適當的許可權,並將請求傳遞給博客帖子的實際更新業務邏輯,並返迴響應。當然,在內部,它需要從資料庫中獲取訪問令牌,檢索用戶,如果需要可能涉及緩存層,但這不需要涉及API。
傳統的Web應用程序
到目前為止,我們只談到了一個特定於單頁應用程序的架構,其中的東西很簡單。實際上,您的應用程序不必包含任何狀態。它只能傳遞管道中的任何請求並返回結果。從本質上講,您的應用程序基本上是一組函數,通常是純粹的或至少是無狀態的,具有依賴注入。(向JavaScript大家們致敬,我們要感謝近年來函數式編程的興起!)
但是,當談到傳統的Web應用程序時,事情會變得混亂。他們希望在會話中存儲臨時表單數據和一堆其他內容。雖然我不提倡使用會話,但我們必須處理API無需處理的許多事情。
所以,這裡有一個想法:為什麼我們不在API之上再添加一層?畢竟,許可權檢查和所有其他事情已經處理完畢,因此Web層應該只處理傳統Web應用程序特有的內容!
總結
Michael Cullum稱這是Cookie Cutter方法,所以我正式稱之為。基本思路如下:
- 將您的應用程序拆分為服務和實體。
- 實體應該是不可變的,並且只包含驗證代碼並創建自身的更改副本。(banq註:正是DDD值對象)
- 除了注入的依賴項之外,服務應該沒有內部狀態。
- 服務應該具有非常少量的公共方法,理想情況下只有一種,通常是純粹的或至少是無狀態的 功能。(如果需要,可以添加私有方法以便於閱讀,但通常最好分割整個類。)
- 服務應該儘可能少地處理。盡量讓它們低於?150行代碼。
- 應將服務分組為多個層,每個層負責一組任務。
額外的事實:這不是我的定製、瘋狂gou屁框架特有的。您可以在支持依賴項注入的任何現代Web框架中實現此功能。您只需要願意放棄在應用程序的所有部分中使用該框架。
好處
您可能已經意識到,這種架構要求您編寫大量代碼,尤其是最初的代碼。它至少不適用於任何類似於快速原型製作方法的東西。
不可否認,我致力於維護周期很長的應用程序,並且經常會收到客戶更改請求。你的情況可能會有所不同,也許你把網站交給你再也見不到的客戶,但是讓我問你:你最後一次走捷徑的時又是什麼時候再次困擾了你?
對我而言,這是最可怕的感受之一,看到客戶提出了一個相對簡單的變更請求,然後導致整個團隊多周頭痛。
這種架構已經證明是一致的。不快,但是一致。我們知道開發某個功能需要多長時間。系統中沒有任何意外,但它帶來的缺點是我們必須自己編寫很多代碼。
此外,由於此設置不依賴於Java,因此我設法聘請了一位經驗不足的PHP開發人員,並在IDE的幫助下,讓他們在大約3天內提供生產就緒代碼。
此外,由於一切都非常好並且切割得很好,因此對單個零件進行單元測試非常容易。它使維護起來非常舒適。
作者:JDON
原文:https://www.jdon.com/51386
※各大框架都實用的axios封裝,攔截器統一封裝 get ,post,put請求
※LieBrother說Git 分支模型
TAG:程序員小新人學習 |