l

2022年7月18日 星期一

再談Clean Architecture實作

July 18 18:20~19:41

▲圖1:《Clean Architecture》書中最近接實作的一張圖

 

前言

圖1是《Clean Architecture》書中最近接實作的一張圖,雖然Teddy一開始實作Clean Architecture主要就是參考這張圖,但Teddy一直覺得這張圖畫得有問題。一開始只看出幾個很明顯的問題,例如:

  • 分層不清楚:Data Access到底在第三還是第四層?如果是第三層,為什麼違反依賴原則直接存取資料庫?如果是第四層,為什麼直接跨層實作在第二層的Data Access Interface?
  • 誰來維持跨層原則:Data Access Interface直接參考最內層的Entity,那麼當Entity離開Use Case Layer往外傳的時後是誰負責轉換?
  • 用語不一致:這張圖過於強調 Use Case Interactor與Input Boundary, Output Boundary以及Controller和Presenter彼此之間的關係,忽略了其他Boundary。例如,Data Access Interface應該也是一種Output Boundary,但卻沒有用Output Boundary這個名稱而是用Data Access Interface這種很特定的稱呼。
    這個現象導致讀者可能以為Output Boundary與Data Access Interface是兩種不同的概念,但實際上依據六角形架構的講法,他們應該都是Output Port。

***

因為要幫《Clean Architecture實作篇:在整潔的架構上弄髒你的手》這本翻譯的新書(預計八月出版)寫推薦序,Teddy昨天用力把這本書看完。這本書英文版一年多前Teddy就讀過,還寫了書評:<【還少一本書】Get Your Hands Dirty on Clean Architecture>。但過了一年多書中有些細節已經不復記憶,昨晚將書中的做法與圖1仔細對照,突然覺得Teddy之前被圖1給制約了,ezKanban目前套用Clean Architecture的方式還可以進一步優化。今天就來談如何簡化Clean Architecture的實作。

***

 

更多地雷

除了上述提到的問題以外,在《Clean Architecture》中,關於Use Case Layer與外界溝通的介面,使用了如圖1中的Input BoundaryOutput Boundary的術語,但是在書中圖22.1(請參考圖2)右下角又使用Use Case Input PortUse Case Output Port這個與六角形架構中比較接近的用語(使用port)。但不管圖1還是圖2,這種Use Case Interactor與Use Case Input Port和Use Case Output Port之間關係的表達方式,很容易讓人誤會以下幾點:

  • 看圖1與圖2會讓人以為實作Use Case Output Port的物件就只有Presenter,但實際上一個Use Case Interactor可以使用任意個Output Port。例如,將物件儲存到資料庫中也是一種Output Port。
  • Use Case Input Port到底是什麼?是Command嗎?還是Use Case本身的介面?
  • 在圖1中,感覺Input Boundary只需要定義Input Data,Output Boundary則是定義Output Data。但以一個介面而言,介面上會包含輸入資料(Input Data)與回傳資料(Output Data),例如 int getFileSize(File file)這個介面,其中File就是Input Data,int就是Output Data。

 

▲圖2:《Clean Architecture》書中圖22.1

 

***

看圖說故事

圖3是Teddy將圖1重新修正後的版本,重點如下:

  • 介面就只有兩種:Input Port(Input Boundary)與Output Port(Output Boundary)。
  • 每一個Input Port與Output Port都有Input Data與Output Data(宣告在介面上的輸入與輸出資料)。
  • Presenter不需要實作Output Port,它只要接收Input Port的Output Data就可以建出Read Model。讓Controller直接與Presenter產生耦合,將Input Port的回傳資料(Output Data)直接傳給Presenter,Use Case Interactor不需要注入Presenter。
  • 在套用CQRS的情況下,Projection (一種Output Port) 可以直接產出前端所需要的 View Model,不需再經過Presenter。
     

 

▲圖3:修正後的圖1

***

程式範例

Teddy花了四個小時把【領域驅動設計與簡潔架構入門實作班】(https://teddysoft.tw/courses/clean-architecture/) 的課程範例改成新的寫法,覺得程式碼又更乾淨了一些。接下來將程式範例與圖3對照,首先參考圖4,它是ezKanban中CreateCardUseCase的介面宣告,相當於圖3的Input Port。其中CreateCardInput是Input Data,CqrsCommandOutput是Output Data。



 ▲圖4:Input Port

 

圖5為package結構,Input Port的檔案放在in package中。CqrsCommandOutput因為是系統核心共用物件,所以沒有出現在圖5裡面。另外,CreateCardUseCaseImpl就是圖3的Use Case Interactor,也有人會取CreateCardUseCaseService這樣的名稱。


▲圖5:Package結構

 

CreateCardUseCaseImpl程式碼如圖6所示,它實作Input Port(CreateCardUseCase),也使用了一個Output Port(CardRepository)。


▲圖6:CreateCardUseCaseImpl程式碼

 

參考圖3的架構,再搭配程式範例,Teddy覺得比原本《Clean Architecture》書中提到的做法要具體很多。


***

 

工商服務

領域驅動設計與簡潔架構入門實作班】課程招生中,本梯次內容將採用最新簡化過的Clean Architecture實作方式做為課程範例,歡迎舊雨新知多加利用。

***

友藏內心獨白:持續改善不是嘴巴講講而已。

沒有留言:

張貼留言