構造に関するパターン
Composite
*容器と中身を同一視して再帰的構造を作る*
概要
再帰。
親と子に同じ振る舞いや属性を持たせる
マンガでわかる Composite
マンガでわかる Composite #デザインパターン - Qiita
でざぱたんで覚える Composite
ちびキャラは「コンポジットたん」。「耳がついていて4つ足の生き物は猫」というルールだけを頼りに、鼠にも犬にも「にゃー」と鳴く猫耳ホムンクルス。単体(Leaf)でも入れ子(Composite)でも、同じインターフェースなら同じに扱う——器と中身の同一視がCompositeで、本書は「擬人化パターンと覚えておくと良い」とまとめている。
出典: いしだけ『でざぱたん: ちびキャラで覚えるデザインパターン』(P.178〜)
登場人物
- Leaf: 中身。ファイル
- Composite: 複合体。容器を表す。ディレクトリ
- Component: 中身と複合体を同一とみなすためのスーパークラス
- Client: 利用者
クラス図
このサイトの実装(ファイルシステムの例)での対応関係:
classDiagram
class Entry {
<<abstract>>
+getName() String
+getSize() int
+add(entry) Entry
+printList()
#printList(prefix)
+toString() String
}
class File {
-name String
-size int
+getName() String
+getSize() int
#printList(prefix)
}
class Directory {
-name String
-directory ArrayList
+getName() String
+getSize() int
+add(entry) Entry
#printList(prefix)
}
class FileTreatmentException
Entry <|-- File
Entry <|-- Directory
Directory o-- Entry
Entry ..> FileTreatmentException
やり方
- 親にも子にも同一の抽象クラスを定義させる
- 親が子(または子の一覧)をプロパティに持てば良い
注意点
- データ構造が必ず木構造になること。
- 循環したら再帰により無限ループが発生する
ユースケース
ファイルシステム
ファイルとディレクトリ(まとめてディレクトリエントリという)
HTMLの構文解析
Java
Entry.java
public abstract class Entry {
public abstract String getName(); // 名前を得る
public abstract int getSize(); // サイズを得る
public Entry add(Entry entry) throws FileTreatmentException { // エントリを追加する
throw new FileTreatmentException();
}
public void printList() { // 一覧を表示する
printList("");
}
protected abstract void printList(String prefix); // prefixを前につけて一覧を表示する
public String toString() { // 文字列表現
return getName() + " (" + getSize() + ")";
}
}
File.java
public class File extends Entry {
private String name;
private int size;
public File(String name, int size) {
this.name = name;
this.size = size;
}
public String getName() {
return name;
}
public int getSize() {
return size;
}
protected void printList(String prefix) {
System.out.println(prefix + "/" + this);
}
}
Directory.java
import java.util.Iterator;
import java.util.ArrayList;
public class Directory extends Entry {
private String name; // ディレクトリの名前
private ArrayList directory = new ArrayList(); // ディレクトリエントリの集合
public Directory(String name) { // コンストラクタ
this.name = name;
}
public String getName() { // 名前を得る
return name;
}
public int getSize() { // サイズを得る
int size = 0;
Iterator it = directory.iterator();
while (it.hasNext()) {
Entry entry = (Entry)it.next();
size += entry.getSize();
}
return size;
}
public Entry add(Entry entry) { // エントリの追加
directory.add(entry);
return this;
}
protected void printList(String prefix) { // エントリの一覧
System.out.println(prefix + "/" + this);
Iterator it = directory.iterator();
while (it.hasNext()) {
Entry entry = (Entry)it.next();
entry.printList(prefix + "/" + name);
}
}
}
FileTreatmentException.java
public class FileTreatmentException extends RuntimeException { // RuntimeExceptionで正しいか?
public FileTreatmentException() {
}
public FileTreatmentException(String msg) {
super(msg);
}
}
Main.java
public class Main {
public static void main(String[] args) {
try {
System.out.println("Making root entries...");
Directory rootdir = new Directory("root");
Directory bindir = new Directory("bin");
Directory tmpdir = new Directory("tmp");
Directory usrdir = new Directory("usr");
rootdir.add(bindir);
rootdir.add(tmpdir);
rootdir.add(usrdir);
bindir.add(new File("vi", 10000));
bindir.add(new File("latex", 20000));
rootdir.printList();
System.out.println("");
System.out.println("Making user entries...");
Directory yuki = new Directory("yuki");
Directory hanako = new Directory("hanako");
Directory tomura = new Directory("tomura");
usrdir.add(yuki);
usrdir.add(hanako);
usrdir.add(tomura);
yuki.add(new File("diary.html", 100));
yuki.add(new File("Composite.java", 200));
hanako.add(new File("memo.tex", 300));
tomura.add(new File("game.doc", 400));
tomura.add(new File("junk.mail", 500));
rootdir.printList();
} catch (FileTreatmentException e) {
e.printStackTrace();
}
}
}
Go
Entryはinterfaceで表現。Javaのprotectedな共通実装(printList(), toString())は、GoではPrintList(e Entry)のような自由関数に切り出している(FactoryMethod版のCreate()と同じ考え方)。
実行: go run ./GoF/patterns/Composite/go
$ go run ./GoF/patterns/Composite/go
entry.go
package main
import (
"errors"
"fmt"
)
// ---- entry 層(Java版の Entry.java / FileTreatmentException.java に相当)----
// ErrFileTreatment はJava版 FileTreatmentException 相当。
// 子を持てないEntry(File)に対してAdd()しようとしたときに返す。
var ErrFileTreatment = errors.New("this entry cannot contain other entries")
// Entry はファイルシステムのエントリ(File/Directory共通)を表す抽象。
//
// Java版のEntryは抽象“クラス”で、add()やprintList()(引数なし)に共通のデフォルト実装を
// 持つ。Goのinterfaceはメソッド本体を持てないため、そうした共通デフォルトは
// FactoryMethod/go版のCreate()と同じ流儀で「Entryを受け取る自由関数」に外出しする
// (下のPrintList/entryStringを参照)。
//
// printListだけ非公開メソッド(小文字始まり)にしてある。Java版のprotected abstractに
// 相当させる意図で、パッケージ外からEntryを実装できないよう縛っている
// (Goでは同一パッケージ内でしか非公開メソッドを実装できないので、事実上“sealed”になる)。
type Entry interface {
Name() string // 名前を得る
Size() int // サイズを得る
Add(entry Entry) (Entry, error) // エントリを追加する。Fileはデフォルトでエラーを返す
printList(prefix string) // prefixを前につけて一覧を表示する
}
// PrintList はJava版 Entry.printList()(引数なし・公開)相当のテンプレートメソッド。
// 「先頭のprefixは空文字から始める」という決め事だけをここで固定し、
// 実際の表示ロジックはEntry.printList(prefix)(各具象型の実装)に委ねる。
func PrintList(e Entry) {
e.printList("")
}
// entryString はJava版 Entry.toString() 相当の共通ロジック。
// File/Directoryどちらの String() もこれを呼ぶだけにする。
func entryString(e Entry) string {
return fmt.Sprintf("%s (%d)", e.Name(), e.Size())
}
file.go
package main
import "fmt"
// ---- file 層(Java版の File.java に相当)----
// File はEntryのConcreteComponent(Leaf = 子を持てない末端)。
type File struct {
name string
size int
}
// NewFile はコンストラクタ相当。
func NewFile(name string, size int) *File {
return &File{name: name, size: size}
}
func (f *File) Name() string { return f.name }
func (f *File) Size() int { return f.size }
// Add はJava版で「Fileはadd()をオーバーライドしない」=Entryのデフォルト実装
// (常に例外)をそのまま使う、を明示的に書き下したもの。
// Goには継承のデフォルトメソッドが無いので、「Fileにaddしたら常にエラー」を
// ここに書くしかない。
func (f *File) Add(entry Entry) (Entry, error) {
return nil, ErrFileTreatment
}
func (f *File) printList(prefix string) {
fmt.Println(prefix + "/" + f.String())
}
// String は fmt.Stringer を満たす(Java版 toString() 相当)。
func (f *File) String() string {
return entryString(f)
}
directory.go
package main
import "fmt"
// ---- directory 層(Java版の Directory.java に相当)----
// Directory はEntryのConcreteComponent(Composite = 子Entryの集合を持てる)。
type Directory struct {
name string
entries []Entry
}
// NewDirectory はコンストラクタ相当。
func NewDirectory(name string) *Directory {
return &Directory{name: name}
}
func (d *Directory) Name() string { return d.name }
// Size は自分が抱える全エントリのサイズ合計(再帰的に集計される)。
func (d *Directory) Size() int {
total := 0
for _, e := range d.entries {
total += e.Size()
}
return total
}
// Add はエントリの追加。Java版と同様、追加後は自分自身を返す。
func (d *Directory) Add(entry Entry) (Entry, error) {
d.entries = append(d.entries, entry)
return d, nil
}
func (d *Directory) printList(prefix string) {
fmt.Println(prefix + "/" + d.String())
for _, e := range d.entries {
e.printList(prefix + "/" + d.name)
}
}
// String は fmt.Stringer を満たす(Java版 toString() 相当)。
func (d *Directory) String() string {
return entryString(d)
}
main.go
package main
import "fmt"
// 実行: go run ./GoF/patterns/Composite/go
//
// Java版Main.javaと同じツリー(root/bin,tmp,usr以下)を組み立てて2回表示する。
// 末尾に「Fileにadd()するとエラーになる」ことの確認を追加している
// (Java版はtry/catchで全体を囲むだけで、実際にはサンプル中で例外を踏まない)。
func main() {
fmt.Println("Making root entries...")
rootdir := NewDirectory("root")
bindir := NewDirectory("bin")
tmpdir := NewDirectory("tmp")
usrdir := NewDirectory("usr")
mustAdd(rootdir, bindir)
mustAdd(rootdir, tmpdir)
mustAdd(rootdir, usrdir)
vi := NewFile("vi", 10000)
mustAdd(bindir, vi)
mustAdd(bindir, NewFile("latex", 20000))
PrintList(rootdir)
fmt.Println()
fmt.Println("Making user entries...")
yuki := NewDirectory("yuki")
hanako := NewDirectory("hanako")
tomura := NewDirectory("tomura")
mustAdd(usrdir, yuki)
mustAdd(usrdir, hanako)
mustAdd(usrdir, tomura)
mustAdd(yuki, NewFile("diary.html", 100))
mustAdd(yuki, NewFile("Composite.java", 200))
mustAdd(hanako, NewFile("memo.tex", 300))
mustAdd(tomura, NewFile("game.doc", 400))
mustAdd(tomura, NewFile("junk.mail", 500))
PrintList(rootdir)
fmt.Println()
fmt.Println("Trying to add a file under a file (should fail)...")
if _, err := vi.Add(NewFile("evil", 1)); err != nil {
fmt.Println("error:", err)
}
}
// mustAdd はサンプルツリーの組み立て専用ヘルパー。ここで呼ぶAddは全部Directoryへの
// 追加なので本来失敗しないが、Entry.Add()が返すエラーを黙って握りつぶさず一応チェックする。
func mustAdd(parent Entry, child Entry) {
if _, err := parent.Add(child); err != nil {
panic(err)
}
}
PHP
index.php
<?php
ini_set("display_errors", "1");
ini_set("safe_mode", 1);
/**
* composite: 複合
* 入れ子構造に同じインタフェースを実装して
* 同一に扱う
*
*/
interface Removable
{
public function remove();
}
class RemovableDirectory implements Removable
{
private $path;
public function __construct($path)
{
if (!is_dir($path)) {
throw new Exception("ディレクトリではありません");
}
$this->path = preg_replace("#/$#", "", $path);
}
public function getList()
{
$result = array();
$d = opendir($this->path);
while ($f = readdir($d)) {
if (preg_match("#^\.+$#", $f)) {
continue;
}
$path = $this->path . "/" . $f;
if (is_file($path)) {
$result[] = new File($path);
} elseif (is_dir($path)) {
$result[] = new RemovableDirectory($path);
} else {
throw new Exception("ファイルでもディレクトリでもありません");
}
}
return $result;
}
/**
* getList("tmp/composite")
* opendir("tmp/composite")
* $f = tmp/composite/hoge
* $f = tmp/composite/hoge
*
* @return void
*/
public function remove()
{
$result = array();
var_dump($this->getList());
foreach ($this->getList() as $obj) {//$obj = RemovableDirectory()
foreach ($obj->remove() as $log) {
$result[] = $log;
}
$result[] = "D=>" . $this->path;
rmdir($this->path);
return $result;
}
}
public function makeDirectory($name)
{
mkdir($this->path . "/" . $name);
}
}
class File implements Removable
{
private $path;
public function __construct($path)
{
if (!is_file($path)) {
throw new Exception("ファイルではありません");
}
$this->path = preg_replace("#/$#", "", $path);
}
public function remove()
{
unlink($this->path);
return array("F=>" . $this->path);
}
public function saveFile($name, $data)
{
file_put_contents($this->path . "/" . $name, $data);
}
}
TypeScript
abstract class Entryに共通のadd()/printList()を実装し、サブクラスだけが実装すべき部分を抽象メソッドにする、というJava版そのままの設計。
実行: npx tsx GoF/patterns/Composite/typescript/main.ts
$ npx tsx GoF/patterns/Composite/typescript/main.ts
entry.ts
// Composite パターン: エントリ層 (Java版の Entry.java / FileTreatmentException.java 相当)
// 単体では実行不可。エントリポイントは main.ts (npx tsx main.ts)。
//
// Java版のEntryは抽象クラスで、add()とprintList()(引数なし)に共通のデフォルト実装を
// 持つ。TypeScriptにもabstractクラスがあるので、FactoryMethod版TS実装と同じ方針で
// Javaのクラス階層をほぼそのまま書ける(Go版のように共通デフォルトを自由関数へ
// 外出しする必要がない)。
//
// ただしJava版はprintList()(公開・引数なし)とprintList(String prefix)(protected)を
// 同名オーバーロードで表現しているが、TypeScriptは可視性の異なるオーバーロードを
// 許さないため、prefix付きの方はprintListRecという別名にした。
// また、prefix付きの方はDirectory.printListRec内で「Entry型を経由して」他のインスタンス
// (子のFile/Directory)へアクセスする必要があるが、TypeScriptのprotectedは
// 「基底クラス型を経由したアクセス」を許さない(Javaより厳格)ため、ここではpublicにした。
// FileTreatmentError はJava版 FileTreatmentException 相当。
export class FileTreatmentError extends Error {
constructor(message = "this entry cannot contain other entries") {
super(message);
this.name = "FileTreatmentError";
}
}
export abstract class Entry {
abstract getName(): string;
abstract getSize(): number;
// Java版Entry.add()のデフォルト実装(常に例外)相当。Directoryだけがoverrideする。
add(_entry: Entry): Entry {
throw new FileTreatmentError();
}
// Java版Entry.printList()(引数なし・公開)相当のテンプレートメソッド。
// 「先頭のprefixは空文字から始める」という決め事だけをここで固定する。
printList(): void {
this.printListRec("");
}
// Java版 protected abstract printList(String prefix) 相当。
abstract printListRec(prefix: string): void;
// Java版 toString() 相当。
toString(): string {
return `${this.getName()} (${this.getSize()})`;
}
}
file.ts
// File層 (Java版の File.java 相当)
// 単体では実行不可。エントリポイントは main.ts (npx tsx main.ts)。
import { Entry } from "./entry";
// File: EntryのConcreteComponent(Leaf = 子を持てない末端)。
export class File extends Entry {
constructor(
private readonly name: string,
private readonly size: number,
) {
super();
}
getName(): string {
return this.name;
}
getSize(): number {
return this.size;
}
// add()はoverrideしない = Entryのデフォルト実装(常に例外)をそのまま使う。
// Java版で「Fileがadd()をオーバーライドしていない」のと同じ意図。
printListRec(prefix: string): void {
console.log(`${prefix}/${this}`);
}
}
directory.ts
// Directory層 (Java版の Directory.java 相当)
// 単体では実行不可。エントリポイントは main.ts (npx tsx main.ts)。
import { Entry } from "./entry";
// Directory: EntryのConcreteComponent(Composite = 子Entryの集合を持てる)。
export class Directory extends Entry {
private readonly entries: Entry[] = [];
constructor(private readonly name: string) {
super();
}
getName(): string {
return this.name;
}
// 自分が抱える全エントリのサイズ合計(再帰的に集計される)。
getSize(): number {
return this.entries.reduce((total, entry) => total + entry.getSize(), 0);
}
// エントリの追加。Java版と同様、追加後は自分自身を返す。
add(entry: Entry): Entry {
this.entries.push(entry);
return this;
}
printListRec(prefix: string): void {
console.log(`${prefix}/${this}`);
for (const entry of this.entries) {
entry.printListRec(`${prefix}/${this.name}`);
}
}
}
main.ts
// Composite パターン: ファイルシステムのツリー構造 (Java版Main.javaと同じお題)
//
// 実行: npx tsx GoF/patterns/Composite/typescript/main.ts
//
// Java版と同じツリー(root/bin,tmp,usr以下)を組み立てて2回表示する。
// 末尾に「Fileにadd()するとエラーになる」ことの確認を追加している
// (Java版はtry/catchで全体を囲むだけで、実際にはサンプル中で例外を踏まない)。
import { Directory } from "./directory";
import { FileTreatmentError } from "./entry";
import { File } from "./file";
function main(): void {
console.log("Making root entries...");
const rootdir = new Directory("root");
const bindir = new Directory("bin");
const tmpdir = new Directory("tmp");
const usrdir = new Directory("usr");
rootdir.add(bindir);
rootdir.add(tmpdir);
rootdir.add(usrdir);
const vi = new File("vi", 10000);
bindir.add(vi);
bindir.add(new File("latex", 20000));
rootdir.printList();
console.log("");
console.log("Making user entries...");
const yuki = new Directory("yuki");
const hanako = new Directory("hanako");
const tomura = new Directory("tomura");
usrdir.add(yuki);
usrdir.add(hanako);
usrdir.add(tomura);
yuki.add(new File("diary.html", 100));
yuki.add(new File("Composite.java", 200));
hanako.add(new File("memo.tex", 300));
tomura.add(new File("game.doc", 400));
tomura.add(new File("junk.mail", 500));
rootdir.printList();
console.log("");
console.log("Trying to add a file under a file (should fail)...");
try {
vi.add(new File("evil", 1));
} catch (e) {
if (e instanceof FileTreatmentError) {
console.log(`error: ${e.message}`);
} else {
throw e;
}
}
}
main();
Python
Entry(ABC)にJava版と同じ形で共通実装を持たせる。
実行: python3 GoF/patterns/Composite/python/main.py
$ python3 GoF/patterns/Composite/python/main.py
entry.py
"""Composite パターン: エントリ層 (Java版の Entry.java / FileTreatmentException.java 相当)
単体では実行不可。エントリポイントは main.py (python3 main.py)。
Java版のEntryは抽象クラスで、add()とprintList()(引数なし)に共通のデフォルト実装を持つ。
PythonにはJavaのinterfaceに相当する言語機能はないため、FactoryMethod版Python実装と
同じ方針で抽象基底クラス(ABC, abcモジュール)を使う。
Java版はprintList()(引数なし・公開)とprintList(String prefix)(protected)を同名の
オーバーロードで表現しているが、Pythonのクラスは同名メソッドを1つしか持てないため、
prefix付きの方は_print_list_recという別名にし、先頭アンダースコアでprotected相当を表す
(FactoryMethod版の_create_product等と同じ慣習)。
"""
from __future__ import annotations
from abc import ABC, abstractmethod
class FileTreatmentError(Exception):
"""Java版 FileTreatmentException 相当。"""
def __init__(self, message: str = "this entry cannot contain other entries") -> None:
super().__init__(message)
class Entry(ABC):
@abstractmethod
def get_name(self) -> str: ...
@abstractmethod
def get_size(self) -> int: ...
def add(self, entry: Entry) -> Entry:
"""Java版Entry.add()のデフォルト実装(常に例外)相当。Directoryだけがoverrideする。"""
raise FileTreatmentError()
def print_list(self) -> None:
"""Java版Entry.printList()(引数なし・公開)相当のテンプレートメソッド。
「先頭のprefixは空文字から始める」という決め事だけをここで固定する。
"""
self._print_list_rec("")
@abstractmethod
def _print_list_rec(self, prefix: str) -> None: ...
def __str__(self) -> str:
"""Java版 toString() 相当。"""
return f"{self.get_name()} ({self.get_size()})"
file.py
"""File層 (Java版の File.java 相当)
単体では実行不可。エントリポイントは main.py (python3 main.py)。
"""
from __future__ import annotations
from entry import Entry
class File(Entry):
"""File: EntryのConcreteComponent(Leaf = 子を持てない末端)。"""
def __init__(self, name: str, size: int) -> None:
self._name = name
self._size = size
def get_name(self) -> str:
return self._name
def get_size(self) -> int:
return self._size
# add()はoverrideしない = Entryのデフォルト実装(常に例外)をそのまま使う。
# Java版で「Fileがadd()をオーバーライドしていない」のと同じ意図。
def _print_list_rec(self, prefix: str) -> None:
print(f"{prefix}/{self}")
directory.py
"""Directory層 (Java版の Directory.java 相当)
単体では実行不可。エントリポイントは main.py (python3 main.py)。
"""
from __future__ import annotations
from entry import Entry
class Directory(Entry):
"""Directory: EntryのConcreteComponent(Composite = 子Entryの集合を持てる)。"""
def __init__(self, name: str) -> None:
self._name = name
self._entries: list[Entry] = []
def get_name(self) -> str:
return self._name
def get_size(self) -> int:
"""自分が抱える全エントリのサイズ合計(再帰的に集計される)。"""
return sum(entry.get_size() for entry in self._entries)
def add(self, entry: Entry) -> Entry:
"""エントリの追加。Java版と同様、追加後は自分自身を返す。"""
self._entries.append(entry)
return self
def _print_list_rec(self, prefix: str) -> None:
print(f"{prefix}/{self}")
for entry in self._entries:
entry._print_list_rec(f"{prefix}/{self._name}")
main.py
"""Composite パターン: ファイルシステムのツリー構造 (Java版Main.javaと同じお題)
実行: python3 main.py
(もしくはリポジトリルートから python3 GoF/patterns/Composite/python/main.py)
Java版と同じツリー(root/bin,tmp,usr以下)を組み立てて2回表示する。末尾に
「Fileにadd()するとエラーになる」ことの確認を追加している(Java版はtry/exceptで
全体を囲むだけで、実際にはサンプル中で例外を踏まない)。
"""
from __future__ import annotations
from directory import Directory
from entry import FileTreatmentError
from file import File
def main() -> None:
print("Making root entries...")
rootdir = Directory("root")
bindir = Directory("bin")
tmpdir = Directory("tmp")
usrdir = Directory("usr")
rootdir.add(bindir)
rootdir.add(tmpdir)
rootdir.add(usrdir)
vi = File("vi", 10000)
bindir.add(vi)
bindir.add(File("latex", 20000))
rootdir.print_list()
print()
print("Making user entries...")
yuki = Directory("yuki")
hanako = Directory("hanako")
tomura = Directory("tomura")
usrdir.add(yuki)
usrdir.add(hanako)
usrdir.add(tomura)
yuki.add(File("diary.html", 100))
yuki.add(File("Composite.java", 200))
hanako.add(File("memo.tex", 300))
tomura.add(File("game.doc", 400))
tomura.add(File("junk.mail", 500))
rootdir.print_list()
print()
print("Trying to add a file under a file (should fail)...")
try:
vi.add(File("evil", 1))
except FileTreatmentError as e:
print(f"error: {e}")
if __name__ == "__main__":
main()