💡

カリー化と分割代入で作る、TypeScriptの関数型DDDとDIアプローチ

2024/10/05に公開

はじめに

この記事では、TypeScriptを使ったカリー化と分割代入を活用したDDDとDIの実装方法を紹介します。
この手法を使うことで関数型プログラミングにおける依存性注入がシンプルになり、コードの再利用性やメンテナンス性が向上するのではないかと思います。

前提

DDDでアプリケーションを実装する場合いくつか重要なポイントがありますが、特徴的なのはDIによってドメイン層とインフラ層の依存関係が逆転していることではないでしょうか。

TypeScriptの有名なDIライブラリとしてはTSyringe等がありますが、これらはオブジェクト指向に基づいています。
近年では、TypeScriptでクラスを使用しない関数型プログラミングのアプローチが増えてきており、関数型を採用したプロジェクトでは、こうしたDIライブラリの採用が難しく感じることがあります。

現状の関数型DI

関数型でDIを行う場合、引数として依存性を毎回渡す方法があります。
この方法は関数呼び出しのたびに依存性を注入する必要があるため、同じ処理を繰り返し記述する必要が出てきてしまい、コードが少し煩雑に感じることがあります。

function preProc(x: number): number {
  return x++
}

function x2(fn: (x: number) => number, x: number): number {
  return fn(x) * 2
}

function main() {
  console.log(x2(preProc, 1)) // -> 4
  console.log(x2(preProc, 2)) // -> 6
}

以下のようにカリー化を使うことで緩和できますが、関数が複数になるとやはり冗長に感じてしまいます。

const preProc = (x: number): number => {
  return x++
}

const x2 = (fn: (x: number) => number) => (x: number): number => {
  return fn(x) * 2
}

const x3 = (fn: (x: number) => number) => (x: number): number => {
  return fn(x) * 3
}

function main() {
  // 関数毎に初期化処理が必要
  const fx2 = x2(preProc)
  const fx3 = x3(preProc)

  console.log(fx2(1)) // -> 4
  console.log(fx2(2)) // -> 6
  console.log(fx3(1)) // -> 6
}

分割代入の応用

分割代入を利用することで関数をオブジェクトでまとめて返し、呼び出し側で個別に取り出すことができます。
これにより初期化処理を簡略化し、同じ関数で複数回初期化する必要がなくなるため、コードの冗長さを減らすことが可能です。
また、一時変数を作成する必要がなくなり、よりシンプルなコードが実現できます。

const preProc = (x: number): number => {
  return x++
}

const functions = (fn: (x: number) => number) => ({
  x2: (x: number): number => fn(x) * 2,
  x3: (x: number): number => fn(x) * 3,
})

function main() {
  // 分割代入
  const { x2, x3 } = functions(preProc)

  console.log(x2(1)) // -> 4
  console.log(x2(2)) // -> 6
  console.log(x3(1)) // -> 6
}

実装例

このセクションでは、カリー化と分割代入を使ってDIを実現する例を紹介します。
Next.jsのアプリケーションにおいて、環境変数を使ってRDBMSかNoSQLのどちらかのDBに接続し、ユーザー情報を取得する処理を実装します。

ディレクトリ構造

以下はsrcディレクトリ以下に配置されたディレクトリ・ファイル構成です。

$ tree src
src
├── app
│   ├── layout.tsx
│   └── page.tsx
├── dependencyResolvers.ts
├── domain
│   ├── entities
│   │   └── user.ts
│   ├── repositories
│   │   └── userRepository.ts
│   └── services
│       └── userService.ts
└── infra
    └── repositories
        ├── noSqlUserRepository.ts
        └── rdbmsUserRepository.ts

実装詳細

Repository定義

ユーザーデータを管理するUserRepositoryは以下のようにインターフェースを要素に持つ型としてドメイン層で定義します。

domain/repositories/userRepository.ts

import {User} from '@/domain/entities/user';

export type UserRepository = {
  get: (id: number) => Promise<User>;
}

Repository実装

ドメイン層で定義されたUserRepositoryをインフラ層で以下のように実装します。

infra/repositories/noSqlUserRepository.ts

import {UserRepository} from '@/domain/repositories/userRepository';

export const noSqlUserRepository: UserRepository = {
  get: async (id: number) => {
    return {
      id: id,
      name: 'data from NoSQL',
    }
  }
}

infra/repositories/rdbmsUserRepository.ts

import {UserRepository} from '@/domain/repositories/userRepository';

export const rdbmsUserRepository: UserRepository = {
  get: async (id: number) => {
    return {
      id: id,
      name: 'data from RDBMS',
    }
  }
}

Service実装

Repositoryを使用するServiceはカリー化しつつ、分割代入できるように実装します。

domain/services/userService.ts

import {User} from '@/domain/entities/user';
import {UserRepository} from '@/domain/repositories/userRepository';

const getUserInfo = (userRepository: UserRepository) => async (id: number): Promise<User> => {
  return userRepository.get(id)
}

export const userService = (userRepository: UserRepository) => ({
  getUserInfo: getUserInfo(userRepository),
})

依存解決処理実装

環境変数に応じてRepositoryを切り替える処理を実装します。

dependencyResolvers.ts

import {noSqlUserRepository} from '@/infra/repositories/noSqlUserRepository';
import {rdbmsUserRepository} from '@/infra/repositories/rdbmsUserRepository';
import {UserRepository} from '@/domain/repositories/userRepository';

export const getUserRepository = (): UserRepository => {
  return process.env.NODE_ENV === 'production'
    ? noSqlUserRepository
    : rdbmsUserRepository;
}

pageで使用

Next.jsのpage.tsxから呼び出してユーザー情報を取得します。

app/page.tsx

import {userService} from '@/domain/services/userService';
import {getUserRepository} from '@/dependencyResolvers';

export default async function Home() {
  const { getUserInfo } = userService(getUserRepository())

  const user = await getUserInfo(1)

  return (
    <div>
      {JSON.stringify(user)}
    </div>
  )
}

動作結果

以下のように起動の仕方を変えるだけで、インフラ層の実装が切り替わっていることを確認できます。

developmentモード

developmentモードで起動すると、getUserRepositoryrdbmsUserRepositoryを返します。

$ npm run dev
  ▲ Next.js 14.2.14
  - Local:        http://localhost:3000

 ✓ Starting...

productionモード

productionモードで起動すると、getUserRepositorynoSqlUserRepositoryを返します。

$ npm run build && npm run start
   Creating an optimized production build ...

  ▲ Next.js 14.2.14
  - Local:        http://localhost:3000

 ✓ Starting...

Discussion