集約と関連とは何か
オブジェクト指向で「集約」と「関連」は、クラスとクラスの“つながり方”を表す言葉です。
ざっくり言うと、
- 関連:ただの「知り合い」関係(A が B を知っている・参照している)
- 集約:あるオブジェクトが「まとまりの単位」として他のオブジェクトを束ねる関係(全体と部分)
どちらも「あるクラスが別のクラスをフィールドに持つ」形で表現されますが、
どこまでを「ひとまとまり」と見なすか、ライフサイクルをどう扱うか、という設計の意味が変わってきます。
関連(Association)とは
「A は B を知っている」くらいのゆるい関係
関連は一番広い概念で、「あるクラスが別のクラスへの参照を持っている」程度の関係です。
お互いのライフサイクルは独立していて、「片方が消えたらもう片方も消える」ような強い意味はありません。
final class User {
private final String name;
private final Address address; // User は Address を“知っている”
User(String name, Address address) {
this.name = name;
this.address = address;
}
String name() { return name; }
Address address() { return address; }
}
final class Address {
private final String city;
Address(String city) {
this.city = city;
}
String city() { return city; }
}
JavaUser は Address を参照していますが、
Address 自体は Address だけで単独のオブジェクトとして意味を持ちます。
User がいなくなっても、Address が他の誰かに使われ続けることもありえます。
ここでは「User と Address は関連している」と言えますが、
User にとって Address は“必ずしも構造上の一部(パーツ)”とは限りません。
集約(Aggregation)とは
「ひとまとまり」を表す全体と部分の関係
集約は「全体(Aggregate Root)と、その配下にある部分オブジェクトのまとまり」を表す関係です。
単なる“知り合い”ではなく、「これらはひとつの一貫した塊として扱うべき」という意味が付きます。
代表的な例として「注文(Order)と注文明細(OrderLine)」があります。
import java.util.List;
final class Order {
private final List<OrderLine> lines; // Order は OrderLine を「自分の一部」として持つ
Order(List<OrderLine> lines) {
this.lines = List.copyOf(lines); // 不変コピーで一貫性を守る
}
int totalPrice() {
return lines.stream().mapToInt(OrderLine::linePrice).sum();
}
List<OrderLine> lines() {
return lines;
}
}
final class OrderLine {
private final String itemName;
private final int unitPrice;
private final int quantity;
OrderLine(String itemName, int unitPrice, int quantity) {
this.itemName = itemName;
this.unitPrice = unitPrice;
this.quantity = quantity;
}
int linePrice() {
return unitPrice * quantity;
}
}
Javaここでのポイントは、
- Order と OrderLine をセットで扱う(Order の合計金額には、全ての OrderLine が関わる)
- Order の外側から、勝手に OrderLine をいじらせないほうが、安全(一貫性を守れる)
といった「まとまりとしての意味」があることです。
集約とライフサイクル(重要な深掘り)
集約の単位で「整合性を守る」
集約の一番大事な役割は
「このグループの中は、常に整合した状態を保ちたい」という境界を作ることです。
Order を例にすると、
- Order の合計金額
- OrderLine の数
- 各 OrderLine の数量や単価
などは、バラバラに変更されるとおかしくなる可能性があります。
そこで、
- Order は自分の配下にある OrderLine を「自分の管理下の一部」として扱う
- Order のメソッド経由でしか OrderLine を追加・削除・変更できないようにする
といった形で、「一貫性を守る責任」を Order に集中させます。
final class Order {
private final List<OrderLine> lines = new java.util.ArrayList<>();
void addLine(String itemName, int unitPrice, int quantity) {
lines.add(new OrderLine(itemName, unitPrice, quantity));
}
int totalPrice() {
return lines.stream().mapToInt(OrderLine::linePrice).sum();
}
}
JavaOrder の外側から勝手に List<OrderLine> をいじらせないようにすると、
Order が「集約のルート(代表)」として、状態の整合性を守りやすくなります。
関連として扱うか、集約として扱うかの判断
「本当に“構造上の一部”か?」を自分に問いかける
次のような視点で考えると、集約か単なる関連かが判断しやすくなります。
- 片方のオブジェクトが中心で、もう片方は「その一部」としてしか意味を持たないか
- 「このまとまりの中の整合性」を、誰か1つのクラスに責任集中させたいか
- 保存や再構築の単位も“そのまとまり”で考えたいか
例えば「ユーザとパスワード履歴」なら、多くの場合「ユーザの集約」に含まれます。
一方、「ユーザと所属部門」なら、どちらもそれぞれ独立した意味を持つので、単なる関連として扱うことも多いです。
final class Department {
private final String name;
Department(String name){ this.name = name; }
}
final class Employee {
private final String name;
private final Department department; // 関連(Employee は Department を参照)
Employee(String name, Department dept){
this.name = name;
this.department = dept;
}
}
JavaDepartment は Employee の「構造上の一部」というよりは、
独立して存在しうる“別の概念”ですよね。
実装レベルではどちらも「フィールド参照」
コード上は同じに見えるからこそ“意味づけ”が大事
ここが初心者に一番ややこしいところですが、
Java のコードだけ見ると、「集約」も「関連」も単に「フィールドに持っているだけ」に見えます。
final class A {
private B b; // これが集約なのか関連なのかは、コードだけでは断定できない
}
Java違いは「設計上の意味づけ」「責任の置き方」です。
- 集約なら:「A が B をまとめて管理し、一貫性を守る責任を持つ」
- 単なる関連なら:「A は B を知っているけど、B のライフサイクルや整合性は別」
というつもりで設計します。
その意図をコードに反映するには、
- B のコレクションを外部にそのまま返さない(コピーを返す)
- B をいじるメソッドを A に用意し、外から直接 B を変更させない
- B 自体を new するのは A 内に限定し、外で勝手に作らせない
などのルールを徹底します。
例題:集約を意識した設計と関連だけの設計
集約を意識した「ショッピングカート」
final class Cart {
private final java.util.List<CartItem> items = new java.util.ArrayList<>();
void add(String productId, int unitPrice, int quantity) {
items.add(new CartItem(productId, unitPrice, quantity));
}
int totalPrice() {
return items.stream().mapToInt(CartItem::linePrice).sum();
}
java.util.List<CartItem> itemsView() {
return java.util.List.copyOf(items); // 防御的コピー
}
}
final class CartItem {
private final String productId;
private final int unitPrice;
private final int quantity;
CartItem(String productId, int unitPrice, int quantity) {
this.productId = productId;
this.unitPrice = unitPrice;
this.quantity = quantity;
}
int linePrice() { return unitPrice * quantity; }
}
Javaここでは Cart が集約のルートです。
CartItem をどう追加・削除するか、合計金額をどう計算するかは Cart の責任です。
外部には itemsView() で「読み取り専用ビュー」だけを渡し、
中身のリストを直接いじれないようにしているところに「集約としての意図」が現れています。
単なる関連としての「ユーザと会社」
final class Company {
private final String name;
Company(String name) { this.name = name; }
}
final class User {
private final String name;
private final Company company; // ユーザは自分の会社を知っている(関連)
User(String name, Company company) {
this.name = name;
this.company = company;
}
}
Javaここでは Company は User の一部というより、
「User がどの会社に属しているか」という“関係”を表しているだけです。
Company 自体は Company として単独で意味を持ちます。
まとめ:集約と関連を設計でどう使い分けるか(重要)
関連は「A は B を知っている」という、ゆるい関係です。
集約は「A が B をまとめて管理し、一貫性を守る責任を持つ全体と部分の関係」です。
実装レベルではどちらも「フィールド参照」で表現されますが、
設計上は次の点が大きな違いになります。
- 整合性とルールを、どのクラスに集中させるか
- 保存や更新の単位(トランザクションの単位)をどこで切るか
- ライフサイクル(作る・変更する・消す)をどの塊で考えるか
