Cookie Cutter架構 - Janos Pasztor

banq發表於2019-01-09

在業務應用程式方面,您需要一個可以很好地擴充套件的體系結構。這是我的看法,基於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天內提供生產就緒程式碼。
此外,由於一切都非常好並且切割得很好,因此對單個零件進行單元測試非常容易。它使維護起來非常舒適。

相關文章