---
title: "從 wire 學到依賴注入沒有講的事"
date: 2023-06-21T00:00:00+08:00
publishDate: 2023-06-21T00:00:00+08:00
lastmod: 2025-04-13T20:34:57+09:00
tags: ["經驗","心得","Golang","Dependency Injection","Clean Architecture"]
toc: true
permalink: "https://blog.aotoki.me/posts/2023/06/21/learn-dependency-injection-from-google-wire/"
language: "zh-tw"
---


最近在工作中對專案套用 [google/wire](https://github.com/google/wire) 來處理依賴注入（DI，Dependency Injection）時，發現有非常多小細節需要注意。大多數語言其實都會碰到這類問題，然而 `wire` 的設計沒有採用大多數框架會提供的控制反轉（IoC，Inverse of Control）機制來處理，反而讓很多過去沒有考慮的狀況浮現出來。

<!--more-->

## 什麼是依賴注入{#what-is-dependency-injection}

要簡單的解釋依賴注入，就是讓物件的依賴從外部提供，而不是自己解決。舉例來說，我們有一個如下的依賴結構， `UserRepository` 所依賴的 `DatabaseClient` 應該在初始化時被提供進去，而不是自己建構出來。

![圖一](images/01.png)

用 Golang 來舉例，我們應該避免這樣實作：

```go
type UserRepository struct {
  db *infra.Database
}

func NewUserRepository(config *app.Config) (*UserRepository, error) {
  db, err := infra.NewDatabase(config.DatabaseURL)
  if err != nil {
    return nil, ErrUnableSetupDatabase
  }

  return &UserRepository {
    db: db,
  }
}
```

而是要將 `infra.Database` 注入到 `UserRepository` 裡面：

```go
type UserRepository struct {
  db *infra.Database
}

func NewUserRepository(db *infra.Database) *UserRepository {
  return &UserRepository {
    db: db,
  }
}
```

大多數時候我們也都能考量到這點，然而這還是會有許多衍生的問題。最常見的是在圖中我們的 `UserRepository` 是 `ProfileController` 依賴的對象，那們我們還有可能做出這樣的設計。

```go
type ProfileController struct {
  db *infra.Database
}

func (ctrl *ProfileController) GetProfile(userId int) *ProfileData {
  repo := repositories.NewUserRepository(ctrl.db)
  // ...
  profile := repo.Find(userId)

  return &ProfileData {
    // ...
  }
}
```

我們確實注入了依賴沒錯，然而這個依賴是 `ProfileController` 提供的會是問題的原因，假設我們的依賴是非常深的狀況，那麼每一層物件都需要攜帶這個 `infra.Database` 物件，直到所有的依賴被解決。

對於 `ProfileController` 來說，他的依賴是 `UserRepository` 那麼他應該要接受的是被注入這個物件，而不是這個物件的依賴。

```go
type ProfileController struct {
  repo *UserRepository
}

func (ctrl *ProfileController) GetProfile(userId int) *ProfileData {
  // ...
  profile := repo.Find(userId)

  return &ProfileData {
    // ...
  }
}
```

要解決這樣的問題，就是控制反轉出場的時機。

## 控制反轉的任務{#the-mission-of-inverse-of-control}

經常跟依賴注入一起討論的控制反轉，通常是「控制反轉容器」的實現，我們拿 C# 的 [Niject](http://www.ninject.org/) 來當作例子，文件提供了一個實作範例。

```csharp
// 被注入者
public class Samurai {
    public IWeapon Weapon { get; private set; }
    public Samurai(IWeapon weapon) {
	    this.Weapon = weapon;
	}
}

// 容器定義
public class WarriorModule : NinjectModule
{
    public override void Load() {
            this.Bind<IWeapon>().To<Sword>();
    }
}
```

我們會在應用啟動時，建構一個容器（通常是一個）來記錄所有型別對應的關係，像是上面的例子就描述了當需要 `IWeapon` 時，要提供 `Sword` 物件進去。這個機制需要依賴程式語言的反射（Reflect）功能，在沒有反射的語言中通常就不會有這樣的實作。

例如 TypeScript 有 [InversifyJS](https://inversify.io/) 可以使用，在 JavaScript 中就沒有對應的套件。在 Ruby 裡面也沒有，想要達到類似的效果，以 [dry-container](https://dry-rb.org/gems/dry-container/0.11/) 這個 Gem 來說，他只能利用 Key-Value 的的方式來模仿這件事情。

這個方式我們需要先向控制反轉容器進行註冊，因此所有物件如何建構都是在啟動時期就被確定下來的，何時被實例化就要看容器的設計，以前面提及的 Niject 和 InversifyJS 為例子，他們都允許延遲生成（在有實際需要時才實例化）

> 經常拿來跟 wire 比較的 [uber-go/fx](https://uber-go.github.io/fx/) 就屬於控制反轉容器的實作。

控制反轉的另一種應用，則是[服務定位（Service Locator）](https://martinfowler.com/articles/injection.html#UsingAServiceLocator)模式，跟依賴注入不同的地方是我們會在需要用到的時候呼叫 Locator 物件來取的想要的物件，這個應用主要被使用在 Router 上，跟前面提到的 Key-Value 對應方式有點類似。

下面是幾年前我用 InversifyJS 來實現 Discord Bot 機器人命令功能的實作：

```typescript
  async on(interaction: Interaction) {
    if (!interaction.isChatInputCommand()) {
      return
    }

    const commandClass = this.routes.get(interaction.commandName)
    if(!commandClass) {
      return
    }

    try {
      const command = container.resolve(commandClass)
      command.setInteraction(interaction)
      await command.execute()
    } catch(error) {
      console.error(error)
      await interaction.reply({ content: '我故障了！', ephemeral: true });
    }
  }
```

上面的例子，我們會去詢問這個命令（如：`/list`）對應的類別（Class）是什麼，然後讓控制反轉容器嘗試將這個命令物件實例化，在這種狀況下使用服務定位的技巧就沒有太大的問題，然而在處理依賴注入的時候則要避免這種方式。

> 正因如此，在 `uber-go/fx` 底層實際上用來處理依賴注入的 [uber-go/dig](https://github.com/uber-go/dig) 是有明確的要求我們不應該用於服務定位的情況。

## Clean Architecture 的依賴{#dependency-in-clean-architecture}

去年讀完 Clean Architecture 後有寫了這篇[讀 Clean Architecture 學習依賴管理](https://blog.aotoki.me/posts/2022/11/18/learn-dependency-management-by-clean-architecture/)來說明對於書中的內容的理解，如果把 Clean Architecture 的觀念在放到依賴注入中，又會發現有更多的議題需要討論。

在 Clean Architecture 提到了元件（Component）應該要能夠被抽換的，因此我們需要能夠區分出哪些物件要被分組在一起，構成一個元件。將前面說明依賴注入的插圖根據實際的情境調整，會得到這樣的關係圖。

![圖2](images/02.png)

這張圖中先簡單的依照 Clean Architecture 舉例的類型把物件稍微分類出來，這時候我們會注意到 `UserRepository` 已經變成了一個介面（Interface）並且由 `PostgresUsers` 來實作。

再繼續觀察 `Infrastructure` 的依賴狀況，扣掉 `PostgresUsers` 的情況是完全只對這個分組裡面的物件互相依賴，在 `UseCases` 裡面則是指定義了介面，根據需要注入到裡面。這也是依賴注入會使用的技巧之一，這個方式也讓依賴關係被消除。

> 有一篇文章 [Using interfaces in Go the right way](https://medium.com/@mbinjamil/using-interfaces-in-go-the-right-way-99384bc69d39) 提到，在 Golang 裡面介面應該要由 Consumer（消費者）來定義，上圖的例子是 `ProfileUseCase` 在使用 `UserRepository` 所以才會被劃分在 `UseCase` 群組裡面，被當作同一個套件。

假設我們繼續延伸上述這張以 Clean Architecture 作為參考的依賴關係圖，會發現大多數情況的依賴關係都不會超過一層，以 `UseCases` 來說，他的依賴固定是 `Entites` 的內容，而 `Controllers` 的依賴就固定會是 `UseCases`。

如果從這樣的角度去看 Domain-Driven Design（DDD，領域驅動設計）提到到的 Layered Architecture（階層式架構）是有其道理在的，越上層的物件是對下一層的物件的封裝。在最上層則是表現層（Presentation Layer）或稱 UI Layer（使用者介面），剛好就是曝露給使用者操作的意思。

## Wire 的使用{#how-wire-works}

根據前面提到的限制跟使用方式，就不難想像 `wire` 應該如何使用，我們會去使用 `wire` 的目的就是要希望可以輕鬆的將整個應用的依賴一次性的建構完畢，如果要自己實作的話會變成這樣。

```go
func main() {
  config := &infra.Config{
    DatabaseURL: "postgres://...",
  }
  dbClient, err := infra.NewDatabase(config)
  if err != nil {
    panic(err)
  }
  // ...
  users := postgres.NewUsers(dbClient)
  profileUseCase := usecases.NewProfile(users)
  profileCtrl := controllers.NewProfile(profileUseCase)
  app := app.New(profileCtrl)
  // ...
}
```

不用太過於複雜的應用，就會需要花上非常多心力去維護。那麼，使用 `wire` 之後，就可以利用 `wire.Build` 和 `wire.NewSet` 來維護，就可以整理成像這樣的實作。

```go
var repoSet = wire.NewSet(
  postgres.NewUsers,
  wire.Bind(new(usecases.UserRepository), new(*postgres.Users)), // 綁定介面
)

// ...

func initApp(cfg *infra.Config) (*app.Application, error) {
  wire.Build(
    infraSet,
    repoSet,
    usecaseSet,
    ctrlSet,
    app.New,
  )

  return nil, nil
}
```

之後只需要維護這些物件的建構函示列表，就能讓 `wire` 自動的生成所有初始化的函示，透過 `Set` 的組合變化，還能做出些微調，而不需要重新處理。

在依賴注入的處理中跟 Clean Architecture 描述的依賴關係必定是 DAG（Directed Acyclic Graph，有向無環圖）是一樣的，我們所有物件的依賴最後一定會有終點，如果沒有終點的話則會造成循環依賴（Circular Dependency）而無法順利初始化。以我們上面的例子，`initApp` 的依賴終點是 `infra.Config` 這個物件，也符合我們「提供設定初始化應用程式」的期待，其他的細節都被隱藏起來。

除了上述的基本使用方式外，在 `wire` 也有一些限制，像是每一種型別只能「提供一次」那麼下面這樣的處理就無法被實現。

```go
// 依賴
func NewA(name string) *A {
  // ...
}

func NewB(name string) *B {
  // ...
}

func NewApp(a *A, b *B) *App {
  return &App{a, b}
}

// Wire 實作

func initApp() *App {
  wire.Build(
    NewA, // name = "A"
    NewB, // name = "B"
    NewApp,
  )
  return nil
}
```

我們除了選擇讓 `name` 是相同的之外，對 `wire` 來說是無法區分出 `NewA` 和 `NewB` 參數的差異來注入不一樣的數值，這也讓這種我們很常會在 Golang 套件看到的實作能看出背後的目的。

```go
func NewA(config *ConfigA) {
  // ...
}

func NewB(config *ConfigB) {
  // ...
}
```

當我們用一個結構定義後，因為他們具備了不同的型別，對 `wire` 來說就可以區分出來，那麼我們就可以像這樣子去實作。

```go
func initApp() *App {
  wire.Build(
    wire.Value(ConfigA{Name: "A"}), // 固定數值
    wire.Struct(new(ConfigB), "B"), // 動態產生
    NewA, // name = "A"
    NewB, // name = "B"
    NewApp,
  )

  return nil
}
```

對應這樣的情境，如果是固定的設定值，可以用 `wire.Value` 或者 `wire.Struct` 的方式提供，或者定義一個新的工廠來生成這個設定。

```go
func newConfigA(cfg *Config) *ConfigA {
  return &ConfigA{
    Name: cfg.Prefix + "A",
  }
}

// ...

func initApp(config *Config) *App {
  wire.Build(
    newConfigA,
    newConfigB,
    NewA, // name = "Example_A"
    NewB, // name = "Example_B"
    NewApp,
  )

  return nil
}
```

基於這個技巧我們可以用來針對 Middleware 這類情境，還能做出類似這樣的效果。

```go
func provideMiddleware(
  config *Config,
  jwtAuth *jwt.Authenticator
) []OptionFn {
  options := make([]OptionFn, 0)

  if config.AuthEnabled {
    options = append(
      options,
      WithAuthenticator(jwtAuth), // 實作 `server.Use(xxx)` 啟用 Middleware
    )
  }

  // ...

  return options
}

func newHttpServer(options ...OptionFn) *echo.Echo {
  server := echo.New()

  for _, fn := range options {
    fn(server)
  }

  return server
}

func initApp(config *Config) *App {
  wire.Build(
    provideMiddleware,
    newHttpServer,
    newApp,
  )

  return nil
}
```

像這樣子調整後，我們就能更輕鬆的提供初始化的調整選項，甚至直接在 `initApp()` 裡面注入對應的 Hook 來讓使用者自行定義。

依賴注入看起來是個很簡單的概念，在實作上卻有許多跟架構規劃問題上互相關聯的問題存在，如果沒有想清楚就很容易規劃了錯誤的依賴關係，而讓系統的複雜度增加。

> 邊界的規劃也是很重要的，像是 `UserRepository` 的介面被劃分出來後，我們在單元測試的時候只需要注入一個 Mock（造假）的物件即可，而不需要把完整的依賴搭建出來，也能讓可測試性獲得非常大的改善。

