技術的負債の発生を抑制する、UI指向アーキテクチャ
こんにちは。
株式会社 CHILLNN という京都のスタートアップにて CTO を担っております永田と申します。
本記事では、GraphQL を活用した、規模が拡大しても崩壊しづらいソフトウェアアーキテクチャについて考察していきます。
自分は普段はバックエンドを書いているのですが、先週は少し React を書いていて、その中で考えたアーキテクチャについて紹介させていただきます。
はじめに
弊社では API として GraphQL を利用しています。
API レスポンスの型をフロントエンドと共有できるため、API 設計を行う際に UI の構成を強く意識することができます。
弊社でのバックエンドの開発フローはざっくり以下のような流れになっています。
- UI デザイナーが Figma で UI を作成する
- UI の構造を反映させた ViewModel(GraphQL Schema)を定義する
- ViewModel に変換するコードを実装する
次の章から、例を挙げて具体的に説明していきます。
ViewModel 定義の具体例
弊社にて提供している宿泊予約のサービスをイメージして、下記のような UI から ViewModel を設計してみます。
ViewModel を作成する際には、フロントエンドでの Component 分割を意識します。
より具体的には、分割された Component が過不足のない Props の受け渡しがスムーズに行えるようにデータを設計していきます。
まず、大きく二つの Component に分割することができるかと思います。
- 日程に関する Component
- 料金詳細に関する Component
さらに、上記のスクリーンショットでは示されていませんが、料金詳細の各要素はアコーディオンによる開閉ができ、アコーディオンを開くと予約したリソースの詳細情報を確認することができます。
以上の分析から、下記のように ViewModel を設計します。
type ReservationDetailViewModel = {
dateInfo: {
checkInDate: string;
checkOutDate: string;
},
resourceInfo: {
planInfo: {
priceTotal: number;
planInfoDetail: {
...省略
}
},
roomInfo: {
priceTotal: number;
roomInfoDetail: {
...省略
}
},
optionInfo: {
priceTotal: number;
optionInfoDetail: {
...省略
}
}
},
};
このデータ構造は、「階層ごとに UI で表現される情報の抽象度を揃えたもの」だということができます。
どの階層のオブジェクトも分割された UI Component と一対一で対応するように設計されているため、フロントエンドでは常に、「親から一つの Props を受け取るだけで、描画に必要なすべての情報を受け取る」ことができます。
ユーザーアクションが存在する場合
同一の ViewModel によって表現される UI Component 群の中で、Mutation を伴うユーザーアクションを行う必要がある場合には、そのレスポンスは常に ViewModel を返すように実装しています。
ユーザーアクションの結果は、root component の state に dispatch され、都度 component 群全体が再描画されます。
このように実装を行うと、パフォーマンス上のデメリットが発生することがあります。
一方その対価として、例えば複数箇所で同じ情報を参照する必要があるときに、全体の整合性が保たれていることがバックエンドによって保証されるため、将来の修正についてあまり考慮する必要なく、冗長な ViewModel を設計することができます。
その他のモジュールの実装について
人が理解・処理できる情報は限られています。
同じモノを扱うとき、それを同じ構造で認識できることは、認知的な負荷を軽減させます。
これまで紹介してきた方法で ViewModel を定義することで、アプリケーションの実装上生じる、以下の要素の構造を一致させることができます。
- UI の論理的な構造
- API レスポンスのデータ構造
- バックエンドで API レスポンスのデータを作成するコードのディレクトリ構造
- フロントエンドでデータを描画するコンポーネントのディレクトリ構造
get-reservation-detail-view-model
├─ get-date-info
│ └─ index.ts
├─ get-resource-info
│ ├─ get-plan-info
│ │ └─ index.ts
│ ├─ get-room-info
│ │ └─ index.ts
│ ├─ get-option-info
│ │ └─ index.ts
│ └─ index.ts
└─ index.ts
reservation-detail
├─ date-info
│ └─ index.tsx
├─ resource-info
│ ├─ plan-info-detail
│ │ └─ index.tsx
│ ├─ room-info-detail
│ │ └─ index.tsx
│ ├─ option-info-detail
│ │ └─ index.tsx
│ └─ index.tsx
└─ index.tsx
これらを一致させておくことで、UI が修正された際、コード上のどの箇所に手を加えればいいのか、迷いなく視覚的に理解することができます。
また、この構成をとると、詳細を子のモジュールの責務として隠蔽することになるので、関数の責務超過が起きづらくなります。逆に、この構造で表現されていない場合には、異なる抽象度の情報を同一階層で扱っていると考えられるため、どこかで責務超過が発生していると考えられます。
複雑で読みにくいコードの特徴の一つとして、「同一スコープ内で記述されている処理の抽象度が揃っていないコード」が挙げられます。データ構造と dir 構成が一致しているかどうかを確認することで、セルフレビューの精度が高まることが期待できます。
また、ViewModel の構造が変わる場合にも、どこかのレイヤーで無理な実装をして負債を重ねるのではなく、影響範囲の部分のみを損切りし、新たに実装し直す意思決定を行いやすくなり、継続的な負債の返済のしくみをアーキテクチャに内包させることができます。
まとめ
本記事のまとめとして、今回定義したアーキテクチャのルールを記述しておきます。
- フロントエンドの画面描画に必要なデータを、親から一つの Object として過不足なく受け取れるように ViewModel を設計する
- フロントエンドで実行される Mutation の返り値は常に ViewModel の全体を返す
- 下記の 4 つの要素の構造をそれぞれ一致させる
- UI の論理的構造
- ViewModel のデータ構造
- バックエンドの dir 構造
- フロントエンドのコンポーネント分割の dir 構造
最後に
この記事での内容を通して何をしたかったのかというと、冒頭にも書きましたが「崩壊しないソフトウェアアーキテクチャを定義する」ということをしたかったです。
ソフトウェアアーキテクチャの境界は人によって理解が様々で、チームが拡大するごとに一貫性を保ったまま実装していくことが困難になっていきます。
自分自身、実装をする中で感じていた、「アプリケーションの理解レベルによらず、誰もが理解し守ることができるソフトウェアアーキテクチャは定義できるだろうか?」という問いに対する、UI デザイナーの存在を前提とした、フロントエンドでの一つの解が今回の記事になっています。
データ構造という具体的なものをベースにしたアーキテクチャであれば、誰もが遵守できているかを判断しやすいのではないかと思い、実装してみました。現時点ではかなりうまくいっています。
少しでもどなたかの参考になれば幸いです。
さて CHILLNN では、Web エンジニアの方を積極的に採用しています。
もし「実際に動いているコードが見てみたいよ〜」という方がいらっしゃいましたら、お気軽にお声がけください!
Discussion