ソフトウェア設計においてデータが境界を跨ぐ際の依存性の方向に関する考察
はじめに
株式会社CHILLNNという京都のスタートアップにてCTOを務めております永田と申します。
昨夜、「依存性逆転の原則に従う抽象と実装のディレクトリ構成を考える」という記事を公開しました。
この記事を読んでくれた弊社のエンジニアから「FrontEndのGraphQLのAPI呼び出しのベストプラクティスとしてページごとに呼び出しを定義するパターンが用いられていて、これは依存性逆転の原則に従っていないがそれはなぜなのか?」という質問がありました。
本記事では、この観点に関する考えを深掘って解説していきます。
システムに境界を設ける理由
優れたソフトウェアアーキテクチャは、優れた境界を定義します。
では、そもそもなぜシステムに境界を設ける必要があるのでしょうか?
一般に普及しているアーキテクチャでは、アプリケーションをそれを構成する複数のコンポーネントに切り分け、それぞれに特定の名前をつけます。
名前をつけるのはアーキテクチャの具体性をあげて共通見解を作ることで実装に適用しやすいためであり、技術的要請というよりはマーケティング的な観点での要請となっています。アーキテクチャを理解する上で重要なのは、コンポーネントの名前と定義を覚えることではなく、そのアーキテクチャの前身となるアーキテクチャのどのような課題を解決するために生まれたものなのかを理解することです。(自分は固有名詞を覚えるのが苦手なのでその言い訳でもあります)
このコンポーネントの間にあるのが境界です。
長年運用しているソフトウェアがしばしば複雑になっていく原因は、あらゆる開発が必ず以下のような性質を持っているからです。
- 将来は予測不可能である
- 開発は時間軸に沿って連続的に行われる
アプリケーションに対する機能要件が変化していく中で、初期に想定していたモデルと実態の間に、様々なズレが生じるようになってきます。このズレは軽度なものであれば特定の箇所でデータの変換を行うことで解消ができますが、時間経過とともに差分が積み上がっていくことでいずれ理解不能なサイズまで膨れ上がっていきます。
前段が長くなりましたが、システムに境界を設ける理由の一つは、上記のようなズレの発生をあらかじめ見越し、そのズレを吸収するための余地をシステムに残しておくためです。自分はこのズレが発生する境目を境界と呼んでいます。
境界が吸収するズレとは何か
前章で境界について定義を行いました。
では、境界上で発生するズレとは何を指すのでしょうか?
プログラムとは、入力データを処理し、特定の出力データを作成する方法を記述した一連のマシン命令のことです。特定のコードモジュールから、別のコードモジュールにデータが受け渡されることで処理が進んでいきます。
当然、境界の両側に存在するコードモジュール同士も、データを介してやり取りを行います。
ズレとは、唯一境界を跨ぐもの、つまりデータのズレのことを指します。
ズレを生じさせるデータの種類
ソフトウェアで扱うデータは、以下の二種類に分けることができます。
① 変更が困難なデータ
② 変更が容易なデータ
変更が困難なデータとは、DBに保存されているハードウェアに束縛されたデータや外部データソースから取得したデータを指します。
変更が容易なデータとは、EntityやDomainModelなどといった、ソフトウェア内部で定義され、実際に処理を行う際に用いられるデータを指します。
前者のデータは、アプリケーションの要請が変更になったからといってすぐに変更を行うことはできません。一方後者のデータは、アプリケーションの要請に合わせて柔軟に変更していくことが求められます。
どのシステム境界で依存性を逆転させるべきか
システム境界では、データのズレが生じるということをお話ししてきました。
ズレが生じる場合は、依存性逆転の原則を適用することでコードの寿命を伸ばすことが出来る場合があります。
では、どのズレに対して依存性を逆転させるべきなのか一つ一つ見ていきましょう。
依存性を逆転させた方が良い場合
システム境界で特に依存性を逆転させる必要があるのは、データ取得のフローが以下のようになる場合です。(データ更新のフローは逆の変換になります)
①から②への変換
このパターンは、DBからのデータ取得などの処理を想定しています。
この境界では変換前後のデータの変換容易性が異なっており、経験上、ある程度のデータの変換が求められるようになってきます。データ変更に伴うアプリケーションの複雑性の増加のコストを下げるために、依存性を逆転させ、ズレを吸収する余地を残しておいた方が賢明です。
具体例
例えば、弊社のバックエンドアーキテクチャでは、データアクセスレイヤー(①)とデータエンティティ(②)の間で依存性を逆転させています。
この際、データアクセスレイヤーで直接抽象を実装するのではなく、間に変換を行う責務を持ったドライバ層を挟んでいます。この冗長な構成を取ることで、例えば、アプリケーションで扱うデータモデルが複数のハードウェアに束縛されたデータを参照したい場合などでもダイナミックなデータの変換が可能になります。
また、変換のコードとデータアクセスのコードを切り離すことで責務を小さく保つことが期待できます。
(場合によっては)複数の②から②への変換
このパターンは、アプリケーションで共通で保持しているデータモデルから、モジュラーモノリスで構築したアプリケーション内部のサブモジュールで利用するデータモデルへの変換を想定しています。
この境界では、依存性の逆転を行わなくとも、どちらも柔軟なデータ構造を持っているため、呼び出し側で変換用のコードを定義すれば問題がないことも多いのですが、依存性を逆転させることでデータ変換の責務を明示的に切り分けることができ、コードモジュールの責務が明確になり保守性の向上を見込める可能性があります。
依存性を逆転しなくてもよい場合
①から①への変換
外部データソースからデータを取得しデータベースに保存する処理などを想定しています。この場合、参照元のデータの変更頻度は低いはずで、高頻度な変更を想定する必要はありません。また、アプリケーションでデータを利用する際には②に変換する必要があるため、ほとんどの場合不要だと考えて良いです。
単一の②から②への変換
この場合は、②のデータは柔軟なデータ構造を持っており、呼び出されるデータが呼び出し側に最適化されたデータ構造を保持しているべきです。依存関係の逆転が必要になるような場合は、むしろ変換前のデータ設計を疑うべきです。
②から①への変換
これは、外部ライブラリを利用する場合を想定しています。
技術選定に不安がある場合は逆転させておいてもいいかもしれないですが、多くの場合はやりすぎです。依存性の逆転が必要な場合、外部ライブラリへの依存性が大きすぎるため採用を考え直した方がいいかもしれません。通常はラッパーを定義すれば事足ります。
FrontEndからのデータアクセスに関して
最初の話に戻りましょう。
結論から申し上げますと、frontendからserverへのデータアクセスに関して冗長化しなくても良い理由は、backendからfrontendに返されるデータが、変換済みの②のデータだからです。
データ変換の冗長化の責務はbackendが担っています。
backendはクライアントにとって最適なデータを返す責務をもっており、クライアントの要請に合わせてデータの変換を柔軟に行うことが求められます。つまり、API Schemaの変更があったとしても、その変更はクライアントサイドにとって最も都合がいい状態を維持していると考えることができます。
もちろん、これは理想的な開発環境においての話ですが、frontendの開発者が持つべき責務とbackendの開発者が持つべき責務を考えた場合、このような状態を目指すべきです。
もしこれが守られていないとしたら、APIを介してやり取りされるデータの変換のロジックを誰が担うべきかに関して常に迷いが生じてしまい、frontendとbackendの境界が曖昧になっていってしまうでしょう。結果的には両側の責務が漏れ出したようなコードが生まれてしまい、ソフトウェアの複雑性は増していきます。
外部サービスのAPIをfrontendから直接参照する場合は?
外部サービスのAPIをfrontendから直接参照する場合はどうでしょうか?
この場合は、①から②への変換になるため、ベストプラクティスとしては依存性の逆転を行うべきですが、レスポンススキーマに関する将来の変更頻度を考慮に入れる必要があります。
一般に公開APIのスキーマが高頻度で変更されることは考えづらいでしょうが、将来的に変更される可能性が高ければ冗長化すべきだし、ほとんど変更される可能性がないならば冗長化は不要です。変更が想定される場合でも、データソースへのアクセスロジックをbackendに吸収してもらうことができれば最も柔軟で扱いやすくなります。
まとめ
ソフトウェアアーキテクチャの責務の一つは、将来の変更に対する柔軟性を維持することです。
コードの複雑化を防ぐためには、どの変更をどのレイヤーで吸収するのかをあらかじめ設計しておく必要があり、そのために、アプリケーションでは将来どのような変更が起こりうるのかに対して知見を深めておくことが重要です。
今回の記事を通して、なんとなく直感で行っていた分類を言語化してみたのですが、個人的には非常にスッキリしました。参考になれば幸いです。
Discussion