まだクラスへの依存性注入で消耗してるの？TS製の関数型DIヘルパー「velona」のススメ

TypeScriptの普及に伴い、Node.jsでもDIを行うケースが増えています
TypeScriptでDIする場合クラスとデコレータを使うのが主流、というかほぼこれ一択です

一方でフロントを関数型で書くことが増え、バックエンドも関数だけで書きたいというニーズもあります

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">class使わなくても十分なのめっちゃわかります。<br>Dependency Injectionも実はクラスとかデコレーター無しに実現できるので、Nestなどのフレームワーク側も近い将来クラスやデコレーターべったりなの改善されるのかなと思ったり。<a href="https://t.co/ETbPuLrY3Q">https://t.co/ETbPuLrY3Q</a> <a href="https://t.co/sWaGt75YXU">https://t.co/sWaGt75YXU</a></p>&mdash; 名人さん | マナリンクCTO (@Meijin_garden) <a href="https://twitter.com/Meijin_garden/status/1314539326496108546?ref_src=twsrc%5Etfw">October 9, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<br>
そこでこの記事ではTypeScriptの関数だけでDIが出来る超軽量ライブラリ、[velona（ヴェローナ）](https://github.com/frouriojs/velona)を紹介します

先に特徴をまとめると

1. クラスもデコレータも不要、純粋な関数として呼び出せる
1. 関数にデフォルトの依存をあらかじめ注入するので既存コードの書き換えが少ない
1. 全体で15行、型情報を除くと7行程度だからオーバーヘッドが少ない

というTypeScriptの強みを活かせるライブラリです

## クラスもデコレータも不要、純粋な関数として呼び出せる

とりあえずインストール方法
TypeScript環境はすでにある想定です

```sh
$ npm install velona
```

次に、足し算をしてくれるchildFnに依存しているparentFnをDIなしで普通に定義してみます

```ts:parentFn.ts
const childFn = (a: number, b: number) => a + b

export const parentFn = (a: number, b: number, c: number) => childFn(a, b) * c // childFnに依存
```

2, 3, 4を渡して呼び出すと (2 + 3) * 4 という計算で20が表示されます

```ts:index.ts
import { parentFn } from './parentFn'

console.log(parentFn(2, 3, 4)) // 20
```

parentFnについて、「childFnの返り値を第3引数で掛け算しているかどうか」をテストしたくなったと仮定します
childFnの実装がテスト結果に影響を与えないようにDIをしたいパターンですね
velonaを使ってparentFnの「定義だけ」を書き換えます

```ts:parentFn.ts
import { depend } from 'velona'

const childFn = (a: number, b: number) => a + b

export const parentFn = depend(
  { childFn }, // childFnを注入する
  ({ childFn }, a: number, b: number, c: number) => childFn(a, b) * c // こっちのchildFnは注入「された」関数
)
```

クラスもデコレータも使わず注入できました
既存のプロダクションコードは書き換える必要がなく純粋な関数として呼び出せます

```ts:index.ts
import { parentFn } from './parentFn'

console.log(parentFn(2, 3, 4)) // 20
```

テストコードではchildFnを差し替えます
JestでもAVAでも好きなテストツールを使ってください

```ts:index.test.ts
import { parentFn } from './parentFn'

test('childFnの返り値を第3引数で掛け算しているかどうか', () => {
  const childReturnVal = 7
  const injectedParentFn = parentFn.inject({ childFn: (_a, _b) => childReturnVal }) // テスト用のchildFnを注入
  const thirdArgVal = 4

  expect(injectedParentFn(2, 3, thirdArgVal)).toBe(childReturnVal * thirdArgVal) // Pass
})
```

childFnは常にchildReturnVal、つまり7を返してくるので第3引数をいくつか用意してparentFnの挙動をテストできます
これこそがvelonaが実現した関数型DIです

## 関数にデフォルトの依存をあらかじめ注入するので既存コードの書き換えが少ない

上記で書いた呼び出し側のコードをもう一度見てみましょう

```ts:index.ts
import { parentFn } from './parentFn'

console.log(parentFn(2, 3, 4)) // 20
```

ここでchildFnの注入を行っていません
DI導入を決める前の既存部分100か所くらいでparentFnを呼んでいたとしても、DIのために書き換えるのは以下のparentFn定義1か所だけでいいのです

```ts:parentFn.ts
import { depend } from 'velona'

const childFn = (a: number, b: number) => a + b

export const parentFn = depend(
  { childFn }, // childFnを注入する
  ({ childFn }, a: number, b: number, c: number) => childFn(a, b) * c // こっちのchildFnは注入「された」関数
)
```

プロダクション用のデフォルト依存値を注入しておいて、テストするときだけ再度注入すれば良いという素晴らしい設計です

クラス＋デコレータでDIする場合、インスタンス化している箇所全てで依存性注入を記述する必要があります
テスト駆動開発をしていればこれも問題にはなりませんが、現実にはとりあえずリリース優先で実装を行ってある程度価値検証が進んでからテストを書くケースがよくあると思います

velonaを使えば途中からDIを導入する際のコストを最小化できるのです

## 全体で15行、型情報を除くと7行程度だからオーバーヘッドが少ない

TypeScriptでDIが出来るフレームワークと言えば[Nest.js](https://nestjs.com/)が有名ですが、ベンチマークを見るとExpressベースのnest、Fastifyベースのnest-fastifyともに大きくスコアを落としています

| Framework          | Version          |  Requests/sec |  Latency |
| :----------------- | :--------------- | ------------: | -------: |
| fastify            | 3.5.1	          | 38,018	      | 2.54     |
| nest-fastify       | 7.4.4            | 31,960        | 3.04     |
| express            | 4.17.1	          | 8,239        | 11.98     |
| nest               | 7.4.4            | 7,311         | 13.54    |

出典：https://github.com/frouriojs/benchmarks

velonaは以下の15行が全てです

```ts
type Deps<T extends Record<string, any>> = {
  [P in keyof T]: T[P] extends { _velona: boolean }
    ? (...args: Parameters<T[P]>) => ReturnType<T[P]>
    : T[P]
}

export const depend = <T extends Record<string, any>, U extends any[], V>(
  dependencies: T,
  cb: (deps: Deps<T>, ...args: U) => V
) => {
  const fn = (...args: U) => cb(dependencies, ...args)
  fn._velona = true
  fn.inject = (deps: Deps<T>) => (...args: U) => cb(deps, ...args)
  return fn
}
```

型情報を削るとたった7行程度で非常に軽量です
velonaを使えば既存の実行速度をほぼ犠牲にすることなくDIの恩恵を受けることができます

## ロゴがカワイイ

valonaは関数型サーバーフレームワーク「[frourio](https://qiita.com/m_mitsuhide/items/00b139bb565dddf8006a)」でDIを実現する方法を模索して生まれたスピンオフのライブラリです
なのでfrourioと同じカワイイロゴを使っています
![velona-github-ogp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63528/9a7c3317-1d95-b4d3-67a2-effd5f2fa001.png)
気に入ったらGitHubにスターを押してやって下さい
[GitHub - velona](https://github.com/frouriojs/velona)

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">TypeScript製の関数型DIライブラリ velona のREADMEを更新しました🎉<br><br>- DBアクセスを担う関数にORマッパー（Prisma）を依存性注入する例を追加<br>- ロゴデザインを frourio と統一<br><br>あらゆる関数でDIが可能になるのでぜひ試してみてください<a href="https://t.co/SC3dRYantR">https://t.co/SC3dRYantR</a></p>&mdash; Solufa (@m_mitsuhide) <a href="https://twitter.com/m_mitsuhide/status/1315410248199335936?ref_src=twsrc%5Etfw">October 11, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>