内容
本記事は技術の話ではなく、設計の話というとこれまた大袈裟でして、個人的な考察くらいのものだと思ってください。
私が開発のリーダーを担当している案件で、自治体向けの業務支援ツールをSaaSとして開発しているものがあります。
本案件の開発を通じて悩んだことと、それに対してどうしたかについて簡単に書きます。
業務は同じでも自治体ごとに運用ルールが違う
行政の業務というと様々なものがありそうですが、本記事で例に出すのは住民の申請管理システムです。
申請にも色々ありますが、基本的には以下のフローを想定します。
- 役場から住民に向けて案内
- 住民が申請
- 役場が審査
- 申請受理後の業務(お金の振込や、何らかの書類の郵送など)
システムに求められる最重要要件は ステータス管理 と データ出力 です。
例えば給付金の申請システムであれば、役場の管理画面から振込データを出力することでスムーズに銀行振込を実施できて、すぐにステータスを確認することができます。
この要件さえ満たせればSaaSとして一度にたくさんの自治体に申請管理システムとして利用していただけるのではないか と思いましたが、現実はそう甘くはありませんでした。
実際に様々な自治体を受け入れる中で必要になった個別の要件の一部を紹介します。
1. システムで持つデータの種類が違う
氏名、住所、電話番号など、基本的な情報はある程度共通ですが、審査に必要な情報というのは自治体によって様々です。
2. 出力するデータのフォーマットや形式が違う
振込データや申請内容のデータをPDFやCSVなどで出力する必要がありますが、自治体によって形式が全然違います。
if文地獄
こういった違いを受け入れて対応できるシステムでなければ我々のシステムの存在意義がないので、基本的な枠組みを外れない限りは全ての要件を一旦受け入れます。
プロジェクトの最初の方は、ハードコーディング(if文)で自治体ごとに処理を分けてカスタマイズを実現していました。
しかし自治体が増えていくと、地獄でした。テストが大変すぎますし、コードが膨大に膨れ上がって保守性もない中、さらに新しい機能を追加していくのはかなりきつかったです。
データ定義層という概念を導入
こちらは、1. システムで持つデータの種類が違う に対する解決策です。
そんな概念が一般的にあるのかは知りませんし、あっても別の表現だと思いますが、私は本システムに データ定義層 (独自に命名)という概念を取り入れることにしました。
『層』と呼んでいますが、一般的なレイヤーアーキテクチャの定義とは異なり、本プロジェクトにおける概念的な区分けです。
| 層(通称) | 役割 | 具体的な処理内容 |
|---|---|---|
| 表現層 | 出力・描画 | 最終的なUI描画やPDF/CSV生成を行う |
| データ定義層 | 加工・定義 | 自治体ごとの設定に基づき、必要なデータの選定や名称の変換を行う |
| DB層 | データ保持 | 全自治体のデータを格納できる、柔軟なスキーマ設計で生データを保持する |
表にするまでもないでしょうが、こんな感じです。
本来はDB内のデータをアプリケーション側で取得してそのままフロントで出力しますが、
データ定義層では取得したデータを自治体に応じて、再定義します。
以下はデータ定義層でやっていることのイメージです。
- 「DBテーブルでカラムはあるけどこの自治体では住所はいらないから住所の情報は渡さないでおこう」
- 「氏名はこの自治体では「フルネーム」って扱うから「氏名」ではなく「フルネーム」で渡そう」
といった具合です。
メタデータ駆動というのがあるみたいですが、それに近いかもしれません。(そこまで大袈裟ではない。)
もちろん物理的なサーバー構成が分かれているわけではなく、論理的な層(ロジック上の責務の分離)であり、やってることは結局ロジックによる単なるデータの加工です。
でも、このデータを定義するための共通の処理を通すことで、if文地獄は随分と解消されました。
とにかく一元管理
データ定義層を取り入れる理由でもありますが、このシステムでは
「持つデータの種類の変更は日常茶飯事だ」
というのが大前提です。
そうなると例えば以下のように住民情報を処理してはいけません。
// テーブルのカラムをあらかじめ用意
const residentInfoColumns = [
'name',
'address',
'email',
'phoneNumber',
]
// for文で繰り返し処理
for (let column of residentInfoColumns) {
console.log(resident[column]);
}
最初に配列でカラムを管理しているのでここだけ見ればいいコードにも見えますが、
もしかしたらこれまで当然のように使っていたaddressが、今度の自治体では不要だと言われるかもしれません。
そのため、以下のようにします。
// 自治体名から、使用するデータのカラムを一括取得する関数(データ定義層の関数)
const residentInfoColumns = getTargetColumns(localGovernmentName)
// for文で繰り返し処理(ここは同じ)
for (let column of residentInfoColumns) {
console.log(resident[column]);
}
データ出力は独立した汎用システム化
最後に、2. 出力するデータのフォーマットや形式が違う に対する解決策ですが、こちらはもう頑張って汎用的なファイル出力システムを作る他ないかと思います。
例えば申請ごとにPDFの帳票を出力するにしても運用する自治体によってフォーマットが違いますので、そのフォーマットを自由に指定できるインターフェースが必要です。
誰もが触れるエクセルでフォーマットを作ってもらい、それを登録するだけであとは自動で値を流し込んで、昔に書いたこちらの記事の技術を使ってPDF化させます。
↑情報が古いので最新の公式ドキュメントと併せてご確認ください。
おわり
あまりにも汎用的すぎるシステムを作っても「じゃあkintoneでいいやん」ってなって、痒い所に手が届かないシステムになりますし、特定の業務に合わせすぎると利用者が限られてしまいますので、
「〇〇事業の〇〇業務専用ツール」という軸だけを持ってその中でどれだけ柔軟に対応できるかを日々考えております。