スキーマ駆動開発を支える開発プロセス&ツール群
前回の記事
前回の記事でレセプト機能をBaaS(Backend as a Service)化し、リアーキテクティング(再構築)を行う判断をした背景とゴール設定から大きな方針決定まで流れをお話しました。
後半は
- 採用する開発プロセス
- 開発プロセスを支えるツールや取り組み
についてお話したいと思います。
スキーマ駆動開発とは?
スキーマ駆動開発(Schema-Driven Development、SDD)とは、データの構造や型、項目の名前や概要を事前に定義し、それをもとにアプリケーションやAPIなどのインターフェースを設計する方法です。スキーマを事前に決めることで、データとアプリケーションの間に明確な契約ができ、互いに理解しやすくなります。
前回の記事で紹介しましたが、今回のプロジェクトではOpenAPI V3でAPI仕様書をまとめる方針としています。
最初に各種データのスキーマを設計し、それを公開することで、BaaS本体と、それを利用する各プロダクトで並行開発を行うことを目的としています。
スキーマの複雑さ
前回の記事にまとめていますが、おさらいで今回のプロジェクトのスキーマの複雑さをまとめます。
- API数が100オーバー
- リクエスト・レスポンスBodyのJsonの要素数が50〜100程度。大きいものだと200ぐらい
- 共通項目が結構ある
RESTful APIで機能提供するBaaSの開発についての話となります。
スキーマ駆動開発を支える開発プロセス&ツール群
上述の複雑さなのでAPIスキーマを定義し、仕様書通りに実装していくのはかなり大変です。又、BaaSなのでQAチームがそのままではテストができません。あと複数チームと連携して並行開発する形になるので開発フロー等の整備が必要だと考えました。
実際に取り込みを行なった内容は以下の通りです。
- 実装と乖離させないスキーマ駆動開発フローの整備
- APIシナリオテストの導入
- バージョン管理・リリースフローの整備
全てを一つの記事にまとめるとスペース的に厳しいので別記事としてまとめています。
具体的な取り組み内容のHowについてはそちらを参照頂くことにして今回の記事では取り組みの理由(Why)と実践してみてどうだったか?について補足させて頂きます。
実装と乖離させないスキーマ駆動開発フローの整備
取り組みの理由
APIの仕様が複雑化することは予想されていました。また、法改正時に多くのAPIを一度に修正する必要があるため、そのレビューが大変になると考えていました。
OpenAPIは、jsonやyamlで記述することができます。また、GitHub上でレビューすることも可能ですが、肥大化したファイルをレビューするのは大変です。レビューミスへの懸念がありました。OpenAPIのSpecをファイル分割する方法もありますが、色々と面倒なことが多いです。
OpenAPIの経験のないメンバーが多いため、生のOpenAPI仕様書を書くことは困難であると考え、プロジェクト開始時に、OpenAPIに関するツールの 調査 を行いました。色々な記事を調べながら
スキーマを単純なドキュメントとして扱うのではなく、開発プロセスとして組み込む必要がある
と感じました。
ドキュメントは作成した瞬間から陳腐化していきます。仕様と実装が乖離していかないようにするには開発プロセスが需要だと思いました。紹介した記事は試行錯誤しながら改善していった開発フローになります。
実践してみた結果
スキーマに沿ってValidationやRoutingが自動生成される様になってかなり楽になりました。
- コードの記述量が減った
- ControllerテストのValidationテストが不要になった
- テストの実行時にリクエストとレスポンスがスキーマ通りになっていることが保証される
APIのI/Fの品質担保がされることで、ロジックに集中できるようになりました。
今回のプロジェクトの話から外れますが、社内の他のプロジェクトでAPI仕様書を書く文化がなかったチームに、開発フローを布教しました。
その結果、API仕様書を書くようになり、開発フローが受け入れられてフローの有用性が再認識できました。
あと個人的にはOSSへも貢献 [1] もできて良かったです。プロジェクトではEnumを多様するので記述量を減らすことに繋がりました。
又、PHP8.1でのAttributesの仕組みを理解する良い機会でした。
APIシナリオテストの導入
取り組みの理由
開発するシステムがBaaSで提供する機能はAPIのみとなり、テストをどの様に行なっていくか?が課題でした。
BaaSの連携ができた各プロダクトからテストを行えますが、並行開発するので連携が完了するまでテストができません。開発中の段階からテストが実施できるようにAPIテストを実現しようと考えていました。
今回のプロジェクトではOpenAPIを使うことを決めていました。そのため、親和性の高いツールを探していました。APIは請求データを管理するためのコードを発行し、そのコードを次のAPIに渡して呼び出すスタイルになっています。単独のAPIのテストではなく、シナリオを書けるツールが必要でした。その時にAPIシナリオテストのツールであるrunnの記事を見つけました。yamlベースでシナリオを直感的に書けそうと感じ、コレだ!と確信して直ぐに試してみました。
最初は、runnの不具合でテストがうまくいかなかったのですが、想像通りシナリオの書き味が良かったので、可能性を感じました。
runnはgolangで開発されていますが、私たちがプラットフォームの一部のサービスでgolangで開発していたこともあり、試しにコードを見てみました。直ぐに不具合の箇所が特定でき、修正できそうだったので、Pull Requestを送りました。すぐに取り込んで頂き、無事に使えるようになりました。
その時はrunnの開発が初期段階で機能やコード量もまだ少なかったので、いくつかの提案やPull Requestを送りました。その後にcollaboratorに招待され、templateによるデータ生成やデータ駆動テストの仕組みを議論しながら、runnに組み込んで頂きました。
json読み込み(templateによるjson生成)とデータ駆動テストの仕組みは、リクエストとレスポンスが複雑なのでどうしても欲しかった機能でした。
請求パターンの組み合わせが膨大にあり [2]、シナリオとは別にテストデータを分けて管理し、繰り返しテストを行いたいと考えていました。
実践してみた結果
Controllerテストとの比較になりますが、多数のリクエストパラメータや大きなレスポンスを扱う場合に手続きでテストを実装するのに比べrunnの方が圧倒的に楽に書けます。
runnのシナリオテストでは、データの準備から含めてシナリオとして記述することができるため、非常に効率的に記述することができます。
既にプロジェクトメンバー全員がrunnを使ってシナリオを書くようになっており、APIを実装したら必ずシナリオテストを書くサイクルになっています。
現在、シナリオの数とデータ駆動のテストパターンを合わせると、100ケースを超えています。今後も、データ駆動のパターンを中心に、ケースが増える予定です。
一つのシナリオで請求データを出力するまでに7つぐらいのAPIを呼び出す感じになるのですが、runnのrunbookのinclude機能によるシナリオ再利用ができており、コード量もそこまで増えていません。
課題だった開発段階からテストですが、E2Eでのシナリオテストが実現ができています。具体的には、QAチームがテスト設計したデータパターンを事前に共有してもらい、それをシナリオテストに組み込んで、事前検証を行うようにしました。
APIシナリオテストを導入しての気づきになりますが
- Controllerテストでは発見できなかったAPIの仕様バグを発見できる
- ユースケース単位でシナリオを書くことでAPIの挙動がドキュメントとしてまとまる
- APIの冪等性担保のテストが簡単にできる
- コミット毎にリグレッションテストがされる安心感は非常に大きい
- deploy後の動作検証もコマンド一つで完了できて楽
があります。フロントエンドも含めたE2Eテストは難しい [3] けれど、まずはAPIシナリオテストからやるべきでは?と感じています。
あとrunnならではの所で
- APIテストだけでは確認できないDBの状態チェックができる
- 非同期処理を行うAPIの後続処理もExec Runnerを使えばカバーできる
- ゴールデンテストを作りやすい
というメリットがあり、大変便利に使わせて頂いています。
期待以上の効果を実感しておりプロジェクトになくてはならないツールとなりました。
こちらの要望に対してより良いアイデアの提案やコア機能の改善をしていただいた @k1LoW さんにはこの場を借りて感謝をお伝えしたいと思います。
バージョン管理・リリースフローの整備
取り組みの理由
runnの開発がきっかけでtagprを知りました。その際、tagprを使用したバージョン管理を体験し、それがプロジェクトにマッチすると感じて導入しました。
API仕様書が日々変化するなかで、他チームとの共通認識できるバージョン付けを運用負荷あげずに行いたかったというのが目的になります。
実践してみた結果
運用負荷をかけずに開発段階から細かくバージョン管理できる様になりました。
また以下の効果もありました。
- 他チームとの共有がRelease noteのURLを教えるだけで良くなった
- Release作業をチームの垣根を超えて任せられるようになった
特に後者は色々な側面がありますが
関係者全員に最低限のコミュニケーションでバージョンを共有できる
というのが大きいかと考えています。
開発あるあるですが、プロジェクト全体を把握している人がリリース担当になってしまいがち問題を解消できたのが良かったです。
リリース業を行なっていた時はリリースする内容を調整するために、頭のリソースの一部を専有されていた感がありました。それが導入後には次にリリースする内容が明確になり、各メンバーが自立的に調整する流れができ、リリースに関わる心理的な束縛から開放された感じを覚えました。
リリース作業の民主化
がされたといっていいのでしょうか?
簡単に導入できるので、是非試してみて頂きたいです。
スキーマ駆動開発ではリリースサイクルを短くする為に、細かくバージョン管理を行うのが良いと考えています。その際に安心してリリースを行うのにtagprは大変良く機能すると感じました。
スキーマ駆動開発の先にドメイン駆動開発がある
スキーマ駆動開発でドメインの関心事の境界となるインターフェースを高品質に定義することができます。
今回のプロジェクトではレセプト業務のコア機能をBaaSとして切り出し切り出すことを行なっていますが、切り出ししたドメインを集中して開発できる様にすることが重要になります。
スキーマ駆動開発とドメイン駆動開発を組み合わせることで、複雑なドメインであってもよりドメインに集中して開発を行なっていけるメリットがあると考えています。
ここまでスキーマ駆動開発における取り組みを紹介しましたが、ドメインに向き合って法改正に迅速に対応する為に行なっている内容を軽く触れたいと思います。
ドメインモデルを蒸留させる
レセプトの複雑な問題をドメインに凝縮させる様に日々試行錯誤しています。
リアーキテクティングに際して以下のようなことを行なっています。
- 請求の関心事に特化した軽量フレームワークを構築
複雑な請求業務ですが、大きな処理の流れは同じになります。
支援サービス固有のロジックと、共通部分を見定めてうまくドメインに切り出しして設計を行いました。関心事を関連するドメインに閉じ込めることで、全ての業務ルールを理解してなくても開発できるようになります。
複雑な業務ルールを抽象化してモデリングを丁寧にして、フレームワークとして使えることを目指しました。 - 算定構造を型で表現する
以前は算定構造にある算定項目はデータベースのテーブルのカラムとして表現されていました。各種実績データを算定構造に当てはめて計算を行うのですが、条件を検索する処理が手続きとして実装されていました。
算定構造表
を扱うのに実装が手続き処理が多く、実装とモデルとの間に差がうまれてしまっていました。一つの条件を見直すのに処理を追わないと修正ができません。
今回は算定構造を明確なドメインとして設計して型を組み合わせて表現するようにしました。
型の組み合わせで請求パターンを表現できるようになり、以前は一部の請求パターンのみしかテスト出来なかったものを型の組み合わせを自動生成して網羅的にテストを行えるようになりました。
型を組み合わせて表を作るだけでよくなり、関心事が集約されました。 - 手続き処理をなるべく排除してルールベースで機能するようにする
@t-konta さんが記事でまとめてくれています。
法改正対応時に手数を減らす意味で、なるべく手続きをなくしルールベースで処理がされるようにします。手続きがない分テストの範囲も限られれます。
またデータのプロパティも数多く存在するので、データとルールがセットで見れるとレビューも大変しやすいし、コードを追いやすいです。
PHP8.1からのAttributeはドメインにルールを埋め込むのがしやすく大変良いと感じています。
最後に
最後までお読み頂いてありがとうございます!
色々な取り組みをしていますが、その背景を知って貰えたら嬉しく思います。
どの取り組みも課題を解決する為の一つのアイデアですが、背景を再整理してより良いアイデアが生まれたらと思い記事にしました。
リアーキテクティングはまだ道途中ですが、今後も開発プロセスをいい感じにしていき複雑なドメインに向き合っていきたいと思います。
Discussion