---
title: "依賴注入 - Clean Architecture in TypeScript"
date: 2025-10-31T00:00:00+08:00
publishDate: 2025-10-31T00:00:00+08:00
lastmod: 2025-11-21T10:38:12+08:00
tags: ["TypeScript","Clean Architecture","架構","經驗","AI"]
series: "clean-architecture-in-ts"
toc: true
permalink: "https://blog.aotoki.me/posts/2025/10/31/clean-architecture-in-ts-dependency-injection/"
language: "zh-tw"
---



使用 AI 工具可以讓我們在初期很快地進行開發，大部分物件也不太需要對依賴注入的問題進行管理。然而在測試階段，能夠透過統一的介面切換測試用的物件，對於後續開發還是非常有幫助的。

我們接下來會使用 AI 協助我們一開始設定的 `tsyringe` 套件，正式導入到專案中。

<!--more-->

## 大量修改{#mass-modify}

Claude Code 跟 Aider 不同的地方是大量修改相似的行為非常容易，因為能夠自己搜尋檔案以及修改內容，不過這可能會造成大量的錯誤，我們可以用 Shift + Tab 先使用 Plan Mode 確認預計調整的範圍。

首先，輸入以下指示

````markdown
We are using the `tsyring` to manage our dependencies.

Currently, we do not use it to inject dependencies into our classes.

Let's update `src/controller` to use `tsyringe` for dependency injection.

> The `src/controller` is our entrypoint will initialize the usecase.

```typescript
const repository = container.resolve(CartRepositoryToken);
// ...
const query = new GetCartQuery(repository);
```

The usecase do not depend on the 3rd-party library, so we cannot set it as `injectable`, therefore we manually resolve it in the controller.

For other components, e.g. repository, service, presenter, we can use `@injectable()` decorator to mark them as injectable.

- `src/container.ts` is the file where we register our dependencies.
- `src/usecase/interface.ts` is the file where we define our interfaces and tokens for dependency injection.

Let's refactor the `src/controller` to use `tsyringe` for dependency injection. 

````

上面這段指示說明了我們會對 `src/controller` 進行調整，將原本使用 `new KvCarRepository` 的實作，修改為 `container.resolve(CartRepositoryToken)` 這樣的形式。

不過 Use Case 不應該去依賴第三方套件，我們不希望有 `@injectable()` 的存在，所以還是會在 Controller 手動使用 `new GetCartQuery()` 這樣的方式初始化。

另一方面，如果不明確說明我們管理依賴注入容器的習慣，AI 大多會從網路上拼湊一個可能的做法，因此需要指定 `src/container.ts` 和 `src/usecase/interface.ts` 來要求實作的範圍。

下達指示後，在 Plan Mode 應該會得到類似下面這樣的計劃確認。

```markdown
Plan: Refactor Controllers to Use tsyringe Dependency InjectionStep 1: Create Container Configuration (src/container.ts)

- Define dependency injection tokens for all interfaces
- Register concrete implementations for repositories, presenters, and services
- Configure the tsyringe container with proper bindings

Step 2: Add Injectable Decorators

- Add @injectable() to all repository classes (KvCartRepository, KvChatMessageRepository, MockProductRepository)
- Add @injectable() to all presenter classes (JsonCartPresenter, JsonChatMessagePresenter, etc.
- Add @injectable() to service classes (LlmAssistantService) - with special handling for constructor dependencies

Step 3: Update Interface Tokens (src/usecase/interface.ts

- Add injection tokens for each interface (e.g., CartRepositoryToken, ChatMessageRepositoryToken
- Export tokens for use in controllers

Step 4: Refactor Cart Controller (src/controller/cart.ts)

- Replace manual instantiation with container.resolve()
- Use tokens to resolve dependencies
- Maintain existing API structure and functionality

Step 5: Refactor Chat Controller (src/controller/chat.ts)

- Replace manual instantiation with container.resolve()
- Handle complex dependencies (LlmAssistantService with env parameter)
- Use tokens for all repository and presenter dependencies

Step 6: Handle Special Cases

- Configure LlmAssistantService to handle Cloudflare env injection
- Ensure proper lifecycle management for request-scoped dependencies

This refactoring will:
- Centralize dependency management
- Improve testability through dependency injection
- Follow Clean Architecture principles
- Maintain existing API contracts
- Make the codebase more maintainable
```

務必要仔細確認計劃的內容，以上述的內容大部分都沒有問題，那麼我們就可以先讓 Claude Code 進行第一次的修改。

## 細節修正{#fix-details}

即使 AI 產生的實作大部分都能使用，但仍可能會有一些預期外的行為，這大多跟過往出現過的實作方式有關，但不一定符合當下我們專案的狀況。

以 `src/container.ts` 為例子，很高的機率會產生一個叫做 `configureContainer` 的方法。而且 Claude Code 會在每一個 Controller 呼叫他一次，那麼就有兩種可能：

- 在 Controller 外呼叫，造成多餘的初始化
- 在 Controller 內呼叫，造成後續依賴注入的調整失效

我們可以用下面這個指示要求改進

```
We do not need `configureContainer` method, import container directly in `src/index.tsx` 
```

主要原因是 `tsyringe` 本身就已經有一個共通的 `container` 我們在預設值的情境下，直接使用 `container.register(...)` 即可，只需要確定有被 `src/index.tsx` 引用被編譯進去就沒問題。

## 例外情況{#exception}

另一個例外是 `LlmAssistantService` 的實作，在我們之前的版本裡面會使用 `env` 當作依賴，但是在後續我們改為使用 `import { env } from 'cloudflare:workers'` 的新方法後，這個就沒有必要了。

我們可以用下面的指示要求調整，這邊要發揮正確的作用需要延續在前面重構的對話下進行

```
We can use `import { env } from 'cloudflare:workers'` that we do not need make `env` as dependency, please update `LlmAssistantService`
```

在前面的計劃中，是有提到這個特殊案例

```
Step 6: Handle Special Cases

- Configure LlmAssistantService to handle Cloudflare env injection
- Ensure proper lifecycle management for request-scoped dependencies
```

但我們並沒有特別去處理，現在對 `env` 的依賴已經改為新的做法，這個限制被消除後，自然就可以使用 `@injectable` 的方式處理。

因此 AI 會在「調整依賴注入」的前提下，重新修改一次這段實作，改為 `@injectable()` 的版本，那麼我們就能在非常短的時間內處理好依賴注入的重構。
