Javaを鍛える|デザインパターン全23回 #9

Composite:プロジェクト→サブタスク→チケットの階層

  • Java
  • デザインパターン
  • Composite
  • Javaを鍛える

📋 この記事の目次
  1. Compositeは何のためにあるか
  2. Before:TicketとProjectが別の型で、特別扱いが必要
  3. After:共通インターフェースで統一する
  4. こんな場面で使う/使わない
  5. まとめ
  6. 🎓 理解度テスト

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;
    }
}

TicketProjectは別々のリストで管理されており、Projectクラスの中だけで「チケットはこう数える」「サブプロジェクトはこう数える」という2種類の扱いを書き分けています。

Compositeは何のためにあるか

Compositeは、「個々の要素(葉)と、それらをまとめた集合(枝)を、同じインターフェースで扱えるようにする」 パターンです。会社組織図で「個人」と「部署(複数人の集合)」の両方に「今月の成果を報告して」と同じ形式で依頼できれば、上位の管理者は相手が個人か部署かを気にする必要がありません。

Before:TicketとProjectが別の型で、特別扱いが必要

先ほどのコードのように、TicketProjectが共通のインターフェースを持たないため、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:共通インターフェースで統一する

TicketProjectの両方に、共通の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つのリストで管理するだけになりました。集計処理も、相手がTicketProjectかを区別せず、同じcountOpenTickets()を呼ぶだけで階層全体を辿れます。

🧓 ベテランの現場コラム Compositeは「木構造」を扱うあらゆる場面(組織図、ファイルフォルダ、UIのコンポーネントツリーなど)で応用が効く、非常に汎用性の高いパターンです。「個別のものと、それをまとめたものを、別々の型として扱っていないか」に気づいたら、共通インターフェースで統一できないか検討してみてください。

こんな場面で使う/使わない

  • 使う: 「個々の要素」と「それらの集合」が、同じ操作(今回で言えば集計)を共通に持てる場合
  • 使わない: 個々の要素と集合とで、そもそも共通の操作がほとんどない場合。無理に共通インターフェースを作ると、実装できない操作にダミーの処理を書く羽目になる

まとめ

  • Compositeは「個々の要素と、その集合を同じインターフェースで扱う」パターン
  • 木構造(階層構造)を再帰的に処理する場面と相性が良い
  • 個別のリストを別々に管理する必要がなくなり、集計処理がシンプルになる

🎓 理解度テスト

学んだ内容を確認しましょう。全3問。