「Composite」パターンとは?サンプルを踏まえてわかりやすく解説!【Java】

  • LINEで送る
composite

 本記事ではGoFのデザインパターンのプログラムの構造に関するパターンの一つである「Composite」パターンを解説します。このパターンを一言で説明するならば、「入れ物のクラスと中身のクラスを、1つの抽象クラスでまとめて、それぞれのクラスから得られるオブジェクトを同一視できるようにする」と言えるでしょう。文章では想像がつきにくいと思いますので、例を踏まえながら解説していきます。

「Composite」パターンとは

 先述した通り、「Composite」とは「入れ物のクラスと中身のクラスを、1つの抽象クラスでまとめて、それぞれのクラスから得られるオブジェクトを同一視できるようにする」です。「Composite」を直訳すると「混合の」や「合成物」という意味があります。

 同一視できるということは階層構造のオブジェクト群を扱いやすくなるということです。例えば下記のような会社内での役職を考えてみましょう。この場合一番上に社長、次に部長、その次に係長、最後に平社員というように階層構造になっています。社長は部長の社員情報を持っており、部長は係長の社員情報を持っており、係長は直属の社員の社員情報を持っています。

 ここで社員全員の社員情報を表示する時に、それぞれの役職ごとにクラスを作って社員情報を取得するメソッドを作ってみます。階層構造にはなっていますが、それぞれ独立したオブジェクトであるためクラスごとに社員情報を表示するメソッドを作って、クラスごとにそのメソッドを実行してあげる必要があります。

 これは非常に煩わしい作業です。もしこれらのクラスが親となるインターフェースから実装されたクラスだった場合どうなるでしょうか。全てのオブジェクトを同一視できるため社員情報を表示するメソッドを回帰的に呼び出すことが可能です。

「Composite」パターンの書き方

 上記の会社内での役職とその社員情報を表示する例を「Composite」パターンを利用する例と利用しない例で解説します。下図のような組織の社員情報を一括で表示するコードを作ります。

✖︎ Compositeパターンを使用しない例 ✖︎

 まず、「Composite」パターンを利用しない例から解説し、何が問題なのかを確認します。 最初にそれぞれの役職のクラスを作ります。

 「社員」は階層の末端オブジェクトになります。特に難しいものはなくname(名前)とsalary(給料)のフィールドを持っています。またコンストラクタがあり、名前と給料を引数にとってオブジェクトを生成します。最後にgetEmployeeInfo()で社員情報をStringで返却します。

// 社員
public class Employee {

    private String name;
    private int salary;

    public Employee(String name, int salary) {
        this.name = name;
        this.salary = salary;
    }

    public String getEmployeeInfo() {
        return "役職:なし、氏名:" + this.name + "、給料:" + this.salary + "\n";
    }
}

 次に係長のクラスを作ります。社員がもっているフィールドの他に、executiveCompensation(役員報酬)とsubordinate(部下のリスト)を持っています。subordinateにはEmployeeのみ入れることができます。このsubordinateのListにメンバーを追加するaddSubordinate()メソッドを用意しています。

 最後に部下を含めた社員情報を取得するgetEmployeeInfo()のメソッドを定義しています。自身の社員情報をStringBuilderのインスタンスに追加したあとIteratorでsubordinateのlistを走査して、employeeのgetEmployeeInfo()で部下の社員情報を取得してStringBuilderに追加します。

// 係長
public class SectionChief {

    private String name;
    private int salary;
    private int executiveCompensation;
    private List<Employee> subordinate;


    public SectionChief(String name, int salary, int executiveCompensation) {
        this.name = name;
        this.salary = salary;
        this.executiveCompensation = executiveCompensation;
        this.subordinate = new ArrayList<>();
    }

    public void addSubordinate(Employee employee) {
        subordinate.add(employee);
    }

    public String getEmployeeInfo() {
        StringBuilder builder = new StringBuilder();
        String sectionChiefInfo = "役職:係長、氏名:" + this.name + "、給料:" + this.salary + "、役員報酬:" + this.executiveCompensation + "\n";
        builder.append(sectionChiefInfo);
        
        Iterator<Employee> iterator = this.subordinate.iterator();
        while (iterator.hasNext()) {
            Employee employee = iterator.next();
            builder.append("      " + employee.getEmployeeInfo());
        }
        return builder.toString();
    }
}

 次に部長のクラスを作ります。コンストラクタ、addSubordinate()は係長クラスと同一なため省略します。下記のコード内では省略します。実際には定義しています。

 係長クラスと違うところは部下として、係長も社員もどちらも追加することができるということです。そのためどちらのクラスが入るかわからないためsubordinateのリストもObject型が入るようになっています。さらにiteratorでsubordinateを走査する時もinstanceofでオブジェクトの型を判定した後、係長クラスのgetEmployeeInfo()を呼ぶか、社員クラスのgetEmployeeInfo()を呼ぶかを判定しています。

// 部長
public class Manager {

    private String name;
    private int salary;
    private int executiveCompensation;
    private List<Object> subordinate;

    // コンストラクタ、addSubordinate()は係長クラスと同一なため省略します。

    public String getEmployeeInfo() {
        StringBuilder builder = new StringBuilder();
        String managerInfo = "役職:部長、氏名:" + this.name + "、給料:" + this.salary + "、役員報酬:" + this.executiveCompensation + "\n";
        builder.append(managerInfo);

        Iterator<Object> iterator = this.subordinate.iterator();
        while (iterator.hasNext()) {
            Object obj = iterator.next();
            if (obj instanceof SectionChief sectionChief ) {
                builder.append("    " + sectionChief.getEmployeeInfo());
            } else if (obj instanceof Employee employee) {
                builder.append("    " + employee.getEmployeeInfo());
            } else {
                System.out.println("存在しない役職の社員です。");
            }
        }
        return builder.toString();
    }
}

 最後に社長クラスを作ります。ほとんど部長クラスと相違はありません。社長の部下には部長、係長、社員の3パターンのオブジェクトが入るためif文に部長クラスかどうかの判定が入ります。

// 社長
public class President {

    private List<Object> subordinate;

    // 部長クラスと共通する部分は省略

    public String getEmployeeInfo() {
        StringBuilder builder = new StringBuilder();
        String presidentInfo = "役職:社長、氏名:" + this.name + "、給料:" + this.salary + "、役員報酬:" + this.executiveCompensation + "\n";
        builder.append(presidentInfo);

        Iterator<Object> iterator = this.subordinate.iterator();
        while (iterator.hasNext()) {
            Object obj = iterator.next();
            if (obj instanceof Manager manager) {
                builder.append("  " + manager.getEmployeeInfo());
            } else if (obj instanceof SectionChief sectionChief) {
                builder.append("  " + sectionChief.getEmployeeInfo());
            } else if (obj instanceof Employee employee) {
                builder.append("  " + employee.getEmployeeInfo());
            } else {
                System.out.println("存在しない役職の社員です。");
            }
        }
        return builder.toString();
    }
}

 これで組織図を構成するクラスを作ることができました。それでは実際に社員を入社させて組織を作り、社員情報を一括で表示させてみましょう。

public class Main {
    public static void main(String[] args) {
        President president = new President("前田", 100, 15);
        Manager manager1 = new Manager("田中", 90, 10);
        Manager manager2 = new Manager("佐藤", 90, 10);
        SectionChief sectionChief1 = new SectionChief("鈴木", 80, 5);
        SectionChief sectionChief2 = new SectionChief("中野", 80, 5);
        SectionChief sectionChief3 = new SectionChief("藤井", 80, 5);
        SectionChief sectionChief4 = new SectionChief("荒木", 80, 5);
        Employee employee1 = new Employee("斉藤", 50);
        Employee employee2 = new Employee("藤本", 50);
        Employee employee3 = new Employee("河村", 50);
        Employee employee4 = new Employee("森田", 50);
        Employee employee5 = new Employee("大谷", 50);
        Employee employee6 = new Employee("山本", 50);
        Employee employee7 = new Employee("松井", 50);
        Employee employee8 = new Employee("藤浪", 50);
        Employee employee9 = new Employee("千賀", 50);
        Employee employee10 = new Employee("河合", 50);
        Employee employee11 = new Employee("中西", 50);

        president.addSubordinate(manager1);
        president.addSubordinate(manager2);
        president.addSubordinate(employee10);

        manager1.addSubordinate(sectionChief1);
        manager1.addSubordinate(sectionChief2);
        manager1.addSubordinate(employee11);

        manager2.addSubordinate(sectionChief3);
        manager2.addSubordinate(sectionChief4);

        sectionChief1.addSubordinate(employee1);
        sectionChief1.addSubordinate(employee2);
        sectionChief1.addSubordinate(employee3);

        sectionChief2.addSubordinate(employee4);
        sectionChief2.addSubordinate(employee5);

        sectionChief3.addSubordinate(employee6);
        sectionChief3.addSubordinate(employee7);

        sectionChief4.addSubordinate(employee8);
        sectionChief4.addSubordinate(employee9);

        System.out.println(president.getEmployeeInfo());
    }
}
>> 実行結果
役職:社長、氏名:前田、給料:100、役員報酬:15
  役職:部長、氏名:田中、給料:90、役員報酬:10
    役職:係長、氏名:鈴木、給料:80、役員報酬:5
      役職:なし、氏名:斉藤、給料:50
      役職:なし、氏名:藤本、給料:50
      役職:なし、氏名:河村、給料:50
    役職:係長、氏名:中野、給料:80、役員報酬:5
      役職:なし、氏名:森田、給料:50
      役職:なし、氏名:大谷、給料:50
    役職:なし、氏名:中西、給料:50
  役職:部長、氏名:佐藤、給料:90、役員報酬:10
    役職:係長、氏名:藤井、給料:80、役員報酬:5
      役職:なし、氏名:山本、給料:50
      役職:なし、氏名:松井、給料:50
    役職:係長、氏名:荒木、給料:80、役員報酬:5
      役職:なし、氏名:藤浪、給料:50
      役職:なし、氏名:千賀、給料:50
  役職:なし、氏名:河合、給料:50

 実行結果は上記になります。少しわかりにくいですが、インデントで部下の関係がわかるかと思います。

 これで問題なく実装できました。一安心一安心。というわけにはいきません。このコードには大きな欠点があります。Object型を利用している点です。部下のリストにObject型を利用しているため、どんなObjectでも入ってしまいます。まったく社員情報と関係ないオブジェクトが入れられてしまう可能性もなくはないです。

 他にも問題はあります。例えば、新しく委託さんを部下に入れるようになったとしましょう。下記のようにコードを修正する必要があります。Objectがどのクラスの型なのかを判定するところに委託クラスかを判定して、それを全てのgetEmployeeInfo()のメソッドに適用して、、、、、めんどくさい!!

// 委託
public class Delegate {
    private String name;
    public Delegate(String name) {
        this.name = name;
    }
    public String getEmployeeInfo() {
        return "役職:委託、氏名:" + this.name + "\n";
    }
}

// 社長
public class President {
    // 省略
    public String getEmployeeInfo() {

        // 省略

        while (iterator.hasNext()) {
            Object obj = iterator.next();
            if (obj instanceof Manager manager) {
                builder.append("  " + manager.getEmployeeInfo());
            } else if (obj instanceof SectionChief sectionChief) {
                builder.append("  " + sectionChief.getEmployeeInfo());
            } else if (obj instanceof Employee employee) {
                builder.append("  " + employee.getEmployeeInfo());
            } else if (obj instanceof Delegate delegate) {  // 委託用のオブジェクト判定を追加
                builder.append("  " + delegate.getEmployeeInfo());
            } else {
                System.out.println("存在しない役職の社員です。");
            }
        }
        return builder.toString();
    }
}

 このようにObject型を使うにはリスクがあります。コードを追加しようにも、if文の判定条件をアホみたいに追加する必要も出てきます。このような階層構造を利用したい時に威力を発揮するのがCompositeパターンです。

⭕️ Compositeパターンを使用する例 ⭕️

まずは、どの役職の社員でもgetEmployeeInfo()のメソッドはひつようなため、このメソッドを持ったインターフェースを作ります。それぞれのクラスはそのインターフェースを実装して作ります。

// 全ての社員に共通するインターフェース
public interface EmployeeEntry {
    String getEmployeeInfo();
}

 このインターフェースを実装してそれぞれの役職クラスを作ります。

// 社員
public class Employee implements EmployeeEntry {

    private String name;
    private int salary;

    public Employee(String name, int salary) {
        this.name = name;
        this.salary = salary;
    }

    @Override
    public String getEmployeeInfo() {
        return "役職:なし、氏名:" + this.name + "、給料:" + this.salary + "\n";
    }
}

// 係長
public class SectionChief implements EmployeeEntry {

    private String name;
    private int salary;
    private int executiveCompensation;
    private List<EmployeeEntry> subordinate;


    public SectionChief(String name, int salary, int executiveCompensation) {
        this.name = name;
        this.salary = salary;
        this.executiveCompensation = executiveCompensation;
        this.subordinate = new ArrayList<>();
    }

    public void addSubordinate(EmployeeEntry employee) {
        subordinate.add(employee);
    }

    @Override
    public String getEmployeeInfo() {
        StringBuilder builder = new StringBuilder();
        String sectionChiefInfo = "役職:係長、氏名:" + this.name + "、給料:" + this.salary + "、役員報酬:" + this.executiveCompensation + "\n";
        builder.append(sectionChiefInfo);

        Iterator<EmployeeEntry> iterator = this.subordinate.iterator();
        while (iterator.hasNext()) {
            EmployeeEntry employee = iterator.next();
            builder.append("      " + employee.getEmployeeInfo());
        }
        return builder.toString();
    }
}

// 部長
public class Manager implements EmployeeEntry {
    // 省略

    @Override
    public String getEmployeeInfo() {
        StringBuilder builder = new StringBuilder();
        String managerInfo = "役職:部長、氏名:" + this.name + "、給料:" + this.salary + "、役員報酬:" + this.executiveCompensation + "\n";
        builder.append(managerInfo);

        Iterator<EmployeeEntry> iterator = this.subordinate.iterator();
        while (iterator.hasNext()) {
            EmployeeEntry employee = iterator.next();
            builder.append("    " + employee.getEmployeeInfo());
        }
        return builder.toString();
    }
}

// 社長
public class President implements EmployeeEntry{
    // 省略

    @Override
    public String getEmployeeInfo() {
        StringBuilder builder = new StringBuilder();
        String presidentInfo = "役職:社長、氏名:" + this.name + "、給料:" + this.salary + "、役員報酬:" + this.executiveCompensation + "\n";
        builder.append(presidentInfo);

        Iterator<EmployeeEntry> iterator = this.subordinate.iterator();
        while (iterator.hasNext()) {
            EmployeeEntry employee = iterator.next();
            builder.append("  " + employee.getEmployeeInfo());
        }
        return builder.toString();
    }
}

 部長と社長と係長は持っているフィールドが全て同じなため、省略しています。ここで確認して欲しいのがsubordinateのリストに入る型です。全てのクラスはEmployeeEntryクラスを実装するようにしていますから、ObjectではなくEmployeeEntryで問題ありません。これで全く関係のないオブジェクトがリスト中に入ることは防げました。

 さらにgetEmployeeInfo()のメソッドを見てください。ListがEmployeeEntryの型を格納しているため、EmployeeEntryのgetEmployeeInfoを呼び出すだけで問題なく動作します。どの役職のクラスでも型の判定を行う必要がなくなりました。仮に委託クラスを追加したとしてもEmployeeEntryを実装するように作れば、同じように動作します。

 mainクラスと実行結果はcompositeパターンを利用しない例と全く同一です。このように階層構造を表現する時に、一つのインターフェースを実装するようにすると違うクラスであっても、同じオブジェクトとして扱うことが可能になります。

まとめ

 本記事で利用したサンプルケースのクラス図は下記になります。

 「Composite」とは「入れ物のクラスと中身のクラスを、1つの抽象クラスでまとめて、それぞれのクラスから得られるオブジェクトを同一視できるようにする」です。特に階層構造を表現する時には重宝するデザインパターンです。うまく使っていきましょう。

 本記事で利用したコードは下記で公開しております。

https://github.com/tamotech1028/composite

 

 

最新の投稿

SNSでもご購読できます。

コメント

コメントを残す