# Interface in Clean Architecture
---
## 用一個字描述 Clean Architecture
"Component" or "Module"
---
現實世界中,一台車可以透過更換零件(Component)維修或者改良
---
軟體是為了比硬體更容易改變
---

Input Boundary / Output Bounday 是什麼?
---

系統的邊界,也就是介面(Interface)
> Data Access Interface 也可以視為一種邊界
---

書上的這張圖大致上來說每一環都是一層邊界(介面)
---

以 Domain-Driven Design 的 Layered Architecture 來看,每一層也都有邊界存在
> 借用下個月演講的簡報 😂
---

Netflix 的六角架構(Hexagonal Architecture)來看,每一圈也是一種邊界
---
大多數時候我們認知的邊界都是最外層,也就是 API(Application Interface)或者 UI(User Interface)
---

回來看這張圖 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(¶ms) // Interface Adapter
// ...
}
```
以 Golang 來說,處理 HTTP 請求屬於 Presenter Layer 的角色,在 MVC 框架實際上是 View
---
對外我們很熟悉,服務的內部呢?
> 終於到今天的主題 😅
---
```go=
func (api *Api) ReputationStatus(w http.ResponseWriter, r *http.Request) {
var params RequestRequest
json.Unmarshal(¶ms)
// UseCase
reputation := api.reputation.CreateByNumber(params.Region, params.Number)
// ...
}
```
對 Presenter 來說我們「已知」許多 UseCase API 可以呼叫
---

Clean Architecture 中的 Interface 不是指程式語言的「介面」而是職責類型的「邊界」
---

XRS - API Architecture 中有跟大家分享
> 文件監修中,有點過期但是大方向是這樣的
---

提供系統操作的「介面」可能是 UI 或者 API
> 因此要定義 API Schema 或者 UI Wireframe 決定「如何使用」
---

商業流程,提供系統使用的「方式」
> 會因為公司的業務改變而有變化,如:查詢「一筆」資料變為查詢「多筆」
---

商業邏輯,一個系統的固定規則
> 電話號碼必須是 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}]"}