l

2020年8月19日 星期三

再談Clean Architecture三原則

August 19 08:45~10:00;12:56~13:38


2018年Teddy寫了幾篇介紹Clean Architecture的文章如下,其中有三篇提到Clean Architecture三原則:分層相依性跨層,今天再一次一起討論這三個原則。

***

分層原則

如圖1所示,Clean Architecture有四層:Entities、Use Cases、Interface Adapters、Frameworks & Drivers。分層原則乍看之下最簡單也最沒爭議,但實際上有些物件要放在哪一層還是存在灰色地帶。

最核心的Entities存放domain model物件。如果對照領域驅動設計(Domain-Driven Design;DDD),Aggregate、Entity、Value Object、Domain Service就放在這一層。應用程式邏輯放在Use Cases這一層,也就是DDD裡面的Application Service。

先看這兩層就好,請問DDD的Repository要放在哪裡?大部分的人覺得要放在Use Cases這一層,但也有人主張要放在Entities。所以,雖然Clean Architecture這四層大方向來講區分得很清楚,但在實作的時候有一些「支援物件」,像是剛剛提到的Repository以及Mapper等實際上要放在哪裡,還是有一些討論的空間。


CleanArchitecture

▲圖1:《Clean Architecture》建議的四層架構,畫成同心圓(圖片來源在此)。

***

相依性原則

原始碼的依賴方向必須由外往內、由低層往高層。也就是說Entities層的物件只可以參考(依賴)同一層的其他物件,不可以參考其他層的物件。Use Cases層只可以往內參考Entities層以及自己這一層的物件,不可以往外參考其他層的物件,依此類推。

但是,如果Use Cases需要將Aggregates儲存到資料庫,那一定要參考到最外層的資料庫物件啊,這怎麼辦?如圖2所示,Clean Architecture透過依賴反轉(Dependency Inversion Principle;DIP)確保相依性原則可以被遵守。


▲圖2:透過依賴反轉做到相依性由外往內。

現在問題來了,圖2的Workflow aggregate透過AddWorkflowUseCase把Workflow儲存到MySQL資料庫,這樣子有沒有違反相依性原則?

雖然SqlWorkflowRepository與MySQL都跨層直接參考Workflow,但相依性的方向還是由外向內,因此並沒有違反相依性原則。但是,這麼做違反了Clean Architecture的第三個原則:跨層原則。

相依性原則要完全遵守也是不容易的一件事,例如Entities層的Aggregate要發Domain Event(領域事件)需要用到event bus公具程式,最簡單的方式是找人家已經寫好的工具來使用,但是這樣就違反了相依性原則。為了滿足相依性原則,就要幫著個event bus工具程式定義介面,透過依賴反轉做到相依性原則。有時候定義依賴反轉介面很簡單,但有時候卻不容易。

***

跨層原則

當Entities層的物件跨越離開Use Case層往外傳遞時(傳到UI或是DB或其他外部系統),不要直接把Entities層的物件傳出Use Case層,而是要在Use Cases層定義往外傳遞的介面與資料結構。簡單說就是把Entities層的物件轉成DTO(Data Transfer Object)再往外傳遞。

如果直接把Entities層的物件往外傳,例如直接傳給UI,很有可能因為UI端的需求,導致回頭影響Entities層的物件。也就是說,presentation logic影響了domain logic。為了做到分層負責與單一責任,所以物件跨層的時候要轉一次資料型態。

***

潔癖

完全做到Clean Architecture的三個原則,系統架構就會變得很乾淨,但實作上卻會很麻煩。就好像跟一個有潔癖的人住在一起,居住環境一定很乾淨,但你可能會被對方煩死。一天要擦三次地板、衣服髒了要馬上洗、裝冷飲的杯子要用杯墊,不可以讓水滴到桌上、回家要馬上消毒等等。

這幾年實作Clean Architecture的經驗,Teddy覺得只要做到以下幾點即可,其餘部分可以不用那麼「乾淨」:

  • 相依性原則在大部分的情況下都要遵守,特別是Entities層與Use Case層。如果真的需要使用到一些工具軟體且又不能不想透過依賴反轉滿足相依性原則,這樣的特例不能太多,且要自己心裡有數,承受這樣的依賴關係。
  • Interface Adapters層經常與框架緊密相關,例如Web Controller和所使用的Web Container框架緊密相關。在這種情況下,要做的完全的依賴反轉是很痛苦的一件事。所以Interface Adapters層放生,就讓它和框架緊密耦合吧。
  • 把Use Cases分成Command與Query可以簡化很多Interface Adapters層的物件設計。Command是會改變系統狀態但不回傳值(或只回傳物件的id等特定資料)的操作,Query是回傳資料但不會改變系統狀態的操作。基本上Command可以共用同一個Presenter與View Model,只有Query需要客製化的Presenter和View Model。區分Command和Query之後,Interface Adapters層可以清爽許多。

***

友藏內心獨白:沒有絕對的乾淨,無塵室也是有灰塵。

沒有留言:

張貼留言