不知道你有沒有聽過 SOLID 原則,其中的 D
也就是依賴反轉的實作的其中一種就是這次要介紹的依賴注入 dependency injection (之後都簡稱 DI) ,使用 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 ,你需要兩樣東西
為什麼需要唯一值? 在 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')
}),
)
如果你去看 effect
的 type 會發現它變成了 Effect<void, never, Logger>
,這時候有趣的事情發生了,如果你將這個 effect 傳入 Effect.runPromise
你會發現它出現錯誤訊息了
這是因為 Effect 用 TypeScript 的 type 抓出來了這個 effect 缺少了必要的 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,
)
這樣就能跑了,也不會有 type error
可能是 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
)
另外如果再加上 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 同時還有管理相依性的功能,我們下一篇會再來詳細的介紹