Upgrade to Pro — share decks privately, control downloads, hide ads and more …

高階関数を用いたI/O方法の公開 - DIコンテナから高階関数への更改 - / Functio...

Avatar for momoiroshikibu momoiroshikibu
June 16, 2025
230

高階関数を用いたI/O方法の公開 - DIコンテナから高階関数への更改 - / Functional Onion Factory in Nakano

関数型まつり2025 1日目

2025-06/14
17:00 ~ 17:25
Track C

CfP
https://fortee.jp/2025fp-matsuri/proposal/350e2f70-0b02-4b79-b9f6-254a9d614706

Avatar for momoiroshikibu

momoiroshikibu

June 16, 2025
Tweet

Transcript

  1. 6 • これ配信あるのかな? • エンジニアにとっての「まつり」とは?🔥 • みなさんAIは使ってますか? • 中野の街について •

    今日の資料について • 今日は雨ですね、これが梅雨? • まわりの人をみて、「みんなすごいんだろうな」ってビクビクしてたりし ませんか? • 2 気楽にどうぞ - 今週末のまつりで一番ゆるい発表です -
  2. よくあるDI ~Constructor Injection ~ class FindUserUseCase { private UserRepository repository;

    // コンストラクタ注入 // UserRepositoryインタフェースを持つものは何でもInjectできる public UserService(UserRepository repository) { this.repository = repository; } } // UserRepositoryインタフェースを実装したクラスをコンストラクタに渡す var myUseCase = new FindUserUseCase(new MySqlUserRepository()) // DI var pgUseCase = new FindUserUseCase(new PostgreSqlUserRepository()) // DI
  3. これは…あっ! class FindUserUseCase { // インタフェースではなく、実装クラスを直接利用している private UserRepository repository =

    new MySqlUserRepository(); } // コンストラクタなどで指定できない var useCase = new FindUserUseCase() // DIできない
  4. Infrastructure Application Domain FindUserUseCase UserEntity MySqlUserRepository UserRepository DIライブラリとオニオン Composition Root

    (DI Library) 依存関係解決 各層の境界でインターフェースと実装を 結びつける DI済みアプリケーション ・アプリケーションは interfaceのみでは動作しない ・UseCase + Infra層まで整うことで、動作する状態となる →Composition Rootが本来やるべき依存関係解決は DIライブラリが担ってくれる UseCase Infra + DI =
  5. Infrastructure Application Domain FindUserUseCase UserEntity MySqlUserRepository UserRepository DIライブラリとオニオン Composition Root

    (DI Library) 依存関係解決 各層の境界でインターフェースと実装を 結びつける DI済みアプリケーション UseCase Infra + DI = DIライブラリの守備範囲 Springだと、アノテーションベースで自動的に DIが行われ、依存関係解決される @AutoWired @Component @Service など
  6. Infrastructure Application Domain FindUserUseCase UserEntity MySqlUserRepository UserRepository DIライブラリとオニオン Composition Root

    (DI Library) 依存関係解決 各層の境界でインターフェースと実装を 結びつける DI済みアプリケーション UseCase Infra + DI = DIライブラリの守備範囲 Springだと、アノテーションベースで自動的に DIが行われ、依存関係解決される @AutoWired @Component @Service など おや?ドメイン層にDIライブラリのアレコレが入り込むのは良いのだろうか?? ・アノテーションもDIライブラリのパッケージに属している ・アノテーションを付与しているのは Composition Rootの都合
  7. 25 2. 関数を返す関数 高階関数とは? 1. 関数を引数として受け取る関数 // Array methods [1,

    2, 3].map(x => x * 2) [1, 2, 3].filter(x => x > 1) // Event handlers button.on('click', () => { console.log('clicked') }); function createMultiplier(factor) { return function(x) { return x * factor; }; } const double = createMultiplier(2); double(5); // 10 const triple = createMultiplier(3); triple(5); // 15
  8. 26 応用編 - DIするなら高階関数 - 「ごあいさつ」する // 型定義 type Greet

    = (name: string) => Promise<string> // シンプルな実装 const greet: Greet = async (name) => { return `こんにちは、${name}さん` } greet(‘中野’) // こんにちは、中野さん 型定義 名前を受け取り、挨拶を返す 実装 名前を受け取り、 「こんにちは、{名前}さん」を返す
  9. 27 応用編 - DIするなら高階関数 - 「気の利いた挨拶」をしたい → Geminiを利用する // Geminiのクライアントが必要

    type GeminiGreetFactory = (client: GeminiClient) => Greet Greet関数を返す関数 const createGeminiGreet: GeminiGreetFactory = (client) => { // Greet型の関数を返す return async (name: string): Greet => { const prompt = `${name}さんへの気の利いた挨拶よろ`; const response = await client.generateText(prompt); return response.text }; }; const geminiGreet = createGeminiGreet(getSomeGeminiClient); geminiGreet("中野") // 中野さん、梅雨の季節ですが体調はいかがですか 外側の関数 ・GeminiClientを受け取る ・内側からアクセス可能に 内側の関数 ・名前を受け取る ・GeminiClientを利用して挨拶を生成
  10. 28 const geminiGreet: Greet = async (name) => { const

    client getGeminiClient(); // 関数内で作れば良い const prompt = `${name}さんへの気の利いた挨拶よろ `; const response = await client.generateText(prompt); return response.text }; 応用編 - DIするなら高階関数 - 高階関数の必要ある?Greetのままで良いのでは? GeminiClientを関数内で作ると 環境変数への依存が隠蔽されてしまい、「この関数を使うには何が必要か」が分かりにくくなる テスト時にクライアントをモックするのがやや困難に GeminiClientの生成責任をComposition Rootに委ねることで、関心の分離ができる 高階関数にすることで、依存関係が明示的になる
  11. interface UserRepository { getUserById:(id: UserId) => Promise<User | null>; }

    型の定義 1. UserRepositoryのInterfaceを作る interfaceのみなので、実装はない 便宜上、getUserByIdのみ
  12. // 1. 実際のUseCase type FindUserUseCase = (id: UserId) => Promise<User

    | null>; // 2. UserRepositoryをDIするために、高階関数とする type FindUserUseCaseFactory = (repository: UserRepository) => FindUserUseCase 型の定義 1. 実際のUseCaseを定義する 2. UserRepositoryを利用したいので、それをDIするための ファクトリー関数を用意する 2. FindUserUseCaseを作る
  13. // MySQLへの接続のため、 MySqlClientを引数に取り、UserRepository型を返す type MySqlUserRepositoryFactory = (client: MySqlClient) => UserRepository;

    型の定義 1. UserRepositoryのinterfaceを実装する 2. MySQLへ接続するために、MySqlClientを利用したいの で、それをDIするためのファクトリー関数を用意する 3. MySqlUserRepositoryを作る
  14. 34 型に適合する関数を実装する 1/2 // MySqlClientをDIできるように type MySqlGetUserById = (client: MySqlClient)

    => UserRepository[“getUserById”] // 実際の関数 export const mySqlGetUserById: MySqlGetUserById = (client) => { return async (id: UserId): Promise<User> => { // DIされたMySqlClientを利用する return client.query('SELECT * FROM users WHERE id = ?', [id]); } }
  15. 35 型に適合する関数を実装する 2/2 export const createFindUserByIdUseCase: FindUserByIdUseCaseFactory = (userRepository) =>

    { return async (id: UserId): Promise<User> => { // DIされたRepositoryを利用する return userRepository.findUserById(id); }; }
  16. Infrastructure Application Domain FindUserUseCase UserEntity MySqlUserRepository UserRepository 再掲: Composition Rootで依存関係解決

    Composition Root (DI Library) 依存関係解決 各層の境界でインターフェースと実装を 結びつける DI済みアプリケーション ・アプリケーションは interfaceのみでは動作しない ・UseCase + Infra層まで整うことで、動作する状態となる →Composition Rootが本来やるべき依存関係解決は DIライブラリが担ってくれる UseCase Infra + DI =
  17. Infrastructure Application FindUserUseCase Factory MySqlUserRepository Composition Rootで依存性を解決する MySqlUserRepository Factory MySQLClient

    FindUserUseCase Repository Composition Root MySqlUserRepository 1. Infrastructure層から Repositoryを取得する 2. MySQLUserRepositoryを FindUserUseCaseFactoryに渡し、 FindUserUseCaseを得る main.ts app Composition Rootから DIされたアプリケーショ ンを取得し、 Webアプリ ケーションとして利用す る(など) FindUserUseCase serve(app)
  18. 38 // Infrastructure層の初期化 const createMySqlRepositories: () => Repositories = ()

    => { const mySqlClient = createMySqlClient(/* 接続設定は環境変数から */); const userRepository = createMySqlUserRepository(mySqlClient); // MySqlClientをDI return { userRepository } } // Application層の初期化 const composeApplication = (repositories: Repositories) => { const findUserUseCase = createFindUserUseCase( // FactoryでRepositoryをDI repositories.userRepository ); return { findUserUseCase } } Composition Rootは何をするもの?
  19. 39 // main.ts // 環境により、composeInMemoryRepositoriesなどとしても良い // Infrastructure const repositories =

    createMySqlRepositories(); // Application(UseCase) const app= composeApplication(repositories); // Web API router.get('/users/:id', async (req, res) => { const userId = req.params.id; // DI済みのfindUserUseCaseを利用する const user = await app.findUserUseCase(userId); return res.json(user); }); DI済みのアプリケーションを利用する 実行可能なアプリケーションの依存 関係解決と初期化 (DI済みの関数群) Web APIの例: アプリケーションに登録された DI済 みのUseCaseの実行
  20. 40 ・UseCaseのロジックのテストはほ ぼモックでテスト可能 テストコード // DIコンテナに依存しない // テスト対象の関数にモックを渡し、正しく実行されるかを確認する test('should return

    user when found', async () => { const mockUser = { id: '123', name: 'John Doe' }; const repository = { findUserById: mock.fn(async () => mockUser) }; const useCase = createFindUserByIdUseCase(repository); const result = await useCase('123'); const calls = repository.findUserById.mock.calls; assert.deepStrictEqual(result, mockUser); assert.strictEqual(calls.length, 1); assert.strictEqual(calls[0].arguments[0], '123'); }); ・Infrastructure層のテストはDBな ど、実際の環境が必要なケースも ある
  21. 42 Cache Decoration 元の関数を変更すること無く、動作を追加で きる。 →これをclassでやろうとすると、対象のメ ソッドを変更する必要がある。 // キャッシュ機能を追加する関数 function

    withCache<T extends (...args: any[]) => Promise<any>>(fn: T): T { const cache = new Map<string, any>(); return (async (...args: any[]) => { const key = JSON.stringify(args); if (cache.has(key)) { return cache.get(key); } const result = await fn(...args); cache.set(key, result); return result; }) as T; } // 1. 必ずMySQLから取得する関数 const getUserById = mySqlGetUserById; // 2. キャッシュ機能付きの関数 const getUserByIdWithCache = withCache(mySqlGetUserById);
  22. 43 Class Decoration? // ❌ 元クラスを変更が必要 class UserRepository { @cache

    // デコレーターを追加 getUserById() { ... } } // ❌ 継承で複雑化 class CachedUserRepository extends UserRepository { getUserById() { // キャッシュロジック + super.getUserById() } } 元のクラスを 変更する必要がある 元のクラスを 変更する必要がある (言語次第で対応方法はありそう )