TypeScriptプロジェクトにスキーマ駆動開発を持ち込み、より型安全な世界へ
by
SansanTech
Link
Embed
Share
Beginning
This slide
Copy link URL
Copy link URL
Copy iframe embed code
Copy iframe embed code
Copy javascript embed code
Copy javascript embed code
Share
Tweet
Share
Tweet
Slide 1
Slide 1 text
TypeScriptプロジェクトに スキーマ駆動開発を持ち込み、 より型安全な世界へ Sansan株式会社 Digitization部 Bill One Entryグループ 秋⼭ 雅之 / @aki202
Slide 2
Slide 2 text
今⽇の内容 https://buildersbox.corp- sansan.com/entry/2023/08/ 14/182118 2
Slide 3
Slide 3 text
2021年中途⼊社。 請求書のデータ化システムを⽇々改善 & 運⽤。 { "📛aliases": ["@aki202", "リーダブル秋⼭"], "❤likes": ["プログラミングパラダイム", "アーキテクチャ"], "🧑💻roles": ["バックエンドエンジニア", "フロントエンドエンジニア"] } 秋⼭ 雅之 Sansan株式会社 Digitization部 Bill One Entryグループ 3
Slide 4
Slide 4 text
1. はじめに 1. スキーマ駆動開発とは 2. Testing Trophyとの関係性 2. 構成 1. 最初の構成と課題 2. 最終的な構成 3. バックエンド 1. スキーマから型ファイルを作る 2. APIハンドラに型を与える 3. バリデーターを追加する 4. huskyでスキーマ変更を検知する 4. フロントエンド 1. スキーマからAPIクライアントを作る 5. パターンマッチングを持ち込む 6. おわりに ⽬次 4 © Sansan, Inc.
Slide 5
Slide 5 text
1. はじめに
Slide 6
Slide 6 text
“事前に定義した単⼀のインタフェースを元に、 プロダクトを構成する各サービスを開発する⼿法” スキーマ駆動開発とは 図:ヒトクセ『開発効率UPを実現する「スキーマ駆動開発」』より 6
Slide 7
Slide 7 text
(1) ⾃動化:スキーマからドキュメント・実装コード・テストを部分的に ⾃動⽣成できる。 (2) 依存制約の解消:例えば従来はバックエンドチームが担当するWebAPIの 完成を待たなければ、フロントエンドチームが開発を始められないという 制約があった。スキーマさえ定義されていれば、この制約を取り払える ようになる。 (3) 堅牢性の担保:スキーマを各サービスが共有することで、データ構造や プロトコルの⼀貫性が⾼まり、サービスの堅牢性を担保できます。 何が嬉しいのか 7
Slide 8
Slide 8 text
従来のTesting Pyramidに代わ り提唱されているモダンフロ ントエンドに おけるテストガイドライン。 右図の上から順番に、 - End to End:E2Eテスト - Integration:結合テスト - Unit:単体テスト - Static:Lintや型チェック 下のレイヤーほど低コスト。 Testing Trophyとは? 図:“Testing Javascript” より 図:Google Cloud “Continuous testing” より 8
Slide 9
Slide 9 text
スキーマ駆動開発が寄与するのは ⼀番下のStaticレイヤー。 最もコストの⼩さなレイヤーなので、 ここのStaticレイヤーをどれだけ厚く できるかどうかが良いテストの指標の ⼀つ。 Testing Trophyとの関係性 9 図:“Testing Javascript” より
Slide 10
Slide 10 text
2. 構成
Slide 11
Slide 11 text
最初の構成 Frontend - APIクライアントのパラメータは openapi.yamlを⽬視で⾒て⼿動設定 Backend - スキーマはOpenAPI 3.0で記述 - APIハンドラはany型でデータを受信 - データのバリデーションはAPIハン ドラやその先の処理で個別に実⾏ 11
Slide 12
Slide 12 text
課題1: 信頼境界の不在 クライアント APIハンドラ アプリケーション サービス ドメイン サービス ⼀連のリクエスト・レスポンスのフロ ーは直列の関数実⾏と⾒做せる。 “ここ以降の処理に渡るデータは安全” と⾔えるボーダーが信頼境界。 信頼境界がない場合、各関数で引数が 予期されたデータかチェックする必要 がある。→ 重複やヌケモレのリスク があるアンチパターン エンティティ ルーティング 引数チェック 引数チェック 引数チェック 引数チェック 信頼境界が無い状態 12
Slide 13
Slide 13 text
スキーマ定義からTypeScriptの型を作っておらず、次の状態だった。 Backend - 受信するデータがany型 - レスポンスデータがany型 Frontend - クライアントから送信するデータがany型 - クライアントが受け取るデータがany型 何が問題か - 例えばAPIリクエストやレスポンスのenumの列挙⼦を⼀つ変更した場合、 影響範囲を全て⽬視チェックする必要がある 課題2: 静的解析の範囲が⼩さい 13
Slide 14
Slide 14 text
最終的な構成 Frontend - スキーマから型の付いたクライアントを 作成 Backend - スキーマから型定義ファイルを作成 - APIハンドラの引数とレスポンスに型制 約を追加 - バリデーターを挟み信頼境界を設定 14
Slide 15
Slide 15 text
3. バックエンド
Slide 16
Slide 16 text
スキーマから型ファイルを作る - swagger-typescript-api を利⽤ - 豊富なオプション - 明朗なアウトプット swagger-typescript-api の変換例(acacode/swagger-typescript-api より) 16
Slide 17
Slide 17 text
APIハンドラに型を与える - リクエストとレスポンスに型を付与 - ただしこの時点で引数はany型で⼊ってくる ↑ ExpressのAPIハンドラに型を与える ← スキーマから⽣成した型ファイル 17
Slide 18
Slide 18 text
- ルーティングにはbeforeの構成から引き続き swagger-routes-express を採⽤しているが、 リクエストに対するバリデーションは⾏ってくれ ず、リクエストパラメータはany型でAPIハンド ラに渡される。 - スキーマを元にバリデーションを⾏ってくれる express-openapi-validator を新たに導⼊した。 - これによりOpenAPI 3.0で記述できる範囲での 信頼境界を設定できた。 - つまりリクエストがスキーマ通りのリクエ ストであるかどうかを、APIハンドラは 気にしなくて良い状態 バリデーターを追加する 18
Slide 19
Slide 19 text
- 次の状態になった。 - (1) リクエストに対してバリデーションを早い段階で掛けることで、 信頼境界を設定できた。 - (2) APIハンドラを型安全な関数に変えることができた。 - これらは型ファイルが最新の状態であることを前提にしている。 ➛ 型ファイルか現在のスキーマと⼀致しているかどうかを検知する必要がある。 - huskyを⽤いて、“現在の型ファイル” と “スキーマから⼀時的に作った型ファイ ル” のハッシュが⼀致しているかをgit push前に検証する。 huskyでスキーマ変更を検知する 19
Slide 20
Slide 20 text
4. フロントエンド
Slide 21
Slide 21 text
スキーマからAPIクライアントを作る - APIクライアントの⽣成にも、 バックエンドでスキーマから型定義の⽣成に使ったswagger-typescript-api利⽤ - 右図のようにリクエストを発⾏できる。 - リクエストデータは型安全。 - レスポンスデータも型安全。 21
Slide 22
Slide 22 text
スキーマのパス - beforeの構成と同様、スキーマはbackendレポジトリに置いている。 - APIクライアントの⽣成時、frontendレポジトリから参照するスキーマの パスは、 backendレポジトリにあるスキーマを相対パスで指定している。 - これは⼀⾒雑な⽅法で、開発者PCのレポジトリの配置⽅法に依存するこ とになるが、シンボリックリンクを使うという⽅法もあるので、特に問 題にはなっていない。 - featureブランチをmergeする際に衝突が発⽣するときもあるが、これは どのような⽅法を取っても同様に抱える⼩さな問題。 22
Slide 23
Slide 23 text
5. パターンマッチングを持ち込む
Slide 24
Slide 24 text
- 当初の⽬的だったスキーマ駆動開発の導⼊は終わった。 - バックエンドとフロントエンドが単⼀のスキーマを参照し、型制約の恩 恵が受けられている状態。 ここまでで 本当に型制約の恩恵は⼗分か? 24
Slide 25
Slide 25 text
分岐処理の例 - 次のようなケースは? 25
Slide 26
Slide 26 text
分岐処理のヌケモレ - 型定義にも当然 “guest” が追加されるが、 コードベースの分岐処理がそれに対応できているかは⽬視による確認が 必要。 26
Slide 27
Slide 27 text
switch⽂も同様 - LintルールやExhaustiveness checkingというテクニックで網 羅チェックが可能 - でもそれぞれ、表現⼒が弱い、 冗⻑なコードが必要という弱点 がある 27
Slide 28
Slide 28 text
ts-patternによる網羅的分岐処理 1. 簡潔なコードで網羅的分岐処理が可能 2. guestが追加されることを静的に検知できる 3. stringのunionだけではなく、複数のunionの組み合わせの分岐処理、条件 式から値をキャプチャするなどといった複雑な要件にも対応できる。 28
Slide 29
Slide 29 text
ts-patternあれこれ ↑クラスの種類で分岐処理 ↑2つにUnionの組み合わせで分岐処理 ←詳しくは https://zenn.dev/aki202/articles/5d725c080640f9 29
Slide 30
Slide 30 text
6. おわりに
Slide 31
Slide 31 text
- 次の施策を通して、プロジェクト全体に型制約を与え、より安全な開発をで きるようになりました。 - APIリクエストとレスポンスに型制約を与えた - リクエストに対し早期にバリデーションをかけ信頼境界を作った - パターンマッチで網羅的な分岐処理を静的に保証できるようになった - システムは⽣まれた瞬間から複雑化します。複雑化の中で堅牢性を担保する のはこの仕事の最も⾯⽩い所の⼀つだと思いますし、⾃分もやっていて楽し いです。 Digitization部はSansanの主要なプロダクトのプラットフォームに位置する 重要部署です。是⾮カジュアル⾯談しましょう。気軽にご連絡ください。 まとめ 31
Slide 32
Slide 32 text
No content