iT邦幫忙

2025 iThome 鐵人賽

DAY 11
0
Software Development

Effect 魔法:打造堅不可摧的應用程式系列 第 11

10. Effect 的 dependency injection:打造可抽換的模組

  • 分享至 

  • xImage
  •  

不知道你有沒有聽過 SOLID 原則,其中的 D 也就是依賴反轉的實作的其中一種就是這次要介紹的依賴注入 dependency injection (之後都簡稱 DI) ,使用 DI 可以提供不少的好處

  1. 確保程式的實作可以被動態的抽換,比如你可以很方便的更換 AI 的 model 而不用大改程式 (當然換了 model 後的效果不保證是了)
  2. 更容易測試,我們在之後的章節會來看 DI 怎麼幫助我們寫測試

在 Effect 裡要實作 DI 是很容易的,不如說就是原生支援,還記得一開始提過的 Effect 的 type 裡有三個參數嗎?

const effect: Effect.Effect<A, E, R>

這次終於要來碰到這個 R 了,R 代表的是這個 effect 執行時需要的 service ,例如

const effect: Effect.Effect<A, E, FooService>

代表的是這個 effect 執行時,需要提供給它 FooService ,總之我們先來實作一個 service 試試吧,要實作一個 service ,你需要兩樣東西

  1. 代表這個 service 的唯一值
  2. service 本身的實作

為什麼需要唯一值? 在 Effect 中,為了知道哪些 service 應該要用用哪個實作對應,其實 Effect 的內部維持了一個 Map

Map<service 的唯一值, service 的實作>

而 service 的唯一值在 Effect 中被稱為 Tag ,而實作被稱為 Service ,也就是實際上這個 type 會像是

Map<Tag, Service>

要提供唯一值,我們需要透過 Context.Tag 來提供

import { Context } from 'effect'

interface LoggerService {
  log: (...args: unknown[]) => void
}

class Logger extends Context.Tag('MyLogger')<Logger, LoggerService>() {}

而想要使用 Tag 取得 service 時可以像這樣

const effect = Effect.gen(function * () {
  // Tag 本身就是一個取得 service 的 effect
  const { log } = yield* Logger
  log('hi Logger')
})

// 也可以像這樣
const effect = pipe(
  Logger,
  Effect.map(({ log }) => {
    log('hi Logger')
  }),
)

(playground link)

如果你去看 effect 的 type 會發現它變成了 Effect<void, never, Logger> ,這時候有趣的事情發生了,如果你將這個 effect 傳入 Effect.runPromise 你會發現它出現錯誤訊息了
https://ithelp.ithome.com.tw/upload/images/20250925/20111802TtrN5J3DIa.png

這是因為 Effect 用 TypeScript 的 type 抓出來了這個 effect 缺少了必要的 service 而無法執行

提供實作的 service

如果要提供實作的 service ,有個簡單的方法 Effect.provideService ,老實說這個 function 我不常用,原因晚點會介紹,我們先來看怎麼使用

pipe(
  Effect.gen(function * () {
    const { log } = yield* Logger
    log('hi Logger')
  }),
  Effect.provideService(Logger, { log: (...args) => console.log(...args) }),
  Effect.runPromise,
)

(playground link)

這樣就能跑了,也不會有 type error

Effect.Service

可能是 Tag 跟 Service 常要分開來定義有點麻煩吧,官方其實有是供了一個方法可以一起定義與實作,那就是 Effect.Service

class Logger extends Effect.Service<Logger>()("MyLogger", {
  // 這邊可以提供預設的實作,另外也提供了幾種不同的版本,例如 sync, effect
  succeed: { log: (...args: unknown[]) => console.log(...args) }
}) {}

pipe(
  Effect.gen(function*() {
    const { log } = yield* Logger
    log("hi Logger")
  }),
  Effect.provideService(
    Logger,
    // 使用 make 提供不同的實作
    Logger.make({
      log: (...args) => console.log(...args)
    })
  ),
  Effect.runPromise
)

(playground link)

另外如果再加上 accessors: true ,就會再附贈可以在 Tag 上直接存取 service 實作的存取方法

class Logger extends Effect.Service<Logger>()("MyLogger", {
  accessors: true,
  succeed: { log: (...args: unknown[]) => console.log(...args) }
}) {}

Effect.gen(function*() {
  // 這樣就不會先取 logger 再呼叫了
  yield* Logger.log("hi Logger")
})

另外 Effect.Service 明明有提供預設的實作,那我們要怎麼直接用預設的版本呢?

pipe(
  Effect.gen(function*() {
    yield* Logger.log("hi Logger")
  }),
  // 像這樣就可以用預設的實作了,注意這邊用的是 `Effect.provide`
  Effect.provide(Logger.Default),
  Effect.runPromise
)

Effect.provide 提供的是 layer ,這是可以將 tag 與實作包裝起來的一個東西,是不是看起來方便多了呢?這也是為什麼我不常用 Effect.provideService 的原因。而且 layer 同時還有管理相依性的功能,我們下一篇會再來詳細的介紹


上一篇
9. Effect 的超級魔法:排程與錯誤重試
下一篇
11. Effect layer: 管理 dependency 的相依性
系列文
Effect 魔法:打造堅不可摧的應用程式12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言