アーキテクトの教科書 価値を生むソフトウェアのアーキテクチャ構築を読んで、個人的に大事だと思ったことを整理しました。
誰におすすめの書籍か
- アーキテクトの仕事内容を知りたい人
- ソフトウェア設計とアーキテクチャ設計の基本知識を身につけたい人
コンポーネントとモジュールの違い
本書において、コンポーネントとモジュールは以下の通り定義されています。
- コンポーネント
- 特定の振る舞いを提供し、インターフェースで定義されたソフトウェア部品のこと。
- 複数のクラスから成り立つことが多い。
- DI コンテナで管理対象となるコントローラー、サービス、リポジトリの単位。
- ソースコードでは、コンポーネントを構成するクラス群をパッケージや名前空間として、一箇所でまとめて管理する。
- モジュール
- コンポーネントの集合体で、ユースケースの実行に必要な機能を提供する。
- ソースコードでは、パッケージのツリー構造全体がモジュールに該当する。
注意
コンポーネントとモジュールは様々な定義があります。文脈によって違う意図で使用されることがあるため、注意が必要です。
LSP:リスコフの置換原則
LSP とは、以下のような原則です。
サブタイプのオブジェクトは、スーパータイプのオブジェクトの仕様に従わなければならない。
サブタイプのオブジェクトは、スーパータイプのオブジェクトと置換可能でなければならない。
正方形と長方形の例で説明します。
正方形は幅と高さが等しい長方形と考えることができます。しかし、正方形を長方形のサブタイプとすると、LSP を満たさなくなります。
Rectangle(長方形)クラスの setHeight メソッドは、高さを変更します。このメソッドの暗黙の仕様は、「高さを変更しても幅は変わらない」ということです。
// 長方形クラス
class Rectangle {
int width, height; // 幅と高さ
Rectangle(int w, int h) { width = w; height = h; }
void setHeight(int h) { height = h; } // 高さを設定するメソッド
int getArea() { return width * height; } // 面積を計算するメソッド
}
次に、Square クラスです。正方形では、幅と高さが常に等しくなければなりません。そのため、setHeight メソッドをオーバーライドし、高さを変更する際に幅も同時に変更するように実装しています。
// 正方形クラス
class Square extends Rectangle {
Square(int side) { super(side, side); }
// 問題のあるメソッド
@Override
void setHeight(int h) {
width = height = h; // 高さを変えると幅も変更する
}
}
この実装では、Square の setHeight メソッドは Rectangle の setHeight メソッドの仕様を破っています。「高さを変更しても幅は変わらない」という仕様を満たしていません。
LSPViolation クラスの main メソッドでは、Square オブジェクトを Rectangle 型の変数に代入し、setHeight メソッドを呼び出しています。
class LSPViolation {
public static void main(String[] args) {
Rectangle r = new Square(5); // 一辺5の正方形を作成
System.out.println("最初の面積:" + r.getArea()); // 面積は25
r.setHeight(10); // 高さを10に変更
System.out.println("高さを変更後の面積:" + r.getArea()); // 面積は100
}
}
Rectangle 型の setHeight メソッドを呼び出すと、高さが変更されるだけで幅は変わらないことを期待します。しかし、Square オブジェクトでは幅も変更されてしまいます。
以上から、Square オブジェクトは Rectangle オブジェクトと完全に置換可能ではなくなり、LSP を満たしません。
モノシリックアーキテクチャと分散アーキテクチャのメリット・デメリット
本書で挙げられていた各アーキテクチャのメリット・デメリットから、個人的に重要だと感じたものを抜粋しました。
-
モノシリックアーキテクチャ
- メリット:
- ソースコードの管理、ビルド、デプロイがしやすい。
- 単一のアプリケーション内でトランザクションが完結するため、トランザクションの整合性を保ちやすい。
- デメリット:
- アプリケーションの規模が大きくなるため、ビルドやデプロイに時間がかかる。
- モジュールやコンポーネント間の依存関係が複雑になり、保守性が下がる。
- 小さな変更を加えただけでも、アプリケーション全体のビルドとデプロイが必要になる。
- メリット:
-
分散アーキテクチャ
- メリット:
- サービス単位でデプロイできるため、システムのアジリティが向上する。
- 負荷が高いサービスだけスケーリングできる。
- サービスごとに最適な技術スタックやアーキテクチャを採用できる。
- デメリット:
- ソース管理やバージョン管理が複雑になる。
- 運用監視、障害発生時の原因調査、トランザクションの整合性担保が難しくなる。
- 設計の難易度が高い。
- メリット:
トランザクション境界
レイヤードアーキテクチャにおいて、プレゼンテーション層とドメイン層の境目をトランザクション境界とするのが定石です。
プレゼンテーション層とドメイン層の境目をトランザクション境界とする理由は、以下の 2 つです。
- ステートレスなプロトコルである HTTP を用いる Web アプリケーションでは、トランザクションが複数リクエストにまたがることはない。
- 1 つのリクエストの中で、複数トランザクションが発生することは基本的にない。
ユニットテストの単位
ユニットテストは、ソフトウェアを構成する最小単位の振る舞いが正しいことを確認するテストです。
ユニットテストの単位として、以下の 2 つの考え方があります。
- プログラムの最小単位
- プログラムの最小単位とする考え方で、オブジェクト指向言語の場合はクラスが該当する。
- メリット:
- 1 つのクラスに 1 つのテストを作成するので、テストの作成ルールがわかりやすい。
- デメリット:
- クラス間の依存が強い場合、テストコードが内部設計の変更の影響を受けやすい。
- 振る舞いの単位
- コンポーネントを最小単位とする考え方。
- メリット:
- コンポーネント内部の設計変更に、テストコードが影響を受けない。
- デメリット:
- 分割単位が作業者によってばらつく。
「コンポーネント内部の設計変更に、テストコードが影響を受けない」というメリットがあるため、振る舞いの単位で作成する方が筋は良さそうです。本書の著者も、振る舞いの単位で作成することを推薦しています。
UT、IT、E2E の目的
テストの目的は、以下の 2 つを確認することです。
- 新たに開発したプログラムが正しく動作すること
- UT、IT で確認
- 開発済みのプログラムが引き続き正しく動作すること(リグレッションテスト)
- UT、IT、E2E で確認
UT と IT でビジネスロジックなどの細かい検証をして、E2E ではユーザがユースケースを実行して目的を達成できることを検証します。
まとめ
経験の浅い私でも理解できて読みやすい書籍でした。特に設計からテストまで体系的に整理されている第 3 章から第 5 章は、今後何度も読み返すことになりそうです。