Compositeパターン

概要

一般的には「容器と中身を同一視する」パターンと言われています。
言い換えると、「容器と中身に同一の機能をもたせる」パターンです。

例題

一般的なフォルダ構造のようなItemを作ります。

CompositeItem(フォルダ)とSimpleItem(ファイル)するものを作ります。

サンプルコード

import abc
import sys


# Component
class AbstractItem(metaclass=abc.ABCMeta):
    @abc.abstractproperty
    def composite(self):
        pass

    def __iter__(self):
        return iter([])


# Leaf
class SimpleItem(AbstractItem):

    def __init__(self, name, price=0.00):
        self.name = name
        self.price = price

    def print(self, indent="", file=sys.stdout):
        print("{}${:.2f} {}".format(indent, self.price, self.name), file=file)

    @property
    def composite(self):
        return False


# Composite
class AbstractCompositeItem(AbstractItem):
    def __init__(self, *items):
        self.children = []
        if items:
            self.add(*items)

    def add(self, first, *items):
        self.children.append(first)
        if items:
            self.children.extend(items)

    def remove(self, item):
        self.children.remove(item)

    def __iter__(self):
        return iter(self.children)


# (Composite)
class CompositeItem(AbstractCompositeItem):
    def __init__(self, name, *items):
        super().__init__(*items)
        self.name = name

    def print(self, indent="", file=sys.stdout):
        print("{}${:.2f} {}".format(indent, self.price, self.name))
        for child in self:
            child.print(indent + " ")

    @property
    def composite(self):
        return True

    @property
    def price(self):
        return sum(item.price for item in self)


def main():
    ruler = SimpleItem("Ruler", 1.60)
    eraser = SimpleItem("Eraser", 0.20)
    pencil = SimpleItem("Pencil", 0.40)
    pencilset = CompositeItem("Pencil Set", pencil, eraser, ruler)

    box = SimpleItem("Box", 1.00)
    boxedpencilset = CompositeItem("Boxed Pencil Set", box, pencilset)
    boxedpencilset.add(pencil)

    for item in [pencil, ruler, eraser, pencilset, boxedpencilset]:
        item.print()


if __name__ == "__main__":
    main()

# 出力

$0.40 Pencil $1.60 Ruler $0.20 Eraser $2.20 Pencil Set $0.40 Pencil $0.20 Eraser $1.60 Ruler $3.60 Boxed Pencil Set $1.00 Box $2.20 Pencil Set $0.40 Pencil $0.20 Eraser $1.60 Ruler $0.40 Pencil

特徴

クラス図は以下のようになっています。

覚える点は3点だけ

- 容器と中身で共通の機能はAbstractItem(抽象クラス)へメソッドを追加 - 容器も中身もAbstractItemを継承する - 容器の機能は(Abstract)CompositeItemへメソッドを追加

この3点を覚えておけばCompositeは実装できると思います。

Compositeパターンが活躍するのは、中身の種類が増えたときです。

例えば、個数つきのItem(NumberingItems)などの新機能を追加する場合に、AbstractItemを継承し、 インターフェースを実装するだけでOKです。

Pythonらしく書くためのコツ

実践Python3では、すべてをItemクラスにまとめる方法が紹介されています。
Itemの種類が2種類くらいだったらこちらの方がわかりやすいと思います。

import sys
import itertools


def main():
    ruler = Item.create("Ruler", 1.60)
    pencil = Item.create("Pencil", 0.40)
    eraser = make_item("Eraser", 0.20)
    pencilset = Item.compose("Pencil Set", pencil, eraser, ruler)

    box = Item.create("Box", 1.00)
    boxedpencilset = make_composite("Boxed Pencil Set", box, pencilset)
    boxedpencilset.add(pencil)

    for item in [pencil, ruler, eraser, pencilset, boxedpencilset]:
        item.print()


class Item:

    def __init__(self, name, *items, price=0.00):
        self.name = name
        self.price = price
        self.children = []

        if items:
            self.add(*items)

    @classmethod
    def create(Class, name, price):
        return Item(name, price=price)

    @classmethod
    def compose(Class, name, *items):
        return Item(name, *items)

    @property
    def composite(self):
        return bool(self.children)

    @property
    def price(self):
        return (sum(item.price for item in self) if self.children else self.__price)

    @price.setter
    def price(self, price):
        self.__price = price

    def add(self, first, *items):
        self.children.extend(itertools.chain((first, ), items))

    def remove(self, item):
        self.children.remove(item)

    def __iter__(self):
        return iter(self.children)

    def print(self, indent="", file=sys.stdout):
        print("{}${:.2f} {}".format(indent, self.price, self.name), file=file)
        for child in self:
            child.print(indent + " ")


def make_item(name, price):
    return Item(name, price=price)


def make_composite(name, *items):
    return Item(name, *items)


if __name__ == "__main__":
    main()

クラス図

# 一般的なクラス図

一般的なクラス図(Compositeパターン)

# サンプルコードにおけるクラス図

サンプルコードにおけるクラス図(Compositeパターン)

# サンプルコードにおけるクラス図

Pythonicなサンプルコードにおけるクラス図(Compositeパターン)

参考URL

Compositeパターン - TECHSCORE

Compositeパターン - Wikipedia