# Interface in Clean Architecture --- ## 用一個字描述 Clean Architecture "Component" or "Module" --- 現實世界中,一台車可以透過更換零件(Component)維修或者改良 --- 軟體是為了比硬體更容易改變 --- ![](https://hackmd.io/_uploads/Sy2iGU4an.jpg) Input Boundary / Output Bounday 是什麼? --- ![](https://hackmd.io/_uploads/Sy2iGU4an.jpg) 系統的邊界,也就是介面(Interface) > Data Access Interface 也可以視為一種邊界 --- ![](https://hackmd.io/_uploads/B1ipUDVah.jpg =60%x) 書上的這張圖大致上來說每一環都是一層邊界(介面) --- ![](https://hackmd.io/_uploads/HJ3dvPNp3.png) 以 Domain-Driven Design 的 Layered Architecture 來看,每一層也都有邊界存在 > 借用下個月演講的簡報 😂 --- ![](https://hackmd.io/_uploads/B1L1ODNp3.png =60%x) Netflix 的六角架構(Hexagonal Architecture)來看,每一圈也是一種邊界 --- 大多數時候我們認知的邊界都是最外層,也就是 API(Application Interface)或者 UI(User Interface) --- ![](https://hackmd.io/_uploads/B1ipUDVah.jpg =60%x) 回來看這張圖 Presenter 的邊界就是 UI or Web(RESTful API) 也被視為 Interface Adapter(介面轉接器) --- ```go= type ReputationRequest { Number `json:"number"` } // Controller (Input) + Presenter (Output) func (api *Api) ReputationStatus(w http.ResponseWriter, r *http.Request) { var params RequestRequest json.Unmarshal(&params) // Interface Adapter // ... } ``` 以 Golang 來說,處理 HTTP 請求屬於 Presenter Layer 的角色,在 MVC 框架實際上是 View --- 對外我們很熟悉,服務的內部呢? > 終於到今天的主題 😅 --- ```go= func (api *Api) ReputationStatus(w http.ResponseWriter, r *http.Request) { var params RequestRequest json.Unmarshal(&params) // UseCase reputation := api.reputation.CreateByNumber(params.Region, params.Number) // ... } ``` 對 Presenter 來說我們「已知」許多 UseCase API 可以呼叫 --- ![](https://hackmd.io/_uploads/Sy2iGU4an.jpg) Clean Architecture 中的 Interface 不是指程式語言的「介面」而是職責類型的「邊界」 --- ![](https://hackmd.io/_uploads/BynjRPE62.png) XRS - API Architecture 中有跟大家分享 > 文件監修中,有點過期但是大方向是這樣的 --- ![](https://hackmd.io/_uploads/SJzOYF4Tn.png) 提供系統操作的「介面」可能是 UI 或者 API > 因此要定義 API Schema 或者 UI Wireframe 決定「如何使用」 --- ![](https://hackmd.io/_uploads/ByyFtKVp2.png) 商業流程,提供系統使用的「方式」 > 會因為公司的業務改變而有變化,如:查詢「一筆」資料變為查詢「多筆」 --- ![](https://hackmd.io/_uploads/SJuKYFVTn.png) 商業邏輯,一個系統的固定規則 > 電話號碼必須是 E164 格式,網址只有 `http` 起始的才允許使用 --- ## Infrastructure Layer 輔助 Presenter / Application Layer 的其他類型物件 > 如 `Repository` 是符合 Application Layer 期待的實作 --- Example: WRS 查詢網址是否有風險 | Layer | Description | |-------|--------------| | Presenter | RESTful API | Application | 進行查詢、選取最佳結果 | Domain | 解析網址、計算權重 | Infrastructure | 從 DynamoDB 取得聲望資料 > 如果太抽象我們就開原始碼來讀 --- ```go= type ReputationRepository interface { FindByNumber(region, number string) *entity.Reputation } type ReputationReply struct { SpamCategory string } func (uc *Reputation) CreateByNumber(region, number string) *ReputationReply { reputation := uc.reputations.FindByNumber(region, number) // ... return &ReputationReply{ SpamCategory: reputation.SpamCategory } } ``` 為什麼要有 `ReputationReply` 的存在? --- `ReputationReply` 是 Application Layer 的介面(OutputBoundary)這樣一來 Presenter Layer 才不會知道 Domain Layer 的資訊 > 同時也減少了耦合,我們可以輕鬆的抽換 Application Layer --- ```go= type ReputationUseCase interface { CreateByNumber(region, number, string) usecase.ReputationReply } func New(reputation ReputationUseCase) *Api { return &Api{ reputation: reputation, } } ``` 假設我們將 UseCase 也撰寫成介面呢? --- ```go= func main() { // ... var reputationUseCase := usecase.NewRegionalReputation(config.Region) if config.TenantOf('Gogolook') { reputationUseCase = usecase.NewGlobalReputation() } apiServer := api.New(reputationUseCase) // ... } ``` 我們就得到抽換 UseCase 提供給不同客戶不一項版本的特性 > 假設我們沒有定義 `usecase.ReputationReply` 直接回傳 `entity.Reputation` 擴充難度會增加多少? --- ```go= type ReputationRepository interface { FindByNumber(region, number string) *entity.Reputation } // ... func (uc *Reputation) CreateByNumber(region, number string) *ReputationReply { reputation := uc.reputations.FindByNumber(region, number) // ... } ``` 同理可證,為什麼 `ReputationRepository` 是一個介面,因為我們希望可以替換資料庫 --- ```go= type Route interface { Path() string Handler() http.HandlerFunc } ``` ```go= func NewServer(routes Route[]) { // ... for route := range routes { router.Add(route.Path(), route.Handler()) } // ... } ``` 這是 Docker API Server 的實作,巧妙的讓擴充 API 變得非常容易 --- ```go= // NRS /v1/reputation_status type ReputationSource struct { // ... Provider string `json:"provider"` } ``` ```go= // NRS /v2/reputation_status type ReputationSource struct { Provider string `json:"provider"` Channel string `json:"channel"` } ``` 當遇到這樣的情況,會怎麼實作? > 在 Controller 上實作嗎? --- ```go= // V1 func (api *Api) ReputationStatus(w http.ResponseWriter, r *http.Request) { // ... reputation := api.reputation.CreateByNumber(params.Region, params.Number) status := &ReputationStatus{ Provider: reputation.Source[0].Provider } enc := json.NewEncoder(w) enc.encode(status) } ``` NRS v1 實作會呼叫 UseCase 處理邏輯取得結果 --- ```go= // V2 func (api *Api) ReputationStatus(w http.ResponseWriter, r *http.Request) { // ... reputation := api.reputation.CreateByNumber(params.Region, params.Number) status := &ReputationStatus{ Provider: reputation.Source[0].Provider, Source: reputation.Source[0].Source } enc := json.NewEncoder(w) enc.encode(status) } ``` NRS v2 只有改變「介面」內部邏輯完全沒有變化 --- ## 針對介面的思考 透過區分職責(類型)劃分出不同種類的物件,再透過介面的定義(約定)來限制物件之間的依賴,對「重複」要思考清楚是必要還是非必要 --- 當我們對介面的設計足夠完善,就能像替換車子的零件、外殼來改變系統 > 有時候介面是隱含的不一定要明確定義(如:UseCase / Domain 不會定義介面)然而需要時我們能很快重構出介面版本 --- ## 針對開發的影響 介面確定下來後要跟 PM/QA 協作也很容易,像是 API 確立後就能寫自動化測試 > 可以想像測試的入口都是那些介面(e.g. Public Method) --- ## 針對系統的反思 最近了解到 SmartUI 和 Domain-Driven 的區別,前者是思考「畫面有什麼,資料庫就有什麼」的情境,後者則是思考「能做什麼」再決定資料庫「要記錄什麼」 > XRS 使用 DynamoDB 沒有以 ORM 作為基礎的理由,我們會把資料轉換成系統中發揮某個作用的物件,而不是資料的映射
{"title":"Interface in Clean Architecture","lang":"zh-TW","description":"Interface in Clean Architecture","contributors":"[{\"id\":\"f37d49bd-d692-405a-b768-5e0a6d63c524\",\"add\":6001,\"del\":113}]"}
    162 views