本文へスキップ
BecomeCoder

Javaデザインパターンコース · 第3章 構造に関するパターン ― オブジェクトの「組み合わせ方」を工夫する · レッスン8

Composite ― 全体と部分を同じ操作で扱う

ブラウザで完結

導入

フォルダの中にファイルもあれば、さらにフォルダも入っている——そんな「木構造」を扱うとき、「これは1つのファイルか、それともフォルダか」をいちいち判定するコードは煩雑になります。Composite(コンポジット) は、単体」と「集まり」を同じインタフェースで扱えるようにする パターンです。

説明

FileItem(単体のファイル)と Folder(複数の要素をまとめるもの)の両方が、同じ FileSystemItem インタフェースを実装します。呼び出し側は相手が単体か集まりかを気にせず、同じメソッドを呼ぶだけで済みます。

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        FileItem file1 = new FileItem("読書レポート.txt", 20);
        FileItem file2 = new FileItem("写真.jpg", 500);

        Folder sub = new Folder("resources");
        sub.add(file2);

        Folder root = new Folder("work");
        root.add(file1);
        root.add(sub);

        System.out.println(root.getName() + ": " + root.getSize() + "KB");
    }
}

interface FileSystemItem {
    String getName();
    int getSize();
}

class FileItem implements FileSystemItem {
    private String name;
    private int size;

    FileItem(String name, int size) {
        this.name = name;
        this.size = size;
    }

    public String getName() {
        return name;
    }

    public int getSize() {
        return size;
    }
}

class Folder implements FileSystemItem {
    private String name;
    private List<FileSystemItem> children = new ArrayList<>();

    Folder(String name) {
        this.name = name;
    }

    void add(FileSystemItem item) {
        children.add(item);
    }

    public String getName() {
        return name;
    }

    public int getSize() {
        int total = 0;
        for (FileSystemItem child : children) {
            total += child.getSize();
        }
        return total;
    }
}
classDiagram
  FileSystemItem <|.. FileItem
  FileSystemItem <|.. Folder
  Folder --> "many" FileSystemItem : children
  class FileSystemItem {
    <<interface>>
    +getName() String
    +getSize() int
  }
  • FileItem(葉)と Folder(枝)は、どちらも FileSystemItem を実装します。
  • FolderList<FileSystemItem> children を持ち、その中身は FileItem でも別の Folder でも構いません。Folder の中に Folder を入れられるのが「木構造」たるゆえんです。
  • Folder.getSize() は、子どもたち一人ひとりの getSize() を呼んで合計するだけです。子が FileItem でも Folder でも同じ getSize() という呼び方で済みます。subresources フォルダ)自身も getSize() を持つので、root から見れば「ただの1つの FileSystemItem」として扱えます。
  • root.getSize()file1(20)+ sub.getSize()file2 の500)= 520KBのように、再帰的に足し合わさります。

やってみよう

下のエディタを実行し、work フォルダの合計サイズが520KBになることを確認しましょう。sub の中にさらに別の Folder を追加して、3階層にしても正しく合計されるか試してみてください。

演習

resources フォルダに 動画.mp4(1200KB)の FileItem をもう1つ追加してください。root.getSize() を表示すると合計がいくつになるか確認してください。

ヒント1を見る

FileItem file3 = new FileItem("動画.mp4", 1200); を作り、sub.add(file3);root.add(sub); より前に呼びます。

ヒント2を見る

合計は 20(file1) + 500(file2) + 1200(file3) = 1720KBです。

実際に動かしてみよう

下のエディタにJavaを書いて「実行」を押すと、学習用シミュレータが System.out.println の出力を表示します(本物のJVMではなく、教材の範囲を再現した軽量エンジンです)。クラス・継承・コレクションなども動きます。Scanner で入力を読む例は「標準入力」欄に値を入れてください。本文の例を書き換えて動かしてみましょう(Stream・ラムダ・スレッド・ファイル入出力など一部の機能は対象外です)。

Java — ブラウザ内で実行(学習用シミュレータ)

Javaの教材サブセットを動かす学習用シミュレータを読み込みます(本物のJVMではなく、動きを再現した軽量な自作エンジンです)。
スクロールして表示された時点でも自動で読み込まれます。