『クリーンアーキテクチャ』再読(5):ソフトウェアアーキテクチャ

Tommy Cheese | · 読了目安:8 分

いよいよソフトウェアアーキテクチャに進みます。これまではモジュール/クラスやコンポーネントを紹介しました。この回では、ソフトウェアアーキテクチャの基本的な定義を中心に説明します。

最初に伝えたいこと

🌟ソフトウェアアーキテクト自身がプログラマーであり、現場でコードを書き続ける必要があります。システム設計がもたらす問題を自分で引き受けなければ、設計の悪さによる苦痛を実感できず、やがて正しい設計の方向を見失うからです。

ソフトウェアアーキテクチャとは何か

ソフトウェアアーキテクチャの仕事の本質は、システムをどのようにコンポーネントへ分割し、その配置関係や通信方法をどうするかを検討、計画することです。つまり、境界を決めることです。

ソフトウェアアーキテクチャを設計する目的は、一般に二つあります:

  • コンポーネントの開発、デプロイ、実行、保守を、日々の仕事の中でより円滑に行えるようにすること。
  • さまざまな作業を進めやすいシステムを設計するために、設計の中でできるだけ長い間、できるだけ多くの選択肢を残すこと

ソフトウェアシステムのアーキテクチャの品質と、システムが正常に動くかどうかという振る舞いには、それほど強い関係はありません。設計はひどくても正常に動くシステムは、世の中にたくさんあります。本当の問題は、開発、デプロイ、その後の追加開発で現れがちです。これも、アーキテクチャの価値はある程度、振る舞いの価値より大きいという著者の考えを示しています。

ソフトウェアアーキテクチャ設計の目標は、次のとおりです:

  • 中核的な目標:良いアーキテクチャはユースケースを中心に設計されるべきです。ユースケースに集中し、他の周辺要素から隔離します。そのためには選択をできる限り先送りし、選択肢を多く残します。
  • 主な目標:ソフトウェアシステムのライフサイクル全体を支えることです。適切なアーキテクチャは、システムを理解しやすく、変更しやすく、保守しやすくし、簡単にデプロイできるようにします。
  • 究極の目標:プログラマーの生産性を最大化し、その力を解放すると同時に、システムの運用コストを最小化することです。

アーキテクチャ設計で重視するのは、方針を互いに分離し、変更のされ方に応じて再びグループ化すること、つまり境界を引くことです。この際の指針には、SRP、CCP、CSP、SDP、SAPなどがあります。

ソフトウェアアーキテクチャの責務

ソフトウェアアーキテクチャは、開発、デプロイ、実行、保守という複数の側面で責務を持ちます。

開発の面では、アーキテクチャはソフトウェアシステムを開発しやすくする必要があります。そのため、チームが異なれば採用するアーキテクチャ設計も異なります。これは、ある意味でコンウェイの法則を表しています。

コンウェイの法則は、なぜアーキテクチャにチーム構造が反映されることを説明できるのでしょうか。詳しくは動画をご覧ください:コンウェイの法則:なぜアーキテクチャにはチーム構造が反映されるのか?—bilibili

デプロイの面では、ワンクリックで簡単にソフトウェアをデプロイできるようにする必要があります。

実行の面では、

  • 前述のとおり、効率的な実行に対するアーキテクチャの影響は、他の側面より小さいとされています。著者はその主な理由を、実行面での配慮不足はハードウェアの追加で補える一方、アーキテクチャ設計には通常それより高価な人的資源が必要だからだと考えています。
  • 効率的な実行に寄与するだけでなく、アーキテクチャはシステムの実行時の要件も表すべきです。適切に設計されたアーキテクチャなら、開発者はシステムの動作を一目で理解できます。アーキテクチャは実行過程を明らかにし、ユースケース、機能、必須の振る舞いを、開発者に見える第一級の要素として扱うことで、システムを理解しやすくするべきです。「叫ぶアーキテクチャ」の章は、この点を重点的に説明しています。

保守の面では、ソフトウェアシステムの保守コストは通常最も高くなります。そのコストは、探索リスクの二つに分けられます:

  • 探索:探索(spelunking)のコストは、既存のソフトウェアシステムを調べ、新機能の追加や問題の修正を行う最適な場所と方法を見つける作業から生じます。
  • リスク:リスク(risk)は、これらの変更によって新しい問題を生む可能性を指します。その可能性がリスクのコストです。

選択肢を残す

以前紹介した、ソフトウェアの「振る舞いの価値」と「アーキテクチャの価値」を思い出してください。アーキテクチャの価値を高めるにはソフトウェアをより柔軟にし、そのためにはアーキテクチャにできるだけ多くの選択肢を残します。

ソフトウェアシステムの要素は、方針詳細に分けられます。方針は、ソフトウェア内のすべての業務ルールと処理手順を表し、システムの本当の価値を担います。詳細は、利用者、他のシステム、プログラマーが方針とやり取りするために必要ですが、方針そのものには影響しない振る舞いを指します。IOデバイスやデータベースなどが例です。こうした詳細については、できるだけ選択肢を残すべきです。

ソフトウェアアーキテクトの目標は、方針を最も基本的な要素とし、詳細を方針から切り離したシステムの形を作ることです。これにより、具体的な意思決定で詳細に関する決定を先送りできます。プロジェクトの後半になるほど、妥当な判断のための情報が増えるからです。優れたアーキテクトは、選択肢の数を最大化することに力を注ぐべきです。

著者は、詳細の決定を先送りする重要性をデバイス独立性の例で説明しています。現代のOSには多種多様な入出力デバイスがありますが、コンピューターの初期には、穿孔紙テープが主流で、ほぼ唯一の入出力手段でした。プログラマーは自然に、紙テープを読み書きするコードをシステムコードへ結合させていました。磁気テープが登場すると、その入出力に対応する新しいコードが必要になり、光ディスクが登場するとまた同じことが起きました。この重複開発と適応性の低さに対応するため、デバイス独立性という考え方が生まれました。デバイスを関数として抽象化し、OSは関数を通じて入出力デバイスとやり取りし、開発者はその具体的な実装だけを提供します。こうして入出力はOSのプラグインになりました。これは、オープン・クローズドの原則の原型でもあります。「追加を歓迎👏し、変更に抗う🥊!」ということです。

独立性を保つ

良いソフトウェアアーキテクチャには、十分な独立性が必要です。

結合を解くことで、アーキテクチャの独立性を確保できます。

分離には、水平方向と垂直方向があります。

  • 水平方向の分離(レイヤーごとの分離):システムをUI、データベースなど、複数の水平レイヤーに分けます。
  • ユースケースの分離(垂直方向の分離):水平レイヤーを分離すると同時に、ユースケースごとに垂直方向にも分割します。たとえば、注文を追加するユースケースのUIと、注文を削除するユースケースのUIを分けます。

変更理由の違いに応じてシステムを分離すれば、既存のユースケースに影響を与えずに、新しいユースケースを追加し続けられます。

分離を行うレベルも選べます。ソースコード、デプロイ、サービスという三つのレベルがあります。

  • ソースコードレベル:モジュール間のソースコードの依存関係を制御し、コンポーネントは関数呼び出しでやり取りします。この形をモノリシック構造と呼びます。
  • デプロイレベル:Jarファイルなどのデプロイ単位間の依存関係を制御します。コンポーネントは、スレッド間通信(ネットワーク越しではない点に注意)、Socket通信、共有メモリによる通信などを使います。
  • サービスレベル:コンポーネント間の依存をデータ構造のレベルまで減らし、ネットワークパケットで通信します。マイクロサービスがrpcやrestでやり取りする形などが例です。

どの分離レベルが優れているかを決める厳密な基準はありません。プロジェクトの成熟に伴って、最適な方式は変わり得るからです。適切に設計されたアーキテクチャなら、一つのファイルとしてデプロイするモノリシック構造から始め、独立した複数のデプロイ単位、さらには独立したサービスやマイクロサービスへ成長できます。そして状況が変われば、再びモノリシック構造へ段階的に戻せます。この過程でも、システムのソースコードの大部分は変更の影響から守られるべきです。システム全体にとって、分離の方式も選択肢の一つであるべきです。大規模なデプロイではある方式を使い、小規模なデプロイでは別の方式を使えます。

分離について理解したので、システムの独立性にどのような影響を与えるかを見てみましょう。

実行の独立性に対する分離の意義

異なる側面のユースケースが適切に分離されていれば、高スループットが必要なユースケースと低スループットでよいユースケースは自然に分かれます。UIやデータベースを業務ロジックから分離できれば、それぞれを別のサーバーで動かせます。大きな帯域を必要とするアプリケーションは、複数のサーバーで複数のインスタンスを動かせます。

開発の独立性に対する分離の意義

システムが水平レイヤーとユースケースに従って適切に分離されていれば、アーキテクチャは複数チームでの開発を支えられます。チームの分担が機能単位、コンポーネント単位、レイヤー単位、あるいは別の軸であっても対応できます。

デプロイの独立性に対する分離の意義

適切に分離できていれば、システムの実行中に各レイヤーの実装やユースケースをホットスワップできます。この場合、新しいユースケースを追加するには、新しいjarファイルを置くか、いくつかのサービスを起動するだけで済み、それ以外の部分は一切影響を受けません。

コードの重複とは何か

コードの重複には二種類あります:

  • 本当の重複:コードから取り除くべき部分です。
  • 見かけ上の重複:同じように見えても、実際には異なる進化の道筋を持ち、変更の頻度や境界も異なるコードです。CRPは、利用頻度が異なるコードを別のコンポーネントへ分けるよう求めます。この点を考慮せずに読むと、見かけ上の重複を本当の重複と誤認しかねません。このような「重複」は必要な場合があります。給与計算の例を覚えていますか。

(第五回・完)

comments powered by Disqus