今天我們會進一步介紹如何做元件層級的依賴注入。
程式碼請參考 D26/effect-context
我們以 UsersField 這個元件為例,他有幾個地方會觸發事件
不論是哪一種,都會和元件當前 狀態
互動,而且通常都會造成 副作用
,例如 addUser 背後其實會呼叫 getUser
,等到執行完畢還會做 setUsersField
。我們雖然討厭副作用,但完全把它變不見式不可能的,所以我們只能把它 延遲處理
。藉由把它們當作參數傳入負責資料處理的函式中,就能確保這些函式至少在被呼叫前是純淨的,而純淨函式則代表著容易閱讀、容易維護與容易測試。
這邊可參考 D12 - 設計依賴注入 與 D13 - 實作依賴注入
xport interface Constants {
settings: Settings
texts: Texts
xport interface Functions {
clock: Clock
contracts: Contracts // < Functions 新增了 Contracts
xport interface Service extends Constants, Functions {}
``
/ 屬於 UsersField
xport interface UsersFieldService {
getUser: GetUser
field: UsersField
setField: SetAtom<UsersField>
``
把這些共通狀態抽出來以後,我們就可以利用 Effect context 來做一個引子
export const UsersFieldService = {
context: Context.Tag<UsersFieldService>(),
}
還記得 Effect<R, E, A> 三本柱嗎 ? Effect context 就用來提供裡面的 R
(the parameter R represents the contextual data required by the effect to be executed)
const onAddUser = (
_: AddUserEvent
): Effect.Effect<UsersFieldService, never, UsersField> => {
return pipe(
UsersFieldService.context,
// 取得 UsersFieldService,從裡面解構出 getUser 函式和 field
// 最後產生執行 getUser 產生另一個 effect
Effect.flatMap(({ getUser, field }) => getUser(field.input)),
// 由於 updateError 和 addUser 都會產生 effect
// 所以使用 matchEffect 來把它們合併後再攤平
Effect.matchEffect({
onFailure: updateError,
onSuccess: addUser,
})
)
}
最終看到整個函式的輸出型別為 Effect.Effect<UsersFieldService, never, UsersField>
,表示他可以藉由 effect 型別盒子來和其他 effect 做 flatMap 或是 map 等等運算。
此外由於排除了 getUser
這個副作用函式,在我們進行測試時就不用再啟用 mockserver,只需要變更傳入的 UsersFieldService
即可。
//src\app\edit\courses\all\data\states\add-course-form\users-field\index.spec.ts
it('should return invalid user field with error when get user failed', async () => {
//arrange
const event = AddUserEvent.self
const fakeGetUserError = UnexpectedRequestError.of({
error: '',
method: 'GET',
url: '',
})
const service = UsersFieldService.of({
getUser: () => Effect.fail(fakeGetUserError),
field: InvalidUsersField.ofInput('richard_01'),
setField: () => {},
})
//act
const result = await pipe(
UsersField.on(event),
Effect.provideService(UsersFieldService.context, service),
Effect.runPromise
)
//assert
expect(result._tag).toBe('InvalidUsersField')
expect(result.error._tag).toBe('Some')
})
以上程式另一個重點是 Effect.provideService(context, instance)
只要使用它就可以對含有 R
的 Effect
完成依賴注入。
有時候我們會遇到某一個依賴需要另外一個依賴的情形,明天會介紹這個時候 effect 可以怎麼幫助我們管理這些複雜的依賴關係。