スキーマ駆動開発を支える開発プロセス&ツール群
前回の記事
前回の記事でレセプト機能を 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 シナリオテストの導入
- バージョン管理・リリースフローの整備
全てを1つの記事にまとめるとスペース的に厳しいので別記事としてまとめています。
具体的な取り組み内容の 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 ケースを超えています。今後も、データ駆動のパターンを中心に、ケースが増える予定です。
1つのシナリオで請求データを出力するまでに 7 つぐらいの API を呼び出す感じになるのですが、runn の runbook の include 機能によるシナリオ再利用ができており、コード量もそこまで増えていません。
課題だった開発段階からテストですが、E2E でのシナリオテストが実現ができています。具体的には、QA チームがテスト設計したデータパターンを事前に共有してもらい、それをシナリオテストに組み込んで、事前検証を行うようにしました。
API シナリオテストを導入しての気づきになりますが
- Controller テストでは発見できなかった API の仕様バグを発見できる
- ユースケース単位でシナリオを書くことで API の挙動がドキュメントとしてまとまる
- API の冪等性担保のテストが簡単にできる
- コミット毎にリグレッションテストがされる安心感は非常に大きい
- deploy 後の動作検証もコマンド1つで完了できて楽
があります。フロントエンドも含めた E2E テストは難しい [3] けれど、まずは API シナリオテストからやるべきでは?と感じています。
あと runn ならではの所で
- API テストだけでは確認できない DB の状態チェックができる
- 非同期処理を行う API の後続処理も Exec Runner を使えばカバーできる
- ゴールデンテストを作りやすい
というメリットがあり、大変便利に使わせて頂いています。
期待以上の効果を実感しておりプロジェクトになくてはならないツールとなりました。
こちらの要望に対してより良いアイデアの提案やコア機能の改善をしていただいた @k1LoW さんにはこの場を借りて感謝をお伝えしたいと思います。
バージョン管理・リリースフローの整備
取り組みの理由
runn の開発がきっかけで tagpr を知りました。その際、tagpr を使用したバージョン管理を体験し、それがプロジェクトにマッチすると感じて導入しました。
API 仕様書が日々変化するなかで、他チームとの共通認識できるバージョン付けを運用負荷あげずに行いたかったというのが目的になります。
実践してみた結果
運用負荷をかけずに開発段階から細かくバージョン管理できる様になりました。
また以下の効果もありました。
- 他チームとの共有が Release note の URL を教えるだけで良くなった
- Release 作業をチームの垣根を超えて任せられるようになった
特に後者は色々な側面がありますが
関係者全員に最低限のコミュニケーションでバージョンを共有できる
というのが大きいかと考えています。
開発あるあるですが、プロジェクト全体を把握している人がリリース担当になってしまいがち問題を解消できたのが良かったです。
リリース業を行なっていた時はリリースする内容を調整するために、頭のリソースの一部を専有されていた感がありました。それが導入後には次にリリースする内容が明確になり、各メンバーが自立的に調整する流れができ、リリースに関わる心理的な束縛から開放された感じを覚えました。
リリース作業の民主化
がされたといっていいのでしょうか?
簡単に導入できるので、是非試してみて頂きたいです。
スキーマ駆動開発ではリリースサイクルを短くする為に、細かくバージョン管理を行うのが良いと考えています。その際に安心してリリースを行うのに tagpr は大変良く機能すると感じました。
スキーマ駆動開発の先にドメイン駆動開発がある
スキーマ駆動開発でドメインの関心事の境界となるインターフェースを高品質に定義できます。
今回のプロジェクトではレセプト業務のコア機能を BaaS として切り出し切り出すことを行なっていますが、切り出ししたドメインを集中して開発できる様にすることが重要になります。
スキーマ駆動開発とドメイン駆動開発を組み合わせることで、複雑なドメインであってもよりドメインに集中して開発を行なっていけるメリットがあると考えています。
ここまでスキーマ駆動開発における取り組みを紹介しましたが、ドメインに向き合って法改正に迅速に対応する為に行なっている内容を軽く触れたいと思います。
ドメインモデルを蒸留させる
レセプトの複雑な問題をドメインに凝縮させる様に日々試行錯誤しています。
リアーキテクティングに際して以下のようなことを行なっています。
- 請求の関心事に特化した軽量フレームワークを構築
複雑な請求業務ですが、大きな処理の流れは同じになります。
支援サービス固有のロジックと、共通部分を見定めてうまくドメインに切り出しして設計を行いました。関心事を関連するドメインに閉じ込めることで、全ての業務ルールを理解してなくても開発できるようになります。
複雑な業務ルールを抽象化してモデリングを丁寧にして、フレームワークとして使えることを目指しました。 - 算定構造を型で表現する
以前は算定構造にある算定項目はデータベースのテーブルのカラムとして表現されていました。各種実績データを算定構造に当てはめて計算を行うのですが、条件を検索する処理が手続きとして実装されていました。
算定構造表
を扱うのに実装が手続き処理が多く、実装とモデルとの間に差がうまれてしまっていました。1つの条件を見直すのに処理を追わないと修正ができません。
今回は算定構造を明確なドメインとして設計して型を組み合わせて表現するようにしました。
型の組み合わせで請求パターンを表現できるようになり、以前は一部の請求パターンのみしかテスト出来なかったものを型の組み合わせを自動生成して網羅的にテストを行えるようになりました。
型を組み合わせて表を作るだけでよくなり、関心事が集約されました。 - 手続き処理をなるべく排除してルールベースで機能するようにする
@t-konta さんが記事でまとめてくれています。
法改正対応時に手数を減らす意味で、なるべく手続きをなくしルールベースで処理がされるようにします。手続きがない分テストの範囲も限られれます。
またデータのプロパティも数多く存在するので、データとルールがセットで見れるとレビューも大変しやすいし、コードを追いやすいです。
PHP8.1 からの Attribute はドメインにルールを埋め込むのがしやすく大変良いと感じています。
最後に
最後までお読み頂いてありがとうございます!
色々な取り組みをしていますが、その背景を知って貰えたら嬉しく思います。
どの取り組みも課題を解決する為の1つのアイデアですが、背景を再整理してより良いアイデアが生まれたらと思い記事にしました。
リアーキテクティングはまだ道途中ですが、今後も開発プロセスをいい感じにしていき複雑なドメインに向き合っていきたいと思います。
Discussion