依賴注入 - Clean Architecture in TypeScript
使用 AI 工具可以讓我們在初期很快地進行開發,大部分物件也不太需要對依賴注入的問題進行管理。然而在測試階段,能夠透過統一的介面切換測試用的物件,對於後續開發還是非常有幫助的。
我們接下來會使用 AI 協助我們一開始設定的 tsyringe 套件,正式導入到專案中。
大量修改
Claude Code 跟 Aider 不同的地方是大量修改相似的行為非常容易,因為能夠自己搜尋檔案以及修改內容,不過這可能會造成大量的錯誤,我們可以用 Shift + Tab 先使用 Plan Mode 確認預計調整的範圍。
首先,輸入以下指示
1We are using the `tsyring` to manage our dependencies.
2
3Currently, we do not use it to inject dependencies into our classes.
4
5Let's update `src/controller` to use `tsyringe` for dependency injection.
6
7> The `src/controller` is our entrypoint will initialize the usecase.
8
9```typescript
10const repository = container.resolve(CartRepositoryToken);
11// ...
12const query = new GetCartQuery(repository);
13```
14
15The usecase do not depend on the 3rd-party library, so we cannot set it as `injectable`, therefore we manually resolve it in the controller.
16
17For other components, e.g. repository, service, presenter, we can use `@injectable()` decorator to mark them as injectable.
18
19- `src/container.ts` is the file where we register our dependencies.
20- `src/usecase/interface.ts` is the file where we define our interfaces and tokens for dependency injection.
21
22Let'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 應該會得到類似下面這樣的計劃確認。
1Plan: Refactor Controllers to Use tsyringe Dependency InjectionStep 1: Create Container Configuration (src/container.ts)
2
3- Define dependency injection tokens for all interfaces
4- Register concrete implementations for repositories, presenters, and services
5- Configure the tsyringe container with proper bindings
6
7Step 2: Add Injectable Decorators
8
9- Add @injectable() to all repository classes (KvCartRepository, KvChatMessageRepository, MockProductRepository)
10- Add @injectable() to all presenter classes (JsonCartPresenter, JsonChatMessagePresenter, etc.
11- Add @injectable() to service classes (LlmAssistantService) - with special handling for constructor dependencies
12
13Step 3: Update Interface Tokens (src/usecase/interface.ts
14
15- Add injection tokens for each interface (e.g., CartRepositoryToken, ChatMessageRepositoryToken
16- Export tokens for use in controllers
17
18Step 4: Refactor Cart Controller (src/controller/cart.ts)
19
20- Replace manual instantiation with container.resolve()
21- Use tokens to resolve dependencies
22- Maintain existing API structure and functionality
23
24Step 5: Refactor Chat Controller (src/controller/chat.ts)
25
26- Replace manual instantiation with container.resolve()
27- Handle complex dependencies (LlmAssistantService with env parameter)
28- Use tokens for all repository and presenter dependencies
29
30Step 6: Handle Special Cases
31
32- Configure LlmAssistantService to handle Cloudflare env injection
33- Ensure proper lifecycle management for request-scoped dependencies
34
35This refactoring will:
36- Centralize dependency management
37- Improve testability through dependency injection
38- Follow Clean Architecture principles
39- Maintain existing API contracts
40- Make the codebase more maintainable
務必要仔細確認計劃的內容,以上述的內容大部分都沒有問題,那麼我們就可以先讓 Claude Code 進行第一次的修改。
細節修正
即使 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 引用被編譯進去就沒問題。
例外情況
另一個例外是 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() 的版本,那麼我們就能在非常短的時間內處理好依賴注入的重構。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- 連載介紹 - Clean Architecture in TypeScript
- 目標設定 - Clean Architecture in TypeScript
- Hono 框架 - Clean Architecture in TypeScript
- tsyringe 套件 - Clean Architecture in TypeScript
- 專案設定 - Clean Architecture in TypeScript
- 介面規劃 - Clean Architecture in TypeScript
- 架構規劃 - Clean Architecture in TypeScript
- 助手對話介面 - Clean Architecture in TypeScript
- 對話紀錄 API - Clean Architecture in TypeScript
- 對話紀錄 UseCase - Clean Architecture in TypeScript
- 整合大型語言模型 - Clean Architecture in TypeScript
- 對話 UseCase - Clean Architecture in TypeScript
- 購物車側欄 - Clean Architecture in TypeScript
- 側欄 Use Case - Clean Architecture in TypeScript
- 查詢商品 - Clean Architecture in TypeScript
- 更新購物車 - Clean Architecture in TypeScript
- 對話階段 - Clean Architecture in TypeScript
- 依賴注入 - Clean Architecture in TypeScript