Slide 1

Slide 1 text

高階関数を用いたI/O方法の公開 - DIコンテナから高階関数への更改 - 2025年6月14日 関数型まつり2025 株式会社コドモン 松浦康介

Slide 2

Slide 2 text

2 経歴 2025年1月にコドモンに入社しました。札幌在住です。 以前はSIerや事業会社に在籍していました。 元オブジェクト指向ピーポー、現ゆるふわ関数ピーポーです。 自己紹介 松浦 康介 まつうら こうすけ 2025.01 コドモン入社 2025.06 祝・入社6ヶ月 🎉

Slide 3

Slide 3 text

3 すべての先生に 子どもと向き合う 時間と心のゆとりを こんなプロダクトを開発しています メインプロダクトは、保育・教育施設向けWebアプリケーション。 保護者と施設のやり取りを支えるモバイルアプリケーションや、施設職員向けモバイル版 アプリケーション、外部サービスと連携するAPIなども開発しています。

Slide 4

Slide 4 text

4 Mission

Slide 5

Slide 5 text

5 今日お話ししたいこと ビギナー向け DIやオブジェクト志向 →関数型デビュー

Slide 6

Slide 6 text

6 ● これ配信あるのかな? ● エンジニアにとっての「まつり」とは?🔥 ● みなさんAIは使ってますか? ● 中野の街について ● 今日の資料について ● 今日は雨ですね、これが梅雨? ● まわりの人をみて、「みんなすごいんだろうな」ってビクビクしてたりし ませんか? ● 2 気楽にどうぞ - 今週末のまつりで一番ゆるい発表です -

Slide 7

Slide 7 text

7 私と同じような背景を持つ人、そこそこ多いのでは?と思っています。 関数型エンジョイ勢を増やしたい(純粋な人が多い社内に副作用を与えたい) 上級者のみなさまも同じ目線で観ていただけるとありがたいです! ● 私は昔、Javaに親しみがあった ● オブジェクト指向に馴染みがあった ● SpringのDIを便利に使っていた ● その後、関数型言語に興味を持った ● クラスもない、DIもない、ここで暮らす術がない。となった。 今日お話ししたいこと

Slide 8

Slide 8 text

8 ● DI「Dependency Injection(依存性注入)」の略 ● Martin Fowlerが2004年に概念を整理して普及 ● オブジェクトが依存するものを自分で作成せず、外部から注入してもらう ● 依存関係制御を逆転させる( Inversion of Control: IoC) ● 疎結合な設計を実現する DIとは

Slide 9

Slide 9 text

よくある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

Slide 10

Slide 10 text

これは…あっ! class FindUserUseCase { // インタフェースではなく、実装クラスを直接利用している private UserRepository repository = new MySqlUserRepository(); } // コンストラクタなどで指定できない var useCase = new FindUserUseCase() // DIできない

Slide 11

Slide 11 text

Infrastructure Application Domain FindUserUseCase UserEntity MySqlUserRepository UserRepository 外側の実装クラスに 直接依存している Constructor Injection 内側のインタフェースに依 存している オニオン a.k.a 大きな玉ねぎ

Slide 12

Slide 12 text

12 ● DI(依存性注入)を自動化するための仕組み ● 主な機能 ○ オブジェクトライフサイクル管理 ○ 依存関係自動解決・注入 ○ 設定ベースで依存関係定義 DIライブラリとは

Slide 13

Slide 13 text

DIライブラリの例 // Springの例 @Component class FindUserUseCase { @Autowired private UserRepository repository; // DIライブラリが自動注入 }

Slide 14

Slide 14 text

14 ● DIは「考え方・パターン」 ● DIライブラリは「それを実現する道具」 ● DIライブラリを使うことで便利になることも多い DIとDIライブラリの違い

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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の都合

Slide 18

Slide 18 text

18 設計にはトレードオフが伴うもの。便利さが必要なシーンも有る。 ↓これは私の考え ・ドメイン層からサードパーティのライブラリへの依存は少なくしたい。 ・ドメイン知識以外は含めたくない。 ※DIライブラリへの批判の意味はありませんのでご理解ください。 それぞれの正義でOKなのでは

Slide 19

Slide 19 text

19 JavaScript(TypeScript) では高階関数が利用できる。 これでDI(≠DIライブラリ)を実現することができる! ここからはその手段の一例をご紹介します。 関数型言語は、DIライブラリ不要かも

Slide 20

Slide 20 text

20 ちょっと休憩

Slide 21

Slide 21 text

21 エンジニアキャリアをスタートした頃のこと。 「あの関数、もっとリファクタリングできたよな」 「あの関数、もっと責務分離できたよな」 こんなことを思ったこと、ありますよね? 昔を思い出してみてください

Slide 22

Slide 22 text

22 後悔関数

Slide 23

Slide 23 text

23 ではなく

Slide 24

Slide 24 text

24 高階関数

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

26 応用編 - DIするなら高階関数 - 「ごあいさつ」する // 型定義 type Greet = (name: string) => Promise // シンプルな実装 const greet: Greet = async (name) => { return `こんにちは、${name}さん` } greet(‘中野’) // こんにちは、中野さん 型定義 名前を受け取り、挨拶を返す 実装 名前を受け取り、 「こんにちは、{名前}さん」を返す

Slide 27

Slide 27 text

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を利用して挨拶を生成

Slide 28

Slide 28 text

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に委ねることで、関心の分離ができる 高階関数にすることで、依存関係が明示的になる

Slide 29

Slide 29 text

29 関数のみでアプリケーションを作る

Slide 30

Slide 30 text

classを使わずに関数のみで小さなアプリケーションを作る Infrastructure Application Domain FindUserUseCase UserEntity MySqlUserRepository UserRepository 1. UserRepositoryのInterfaceを作る 3. MySqlUserRepositoryを作る 2. FindUserUseCaseを作る 4. 依存を解決する?

Slide 31

Slide 31 text

interface UserRepository { getUserById:(id: UserId) => Promise; } 型の定義 1. UserRepositoryのInterfaceを作る interfaceのみなので、実装はない 便宜上、getUserByIdのみ

Slide 32

Slide 32 text

// 1. 実際のUseCase type FindUserUseCase = (id: UserId) => Promise; // 2. UserRepositoryをDIするために、高階関数とする type FindUserUseCaseFactory = (repository: UserRepository) => FindUserUseCase 型の定義 1. 実際のUseCaseを定義する 2. UserRepositoryを利用したいので、それをDIするための ファクトリー関数を用意する 2. FindUserUseCaseを作る

Slide 33

Slide 33 text

// MySQLへの接続のため、 MySqlClientを引数に取り、UserRepository型を返す type MySqlUserRepositoryFactory = (client: MySqlClient) => UserRepository; 型の定義 1. UserRepositoryのinterfaceを実装する 2. MySQLへ接続するために、MySqlClientを利用したいの で、それをDIするためのファクトリー関数を用意する 3. MySqlUserRepositoryを作る

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

35 型に適合する関数を実装する 2/2 export const createFindUserByIdUseCase: FindUserByIdUseCaseFactory = (userRepository) => { return async (id: UserId): Promise => { // DIされたRepositoryを利用する return userRepository.findUserById(id); }; }

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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)

Slide 38

Slide 38 text

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は何をするもの?

Slide 39

Slide 39 text

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の実行

Slide 40

Slide 40 text

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な ど、実際の環境が必要なケースも ある

Slide 41

Slide 41 text

41 なぜclassではなく、関数なのか? ・関数はデコレーション可能 ・関数は合成可能(Result型などを用いる。今回は時間の都合で対象外 😢)

Slide 42

Slide 42 text

42 Cache Decoration 元の関数を変更すること無く、動作を追加で きる。 →これをclassでやろうとすると、対象のメ ソッドを変更する必要がある。 // キャッシュ機能を追加する関数 function withCache Promise>(fn: T): T { const cache = new Map(); 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);

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

44 ・優劣の話ではない。 ・みんなプログラミング組のクラスメイト。おともだち。 ・オブジェクト指向でも、関数型でも、同じようなことはできる。 ・ただし、オブジェクト指向に慣れていると、関数型にギャップはありそう ・(私と周りの人の実体験ベース) クラス vs 関数?

Slide 45

Slide 45 text

45 まとめ ● DIは面白い ● 高階関数でComposition Rootを代用できる、DIライブラリは不要 ● 型の考え方など、オブジェクト指向で学んできたことも活かすことができる ● ゆるく オブジェクト指向→関数型 へ周りの人を誘いたい ● 一緒にComposition Rootをつくろう

Slide 46

Slide 46 text

46 コドモン採用ページ コドモンでは一緒に関数を作りたい仲間を募集しています! 開発チームX

Slide 47

Slide 47 text

ありがとうございました! 渾身の懇親をしましょう!