Composite:プロジェクト→サブタスク→チケットの階層
📋 この記事の目次
TicketCraftでは、大きな案件を「プロジェクト」として管理し、その中に個別の「チケット」を入れられます。さらにプロジェクトの中に、別の「サブプロジェクト」を入れ子にすることもできます。この階層の中から「未対応チケット数」を数える処理は、こう書かれています。
import java.util.ArrayList;
import java.util.List;
public class Project {
private String name;
private List<Ticket> tickets = new ArrayList<>();
private List<Project> subProjects = new ArrayList<>();
public Project(String name) {
this.name = name;
}
public void addTicket(Ticket t) {
tickets.add(t);
}
public void addSubProject(Project p) {
subProjects.add(p);
}
// 未対応チケット数を数えるには、サブプロジェクトの分を手動で再帰させる必要がある
public int countOpenTickets() {
int count = 0;
for (Ticket t : tickets) {
if (t.isOpen()) count++;
}
for (Project sub : subProjects) {
count += sub.countOpenTickets(); // Projectの中でだけ特別扱いしている
}
return count;
}
}
TicketとProjectは別々のリストで管理されており、Projectクラスの中だけで「チケットはこう数える」「サブプロジェクトはこう数える」という2種類の扱いを書き分けています。
Compositeは何のためにあるか
Compositeは、「個々の要素(葉)と、それらをまとめた集合(枝)を、同じインターフェースで扱えるようにする」 パターンです。会社組織図で「個人」と「部署(複数人の集合)」の両方に「今月の成果を報告して」と同じ形式で依頼できれば、上位の管理者は相手が個人か部署かを気にする必要がありません。
Before:TicketとProjectが別の型で、特別扱いが必要
先ほどのコードのように、TicketとProjectが共通のインターフェースを持たないため、Project側で「チケットのリスト」と「サブプロジェクトのリスト」を別々に持ち、集計処理もそれぞれ書き分ける必要があります。
public class Main {
public static void main(String[] args) {
Project root = new Project("Webサイトリニューアル");
root.addTicket(new Ticket("トップページのレイアウト崩れ", true));
root.addTicket(new Ticket("お問い合わせフォームの不具合", false));
Project frontend = new Project("フロントエンド刷新");
frontend.addTicket(new Ticket("ダークモード対応", true));
root.addSubProject(frontend);
System.out.println("未対応チケット数: " + root.countOpenTickets());
}
}
実行結果:
未対応チケット数: 2
正しく動いてはいますが、「プロジェクトの中にプロジェクトを入れる」という階層がさらに深くなったり、「チケットの一覧を表示する」といった別の処理を追加するたびに、Ticket用とProject用の両方のコードを書く必要があります。
After:共通インターフェースで統一する
TicketとProjectの両方に、共通のTicketComponentインターフェースを実装させます。
public interface TicketComponent {
int countOpenTickets();
}
public class Ticket implements TicketComponent {
private String title;
private boolean open;
public Ticket(String title, boolean open) {
this.title = title;
this.open = open;
}
@Override
public int countOpenTickets() {
return open ? 1 : 0;
}
public String getTitle() { return title; }
}
public class Project implements TicketComponent {
// import java.util.ArrayList; と import java.util.List; はBeforeと同様に必要
private String name;
private List<TicketComponent> children = new ArrayList<>();
public Project(String name) {
this.name = name;
}
public void add(TicketComponent child) {
children.add(child);
}
@Override
public int countOpenTickets() {
int count = 0;
for (TicketComponent child : children) {
count += child.countOpenTickets(); // TicketでもProjectでも同じ呼び方でよい
}
return count;
}
}
public class Main {
public static void main(String[] args) {
Project root = new Project("Webサイトリニューアル");
root.add(new Ticket("トップページのレイアウト崩れ", true));
root.add(new Ticket("お問い合わせフォームの不具合", false));
Project frontend = new Project("フロントエンド刷新");
frontend.add(new Ticket("ダークモード対応", true));
root.add(frontend);
System.out.println("未対応チケット数: " + root.countOpenTickets());
}
}
実行結果:
未対応チケット数: 2
Projectは「チケットのリスト」と「サブプロジェクトのリスト」を別々に持つ必要がなくなり、TicketComponent型の子要素を1つのリストで管理するだけになりました。集計処理も、相手がTicketかProjectかを区別せず、同じcountOpenTickets()を呼ぶだけで階層全体を辿れます。
🧓 ベテランの現場コラム Compositeは「木構造」を扱うあらゆる場面(組織図、ファイルフォルダ、UIのコンポーネントツリーなど)で応用が効く、非常に汎用性の高いパターンです。「個別のものと、それをまとめたものを、別々の型として扱っていないか」に気づいたら、共通インターフェースで統一できないか検討してみてください。
こんな場面で使う/使わない
- 使う: 「個々の要素」と「それらの集合」が、同じ操作(今回で言えば集計)を共通に持てる場合
- 使わない: 個々の要素と集合とで、そもそも共通の操作がほとんどない場合。無理に共通インターフェースを作ると、実装できない操作にダミーの処理を書く羽目になる
まとめ
- Compositeは「個々の要素と、その集合を同じインターフェースで扱う」パターン
- 木構造(階層構造)を再帰的に処理する場面と相性が良い
- 個別のリストを別々に管理する必要がなくなり、集計処理がシンプルになる
🎓 理解度テスト
学んだ内容を確認しましょう。全3問。