---
title: "反思：必要性 - Clean Architecture in Go"
date: 2025-05-23T00:00:00+08:00
publishDate: 2025-05-23T00:00:00+08:00
lastmod: 2024-10-07T20:05:43+08:00
tags: ["Golang","Clean Architecture","架構","經驗"]
series: "clean-architecture-in-go"
toc: true
permalink: "https://blog.aotoki.me/posts/2025/05/23/clean-architecture-in-go-reflection-the-necessary/"
language: "zh-tw"
---


我們討論了不少 Clean Architecture 在 Golang 實踐的方式和技巧，然而我們是否真的有必要在 Golang 使用，以及使用之後我們能獲得怎樣「實質」的優點。我們經常會聽到 Clean Architecture 的優點，卻很少有實際的案例討論。

<!--more-->

## 區隔商業邏輯{#protect-busines-logic}

在 Clean Architecture 中提到的容易修改通常是指底層元件的抽換，也就是商業邏輯以外的部分。像是我們對資料儲存、API 協定這類情境的範例，就是在展示不修改商業邏輯的前提下，能夠任意地進行替換的特性。

我們不這樣設計也是完全沒問題的，然而很多情境我們經常會有這樣的需求。舉例來說，我們預計從 GCP 轉換到 AWS 上，此時需要將 SDK 轉換，假設我們將這些跟雲端環境呼動的 SDK 跟商業邏輯寫在一起，就會變得難以修改。

```go
// ...
func (uc *VerifyUseCase) Execute(ctx context.Context, input *VerifyUseCaseInput) (*VerifyUseCaseOutput, error) {
  // ...

  // 上傳檔案到 GCS
  obj := bkt.Object("user-document.pdf")
  w := obj.NewWriter(ctx)
  // ...
}
```

以上述的例子，GCS 和 S3 的 SDK 實作差異非常大，然而我們的目的都是要把 `user-document.pdf` 保存到某種 Object Storage 的服務，在商業邏輯的角度上並不需要知道使用哪個服務的細節，因此我們可以像這樣處理。

```go
type DocumentWriter interface {
  Write(ctx context.Context, io.Reader) error
}

func (uc *VerifyUseCase) Execute(ctx context.Context, input *VerifyUseCaseInput) (*VerifyUseCaseOutput, error) {
  // ...
  uc.documentWriter.Write(ctx, input.DocumentContent)
  // ...
}
```

那麼未來除非商業邏輯改變，這個步驟不再需要修改檔案，我們就不需要去修改這個商業邏輯。這是實踐 Clean Architecture 後才能有的優點，也讓可測試性可以利用實作各種 Mock 物件來得到改善。

> 實際上我們依照軟體開發的一些慣例逐步實踐，最終仍會得到這樣的結果，只是 Clean Architecture 透過一個系統性的方式向我們介紹。

## 可擴充性{#extensibility}

另一方面，因為我們將商業邏輯切割開來後，在功能的擴充性也能得到不錯的改善。以下是一個實際的情境，原版的設計只預期到我們只需要限定特定使用者才能存取，因此設計了這樣的行為。

```go
type Config struct {
	AllowedUserIds []string
}

// ...
type PermissionChecker interface {
  IsAllowed(ctx context.Context, userId string) bool
}

// ...
func NewPermissionChecker(config *config.Config) *MyPermissionChecker {
  return &MyPermissionChecker{
    allowedIds: config.AllowedUserIds
  }
}

// ...
var DefaultSet = wire.NewSet(
  NewPermissionChecker,
  wire.Bind(new(PermissionChecker), new(*MyPermissionChecker)),
)

// ...
func (uc *BatchUpdateUseCase) Execute(ctx context.Context, input *BatchUpdateCaseInput) (*BatchUpdateUseCaseOutput, error) {
  if !uc.permission.IsAllowed(ctx, input.UserId) {
    return nil, ErrPermissionNotAllowed
  }
}
```

此時，當我們需要對另一個功能增加使用者權限，但是需要提供不同的使用者權限，此時就可以利用 `wire` 和 Golang 的特性做一些有趣的調整。

```go
type Config struct {
	AllowedUpdateUserIds []string
	AllowedDeleteUserIds []string // 加入刪除權限
}

// ...
type PermissionChecker interface {
  IsAllowed(ctx context.Context, userId string) bool
}

// 利用新增型別讓 wire 注入不同物件
type UpdatePermissionChecker PermissionChecker
type DeletePermissionChecker PermissionChecker

// ...
func NewUpdatePermissionChecker(config *config.Config) *MyUpdatePermissionChecker {
  return &MyUpdatePermissionChecker{
    allowedIds: config.AllowedUpdateUserIds
  }
}

func NewDeletePermissionChecker(config *config.Config) *MyDeletePermissionChecker {
  return &MyDeletePermissionChecker{
    allowedIds: config.AllowedDeleteUserIds
  }
}

// ...
var DefaultSet = wire.NewSet(
  NewUpdatePermissionChecker,
  wire.Bind(new(UpdatePermissionChecker), new(*MyUpdatePermissionChecker)),
  NewDeletePermissionChecker,
  wire.Bind(new(DeletePermissionChecker), new(*MyDeletePermissionChecker)),
)

// ...
func (uc *BatchUpdateUseCase) Execute(ctx context.Context, input *BatchUpdateCaseInput) (*BatchUpdateUseCaseOutput, error) {
  if !uc.permission.IsAllowed(ctx, input.UserId) {
    return nil, ErrPermissionNotAllowed
  }
}

func (uc *BatchDeleteUseCase) Execute(ctx context.Context, input *BatchDeleteCaseInput) (*BatchDeleteUseCaseOutput, error) {
  if !uc.permission.IsAllowed(ctx, input.UserId) {
    return nil, ErrPermissionNotAllowed
  }
}
```

因為我們在設計商業邏輯時，為了跟底層細節區隔開來，幾乎所有傳遞給 UseCase 的物件都是介面，因此在擴充時我們只需要考慮哪個物件要注入到哪個介面即可。

基於這樣的理由，在擴充上就不需要在每段使用到特定機制的實作修改，而是調整注入的物件，就可以達到改變行為或者擴充的目的。

因此原有系統的複雜度並不會消失，我們透過切割和轉移將複雜的問題分配在適當的位置，來讓維護系統的難度降低。

## 可能性{#possibility}

在學習 Clean Architecture 的過程中，另一個會讓人感到疑惑的地方，是一種似乎沒有正確解答的感覺。這是很正常的，因為架構議題通常是大方向，也因此會有許多不一樣的形式和做法。

舉例來說，我們在目前的設計上都沒有使用過 Presenter 類型的物件，因為這樣我們會需要讓 Presenter 拿到當下的 `http.ResponseWriter` 才能順利指定要呈現的內容。

如果還需要注入到 UseCase 裡面的話，就會跟現有的做法非常不同（可能要有個 UseCaseFactory）但是在 [RubyJam](https://www.facebook.com/RubyJamTW/) 的交流中，我看到 Ruby on Rails 框架倒是很容易能這樣做。

```ruby
class JsonPresenter
  def fail(error)
    # ...
  end

  def success(payload)
    @controller.render json: payload
  end
end

# ...
def update
  json_presenter = JsonPresenter.new(self)

  usecase = VerifyUseCase.new(
    presenter: json_presenter
  )

  usecase.call(params)
end
# ...
```

一部分是因為 Rails 沒有預期使用依賴注入框架，因此都是在呼叫時產生物件，那麼就可以很輕易的把回傳處理的實作當作依賴注入進去。

不同語言也會根據不同語言特性有不同的做法，即使在 Golang 我們使用 `wire` 或者 `fx` 可能都會有不一樣的設計方式，然而只要掌握 Clean Architecture 想解決的核心問題，就能打造容易維護、擴充的系統。

