🗺

プロダクトに合わせたアーキテクチャの作り方と原則【読書ログ】

に公開

Q. そもそも、なんで良いアーキテクチャを作りたいのか?

システムを構築、保守するために必要な人数を最小限に抑えるため です。うまくアーキテクチャを作れないと、時間が経つにつれてシステムが変更しづらくなり、リリースもしづらくなってしまいます。

ソフトウェアは常に「ソフト」であるべきです。言い換えると、変更しづらいソフトウェアは危険です。アーキテクトには継続的に開発、運用できるようシステムをソフトにしておく責任があります。

良いアーキテクチャを作るための普遍的な原則

どんなプロダクトであっても以下のステップを行うことで、システムをソフトな状態に保つことができます。

  1. アーキテクチャの目的を検討する(「選択肢を残しておく」、「ユースケースを強調する」など)
  2. SOLIDに従ったコンポーネントを作成する
  3. コンポーネントのまとめ方の3原則、つなぎ方の3原則(後述)に従い、目的に合わせたアーキテクチャを組み上げる
  4. 開発フェーズが進むごとに、重視するまとめ方の原則を変化させ、アーキテクチャもそれに追従させる

以下は実際に各種原則にしたがって作られた、架空のショッピングアプリのアーキテクチャです。

原則にしたがって作られた架空のショッピングアプリのアーキテクチャ

ここからは上の図のようなアーキテクチャを作るためのコンポーネントのまとめ方の3原則、つなぎ方の3原則について見ていきます(SOLIDについては多くの参考文献があるため割愛します)。

コンポーネントのまとめ方(凝集度)の3原則

以下がコンポーネントをまとめる時の3原則になります。

  • 再利用・リリース等価の原則(REP):再利用の単位とリリースの単位は等価になります。
  • 閉鎖性共通の原則(CCP):同じ理由、同じタイミングで変更されるクラスをコンポーネントにまとめます。変更の理由やタイミングが異なるクラスは別のコンポーネントに分けます。
  • 全再利用の原則(CRP):コンポーネントのユーザーに対して、実際には使わないものへの依存を強要してはいけません。

「再利用・リリース等価の原則」は、違反している例を考えるとわかりやすいです。例えば、ユーティリティクラスAとドメインモデルBを同じパッケージに入れて再利用している場合を考えます。この場合、もしユーティリティクラスAだけ最新版にしたくても、ドメインモデルBまで一緒に最新版になってしまいます。こうならないように、別々で再利用されるものは別々にリリースされるように分割します。

「閉鎖性共通の原則」はコンポーネントレベルでの「単一責任の原則」です。コンポーネントを変更する理由が複数あってはいけません。

「全再利用の原則」は非常に直感的です。あるコンポーネントに依存するなら、そのコンポーネントのすべてのクラスに依存するべきということです。そうでないと、依存していないクラスに変更があった際にも再デプロイが必要になってしまい、これは避けるべきです。

重視する原則は開発フェーズごとに変わる

これらの3原則には相反する部分があり、3原則を同時に完全に満たすことはできません。

例えば、「再利用・リリース等価の原則」を意識して再利用可能なコンポーネントを乱立すると、「閉鎖性共通の原則」に違反しやすくなります(異なる理由で変更されるクラスが同じコンポーネントに入ってしまうため)。逆に「閉鎖性共通の原則」を強く意識すると、同じような変更を数十のコンポーネントに入れなければならなくなります。

以下の図の各辺は、その辺で結ばれてない頂点を無視した時にかかるコストを表してます。

3原則間のトレードオフを表した三角形。各辺は、対面の原則を軽視したときのコスト

当然ですが、3原則を同時に満たせないからといって、全てを無視してよいわけではありません。大切なのは、現在のプロダクトにおいて、どの原則を重視すべきかを判断することです。開発フェーズに応じて重視すべき原則を適切に見極め、アーキテクチャを柔軟に変更するのがアーキテクトの役割です。

そもそも、最初にベストなアーキテクチャを作り上げることは不可能です。いつでも状況に応じて変更できるソフトなアーキテクチャを作ることを目指します。

開発初期はCCP、中盤以降はREPを重視する

どの原則を重視すべきかは一般論があり、ある程度はプロダクトに依らず決まっています。

  • 開発初期: 「閉鎖性共通の原則」と「全再利用の原則」が重要
  • 開発がある程度進んだ段階: 「再利用・リリース等価の原則」と「全再利用の原則」が重要

開発初期はアプリの仕様が安定しておらず 「どれが再利用できるのか」まだ判断できない ため、「再利用・リリース等価の原則」より「閉鎖性共通の原則」が重要です。

逆に、開発後半になると、再利用できるようにして同じ変更を適用するコンポーネント数を減らさないと効率が悪くなります。そのため、再利用できるコンポーネントを増やしていくのが重要です(もちろん、どれを再利用するかの判断は慎重にしなければなりません)。

コンポーネントのつなぎ方(結合)の3原則

  • 非循環依存関係の原則(ADP) (自明なので詳細は省略)
  • 安定度・抽象度等価の原則(SAP):コンポーネントの抽象度はその安定度と同程度でなければならない
  • 安定依存の原則(SDP):変動を想定したコンポーネントは、変更しづらいコンポーネントから依存されるべきでない

各コンポーネントには安定度があり、これは「変更のしやすさ、変更の受けやすさ」を意味します。

  • 安定したコンポーネント:多くのコンポーネントから依存されている
  • 不安定なコンポーネント:多くのコンポーネントに依存している

「安定したコンポーネント」は、それを変更した時に多くのコンポーネントに影響が出るので簡単に変更できません。反対に「不安定なコンポーネント」は依存先が多いため、影響を受けやすくなります。

この図で言うとウェブサーバーは不安定なので変更しやすいです。反対に、価格算出サービスは安定しており、他コンポーネントに影響するような変更は簡単にできません。

SAP: 安定度と抽象度は同じにすべき

コンポーネントの抽象度と安定度は同じくらいであるべきです。つまり、抽象度が高い上位レベルの方針には安定度が高いコンポーネント を置き、抽象度の低い下位レベルには安定度の低いコンポーネント を配置します。

上位の方針のコンポーネントほど安定しているアーキテクチャ図

この図では、方針のレベルは以下の順になります。

  1. 最上位の方針:価格算出サービス(最もビジネスロジックに密接に関わっている)
  2. 上位の方針:ユースケース
  3. 最下位の方針:ウェブサーバー

これに合わせて、安定度も高いほうから価格算出サービス、ユースケース、ウェブサーバーの順になるように接続されています。これが安定度・抽象度等価の原則です。

SDP: 安定度の低いほうから高いほうへ依存すべき

変更しづらいコンポーネントから依存されたコンポーネントは変更しづらくなってしまいます。そのため、変更しやすくしておきたいコンポーネント(UIなど)から変更しづらいコンポーネントへ依存するようにします。(安定依存の原則)

SDPに違反している例

上の図はSDPに違反しています。抽象度の高い方から低い方へ依存して、変更が起きやすいコンポーネントに変更されづらくあって欲しいコンポーネントが依存してしまっています。これではビジネスロジックはとても不安定な状態です。

SDPに従い、ウェブサーバー→注文パッケージ→価格算出サービスの順で依存が向かうように修正すべきです。この順序に従うことで、変更しやすい層が安定した層に依存し、全体の柔軟性が保たれます。

プロダクトに合わせたコンポーネントの切り方、アーキテクチャの作り方

ここまでは普遍的な原則の話でした。ここからは実際のプロダクトやビジネスの視点に立ち、コンポーネントをどう切るか、境界をどう引くかについて説明します。

システムの意図をサポートしなければならない

優れたアーキテクチャはシステムの意図をうまく補助(サポート)できる必要があります。 例えばショッピングカートのアプリであれば、ショッピングカートのユースケースを明確にサポートする必要があります。うまくアーキテクチャを作れた場合、システムのユースケースが構造の中にはっきりと見えるようになります。

アーキテクチャが意図をサポートするには、独立性が大事です。独立して開発・運用でき、デプロイでき、選択肢を残しておけることが肝要です。そのために、これまでに述べてきた各原則に従うことが重要になります。

システムそのものについて伝えなければならない

優れたアーキテクチャがあれば、データベースやフレームワークといった詳細の決定を長く延期・留保できます。 優れたアーキテクチャはユースケースを強調し、周辺の関心事からユースケースを切り離します。

アーキテクチャの良し悪しは、「何を『叫んで』いるか」である程度評価できます。最上位レベルのディレクトリ構造やパッケージ名が、ユースケースについて叫んでいると良いアーキテクチャです。もしフレームワーク名などを叫んでいる場合は、ユースケースをうまく強調できていません。

# 良い例:ユースケースを叫んでいる
my-app/
└ modules/
   ├ healthcare-system/
   ├ accounting-system/
   └ inventory-management-system/
# 悪い例:フレームワーク名などを叫んでいる
my-app/
└ modules/
   ├ rails-app/
   ├ spring-hibernate/
   └ asp-inventory/

まとめると、アーキテクチャはシステムそのものについての情報を伝える必要があります。 フレームワーク名ではなく、システムのユースケースを示すことが重要です。

「詳細」は簡単に変更、差し替えできなければならない

ビジネスロジックから遠い入出力層(UI、データベース、フレームワークなど)は簡単に差し替えられるべきです。言い換えると、プラグインアーキテクチャを目指します。

UIやデータベースは詳細かつ下位レベルの方針であり、これらの置き換えがしづらいと上位レベルであるビジネスロジックも変更しづらくなってしまいます。これは非常にまずい状態です。上位レベルは下位レベルの影響を受けるべきではありません。

「安定度・抽象度等価の原則」や「安定依存の原則」に従って詳細をプラグイン化します。 つまりビジネスロジックがUIやデータベースに依存するように設計します。これにより、UIやデータベースがプラグイン化され、差し替え可能になります。

詳細が差し替え可能になった状態

package by componentを採用すべき

パッケージはコンポーネント単位で作るべきです。ここでいうコンポーネントとは「ビジネスロジックと永続化処理をまとめたもの」を指します。

これにより、永続化処理へのアクセスを制限し、関連機能をクリーンなインターフェイスの向こうに閉じ込めることで、凝集度の高いコンポーネントを実現できます。コントローラーまで内包してしまうと、詳細までも抱え込むことになってしまいます。

結論

  1. アーキテクチャの目的を検討する(「選択肢を残しておく」、「ユースケースを強調する」など)
  2. SOLIDに従ったコンポーネントを作成する
  3. コンポーネントのまとめ方の3原則、つなぎ方の3原則に従い、目的に合わせたアーキテクチャを組み上げる
  4. 開発フェーズが進むごとに、重視するまとめ方の原則を変化させ、アーキテクチャもそれに追従させる

以上のステップを踏むことで、 システムの構築・保守コストを抑えられます。 いつでも変更しやすい(ソフトな)システムを作ることは、ビジネスを進める上で非常に重要です。

最後に、本記事は以下の本の読書ログをもとに整理したものです。大胆に省略した部分も多くありますので、本記事が参考になりましたら、実際の書籍もぜひお読みください。

読んだ本のタイトル

『Clean Architecture 達人に学ぶソフトウェアの構造と設計』です。

世間で「クリーンアーキテクチャ」はバズワードと化していて意味を失っているため、本記事ではあえてクリーンという単語を使いませんでした。

Discussion