
tsyringe 套件 - Clean Architecture in TypeScript
tsyringe 是微軟維護的一款輕量依賴注入(DI,Dependency Injection)容器,可以幫助我們很輕鬆的管理物件的依賴注入,在 Clean Architecture 的實踐中我們會將許多物件細分職責,此時有依賴注入的機制可以幫助我們在測試、開發上更加順手。
簡潔
在發現 tsyringe 前,我大多使用 InversifyJS 來處理依賴注入,然而 InversifyJS 的前置準備並不算容易,改用 tsyringe 後只需要很簡單的加上 @injectable()
就可以解決大多數的應用情境。
撰寫本文時 InversifyJS 的設計也變得相對簡單不少,如果需要更複雜的功能可以考慮使用 InversifyJS
舉例來說,我們有一個 KvConversationRepository
用來儲存使用者的對話,可以像下面這樣進行定義。
1@injectable()
2class KvConversationRepository {
3 constructor(@inject(KvStore) private readonly kv: KVNamespace) {}
4
5 // ...
6}
當我們需要使用時,則只需要如下的實作。
1const repository = container.resolve(KvConversationRepository)
這是一個非常常見的依賴注入實作方式,tsyringe 可以幫我們自動找到 KvStore
並且完成 KvConversationRepository
的實例化。
可測試性
使用依賴注入可以很好的提高專案的可測試性,尤其是現在大多數語言模型收費仍然不便宜,也沒有一個很好的方法可以控制輸出。
舉例來說,我們有一個會呼叫語言模型的情境。
1class Chat {
2 constructor(private readonly model: LlmModel) {}
3
4 public async execute(message: string): Promise<string> {
5 // ...
6 const res = await this.model.chat(message)
7 return res.text
8 }
9}
雖然我們設計了依賴注入機制,但會需要在每個測試都重新建立假的測試物件,使用依賴注入套件或者框架,就可以透過覆蓋原本註冊的物件來達到我們的目的。
1container.register(
2 LlmModel,
3 {
4 useFactory: (c) => { return new MockLlmModel() }
5 }
6)
透過這個技巧,我們就不用在每個測試案例重新產生 MockLLmModel
物件,而是自動的在需要時產生新的物件。
Serverless 友善
如果是在非 Serverless 環境,我們仍能透過像是加入 setup.vitest.ts
這類方式預先定義好依賴,但是 Serverless 每一次請求都是獨立的,這能很好的提高拓展性,卻也讓我們無法共用預先定義的依賴。
以 Cloudflare 為例子
1export default {
2 async fetch(request, env, ctx) {
3 // KVNamespace => env.KV
4 return new Response('Hello World!');
5 },
6
7};
如果我們要拿到 env.KV
這個 KVNamespace
的 Binding(綁定)就需要等到我們接受到請求,這樣就不能在 export default
前先將 Repository 這類跟資料庫相關的物件產生出來,必須每個請求都處理。
受限於這樣的特性,我們就很難再設定測試環境時完成定義,也沒辦法讓所有請求共用相同的實例,那麼在測試跟開發階段都會受到很大的限制。
然而,我們可以利用 tsyringe 的 useFactory
特性,動態的處理這個行為。
1container.register(
2 KvConversationRepository,
3 {
4 useFactory: (c) => {
5 const kv = c.resolve(KVNamespace)
6 return new KvConversationRepository(kv)
7 }
8 }
9)
10
11export default {
12 async fetch(request, env, ctx) {
13 container.register(KVNamespace, { useValue: env.KV })
14
15 // ...
16
17 return routes.handle(request, env, ctx)
18 },
19
20};
依照上述的設計,KvConversationRepository
並不會一開始就被初始化,而是在被使用到時才呼叫,那麼就可以在 KVNamespace
還不存在之前被定義(@injectable()
也有類似的效果)
這樣我們就可以在開發跟測試中獲得更多設計上的彈性,而且每次都會產生新物件實例的特性也一定程度上可以避免副作用(Side Effect)的發生,即使我們在設計上就需要避免。
Cloudflare 在最近的更新已經允許用
import { env } from "cloudflare:workers";
來直接取用env
而不需要透過fetch()
這類 Handler 來獲取。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- 連載介紹 - Clean Architecture in TypeScript
- 目標設定 - Clean Architecture in TypeScript
- Hono 框架 - Clean Architecture in TypeScript
- tsyringe 套件 - Clean Architecture in TypeScript