Software Development2025 IT 鐵人 30 天挑戰
Effect 服務管理(五)
用 Effect.Service 簡化服務定義
Effect.Service 是把「tag + 預設實作 + 對應的 Layer」合在一起的語法糖。
很適合應用程式層級的服務(有合理預設實作)。
為什麼要用 Effect.Service?
- 它把你原本要分三步做的事「一次宣告」:
- 建立 Tag(服務型別的識別)
- 定義預設實作(如何建立這個服務)
- 產生對應 Layer(自動處理依賴、可直接提供)
- 好處:少樣板碼、依賴清楚、測試友善(有含依賴與不含依賴兩種 Layer)。
比較 Effect 兩種服務寫法:Context.Tag + Layer vs Effect.Service(循序漸進)
目標情境
- 實作一個快取服務 Cache.lookup(key):讀取檔案內容;若檔案不存在則讓 Effect 失敗(不建立檔案);最後將結果輸出到 stdout。
- 使用到的依賴:FileSystem、Path(均來自 @effect/platform),在 Node 環境使用 NodeFileSystem。
一、寫法 A:Context.Tag + Layer(先定義 API,Layer 中接上依賴)
步驟 1:定義服務契約(先定 API 形狀與錯誤型別)
- 在宣告層就決定 Effect 的成功/錯誤型別,因此需要明確標註錯誤(此處選 PlatformError)。
- 優點:契約邊界清晰、與實作解耦。
- 代價:要自己決定錯誤型別並 import。
class Cache extends Context.Tag("Cache")<Cache, {
readonly lookup: (key: string) => Effect.Effect<string, PlatformError>
}>() {}為何需要手動標註 PlatformError?
- 因為在 Context.Tag 的「契約宣告階段」你就必須決定錯誤型別。由於 lookup 的實作會呼叫 FileSystem API,其錯誤即為 PlatformError,所以契約中選擇 PlatformError 最吻合實際行為。
步驟 2:提供實作(在層內取得依賴並實作 lookup)
- 依賴 FileSystem、Path;建立 cache 目錄;讀不到檔案就讓錯誤往上拋出。
// 建立快取服務的實作
const cacheLive = Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const path = yield* Path.Path
const cacheDir = path.join("src", "day24", "cache")
const lookup = (key: string) => fs.readFileString(path.join(cacheDir, key))
return { lookup }
})步驟 3:組裝依賴(顯式依賴組裝)
- 用 Layer.effect 建立服務層,逐一 provide 其外部依賴。
// 將實作包成 Layer,並提供 Node 檔案系統與 Path 依賴
const CacheLayer = Layer.effect(Cache, cacheLive).pipe(
Layer.provide(NodeFileSystem.layer),
Layer.provide(Path.layer)
)步驟 4:使用與執行
- 取出 Cache,呼叫 lookup,再輸出到 stdout;最後 provide 層並執行。
// 主程式:取得快取服務 → 讀取資料 → 確保結尾換行 → 輸出到 stdout
const program = Effect.gen(function*() {
const cache = yield* Cache
const data = yield* cache.lookup("my-key")
const line = data.endsWith("\n") ? data : `${data}\n`
process.stdout.write(line)
}).pipe(Effect.catchAllCause((cause) => Console.log(cause)))
const runnable = program.pipe(Effect.provide(CacheLayer))
// 執行程式
Effect.runFork(runnable)適用場合
- 需要明確的依賴組裝、自由替換實作、或設計多層降級(fallback)鏈時,顯式 Layer 會更直覺(例如在 day23 展示的降級機制)。
二、寫法 B:Effect.Service(整合式服務模組,型別由實作推斷)
步驟 1:定義服務 + 實作 + 依賴(在同一處)
- 服務的介面由 effect 區塊的實作「自動推斷」。
- dependencies 欄位直接聲明外部依賴,框架會幫你產生 Default / DefaultWithoutDependencies。
// 以 Effect.Service 定義快取服務,並在 effect 區塊中提供實作
class Cache extends Effect.Service<Cache>()("Cache", {
effect: Effect.gen(function*() {
// 取得檔案系統與路徑服務
const fs = yield* FileSystem.FileSystem
const path = yield* Path.Path
// 快取目錄位置(專案內的 src/day24/cache)
const cacheDir = path.join("src", "day24", "cache")
// 確保目錄存在(必要時遞迴建立)
const lookup = (key: string) => fs.readFileString(path.join(cacheDir, key))
return { lookup }
}),
// 宣告此服務啟動時所需的外部依賴 Layer
dependencies: [NodeFileSystem.layer, Path.layer]
}) {}步驟 2:使用與執行(更少樣板)
- 直接 provide Cache.Default,錯誤由實作推斷為 PlatformError,無須顯式 import。
const program = Effect.gen(function*() {
const cache = yield* Cache
const data = yield* cache.lookup("my-key")
const line = data.endsWith("\n") ? data : `${data}\n`
process.stdout.write(line)
}).pipe(Effect.catchAllCause((cause) => Console.log(cause)))
const runnable = program.pipe(Effect.provide(Cache.Default))
Effect.runFork(runnable)這裡為何不需要手動標註 PlatformError 呢?
- 因為 lookup 的錯誤型別會從 FileSystem API 的型別自動推斷出來(就是 PlatformError),不需你在服務宣告時手動寫出。
適用場合
- 中小型服務、一般應用、團隊想要統一與簡潔的服務模組寫法時,DX 極佳。
三、錯誤型別的來源與界線
- FileSystem、Path 等平台能力在 @effect/platform 下,其 Effect 的錯誤型別為 PlatformError。
- Context.Tag 寫法:契約層要先決定錯誤型別,所以你會 import 並標註 PlatformError。
- Effect.Service 寫法:從 effect 實作內呼叫 FileSystem/Path,TypeScript 自動推斷出 lookup 的錯誤即 PlatformError。
四、Effect.Service 的測試方法
以下兩種策略都適用,請依需要選擇:
- 注入測試依賴(Injecting Test Dependencies): 替換 FileSystem
const FileSystemTest = FileSystem.layerNoop({
readFileString: () => Effect.succeed("File Content...")
})
const TestLayer = Cache.DefaultWithoutDependencies.pipe(
Layer.provide(FileSystemTest),
Layer.provide(Path.layer)
)
const runnable = program.pipe(
Effect.provide(TestLayer)
)
Effect.runFork(runnable)- 直接 Mock 服務(覆蓋 Cache)
// 建立假的 Cache
const cache = new Cache({
lookup: () => Effect.succeed("Cache Content...")
})
const runnable = program.pipe(Effect.provideService(Cache, cache))
Effect.runFork(runnable)五、延伸:多實作 / 降級(fallback)何者更合適?
- Context.Tag + Layer 更擅長做顯式依賴組合與降級串接(失敗時以 Layer.catchAll/Layer.orElse 切換至下一層)。像 day23 的示例可組出 Primary → ReadOnly → Degraded 的清晰路徑。
- 用 Effect.Service 也能做降級,但通常是靠在不同地方提供/覆蓋服務來達成;當 fallback 鏈變長時,順序會分散在多個位置,閱讀起來不如用 Layer 在一條管線上明確串好來得直觀。
六、優缺點對照
Effect.Service
- 優點:
- 定義、實作、依賴聚合;樣板碼少、結構一致。
- 自帶 Cache.Default / DefaultWithoutDependencies,提供/測試都方便。
- 錯誤型別由實作推斷,無需手動 import/標註。
- 缺點:
- Layer 圖較隱性;複雜的依賴組裝、跨服務覆蓋時不如手寫 Layer 直觀。
- 超大型服務可能想拆解定義/實作位置以維持邊界。
Context.Tag + Layer
- 優點:
- 契約(API/錯誤)在宣告層就清楚,實作可替換性強。
- 依賴組裝清楚、降級/多實作切換更直覺。
- 缺點:
- 樣板碼偏多;需要自律維持一致結構與命名。
- 需自己決定並標註錯誤型別(如 PlatformError)。
七、總結
- 預設:選 Effect.Service(簡潔、DX 佳、結構一致)。
- 若需要顯式依賴組裝/清楚的降級與多實作切換/嚴格鎖定服務 API 與錯誤邊界:選 Context.Tag + Layer。
- 若實作必須由外部決定與替換:選 Context.Tag(在 Layer 組合處明確指定要用的實作與降級順序,切換點集中且一目了然)。
- 使用 Effect.Service 時:類別本身即為 Tag;要切換實作可用
Effect.provideService(MyService, mock)。
參考資料
Last updated on