PHP7でデザインパターン入門12/23 Decoratorパターン

Decorator パターンとは

このパターンは、既存のオブジェクトに新しい機能や振る舞いを動的に追加することを可能にする。

Decorator パターン - Wikipedia

スポンジケーキが1つあるとする。クリームを塗れば何も載っていないショートケーキができあがる。いちごを並べれば、ストロベリーショートケーキができあがる。いずれにおいてもベースとなるのはスポンジケーキである。様々なデコレーションを施すことで、様々な目的にあったケーキとなる。

オブジェクトもこのような性質がある。まず中心となるベースオブジェクトがある。それに飾り付けとなる機能を一皮一皮かぶせていくことで、より目的にあったオブジェクトに仕上げていく。このようなオブジェクトにデコレーションを施していくようなデザインパターンをDecoratorパターンと呼ぶ。

example

文字列の周りへ飾り枠をつけて表示するサンプルコード。

Display: 文字列表示用抽象クラス

<?php
abstract class Display
{
    abstract public function getColumns(): int;
    abstract public function getRows(): int;
    abstract public function getRowText(int $row);

    public function show()
    {
        for ($i = 0; $i < $this->getRows(); $i++) {
            echo $this->getRowText($i) . PHP_EOL;
        }
    }
}

StringDisplay: 1行だけからなる文字列表示用クラス

<?php
class StringDisplay extends Display
{
    private $string;

    public function __construct(string $string)
    {
        $this->string = $string;
    }

    public function getColumns(): int
    {
        return strlen($this->string);
    }

    public function getRows(): int
    {
        return 1;
    }

    public function getRowText(int $row)
    {
        if ($row === 0) {
            return $this->string;
        }

        return null;
    }
}
~

Border: 「飾り枠」を表す抽象クラス

<?php
abstract class Border extends Display
{
    protected $display;

    protected function __construct(Display $display)
    {
        $this->display = $display;
    }
}

SideBorder: 左右にのみ飾り枠をつけるクラス

<?php
class SideBorder extends Border
{
    private $borderChar;

    public function __construct(Display $display, string $ch)
    {
        parent::__construct($display);
        $this->borderChar = $ch;
    }

    public function getColumns(): int
    {
        return 1 + $this->display->getColumns() + 1;
    }

    public function getRows(): int
    {
        return $this->display->getRows();
    }

    public function getRowText(int $row): string
    {
        return $this->borderChar . $this->display->getRowText($row) . $this->borderChar;
    }
}

FulBorder: 上下左右に飾り枠をつけるクラス

<?php
class FullBorder extends Border
{
    public function __construct(Display $display)
    {
        parent::__construct($display);
    }

    public function getColumns(): int
    {
        return 1 + $this->display->getColumns() + 1;
    }

    public function getRows(): int
    {
        return 1 + $this->display->getRows() + 1;
    }

    public function getRowText(int $row): string
    {
        if ($row === 0) {
            return "+" . $this->makeLine('-', $this->display->getColumns()) . "+";
        } elseif ($row === $this->display->getRows() + 1) {
            return "+" . $this->makeLine('-', $this->display->getColumns()) . "+";
        }

        return "|" . $this->display->getRowText($row - 1) . "|";
    }

    private function makeLine(string $ch, int $count): string
    {
        $buf = "";
        for ($i = 0; $i < $count; $i++) {
            $buf.= $ch;
        }

        return $buf;
    }

Main

<?php
$b1 = new StringDisplay("Hello, world.");
$b2 = new SideBorder($b1, '#');
$b3 = new FullBorder($b2);
$b1->show();
$b2->show();
$b3->show();
$b4 = new SideBorder(
    new FullBorder(
        new FullBorder(
            new SideBorder(
                new FullBorder(
                    new StringDisplay("こんにちは。")
                ),
               '*'
            )
        )
    ),
    '/'
);
$b4->show();

実行結果

$ php Main.php
Hello, world.
#Hello, world.#
+---------------+
|#Hello, world.#|
+---------------+
/+-------------------+/
/|+-----------------+|/
/||*+-------------+*||/
/||*|Hello, world!|*||/
/||*+-------------+*||/
/|+-----------------+|/
/+-------------------+/

ソースコード

github.com

役割

Component

機能を追加するときの核となる役。デコレーションする前のスポンジケーキ(ベース)部分にあたる。インターフェースだけを定める。サンプルコードにおけるDisplayクラス。

ConcreteComponent

具体的な実装をしているスポンジケーキ。サンプルコードにおけるStringDisplayクラス。

Decorator

Component役と同じインターフェースを持つ。さらに、Decorator役が飾る対象となるComponentを持つ。この役は、自分が飾っている対象を知っている。サンプルコードにおけるBorderクラス。

ConcreteDecorator

具体的なDecoratorの役。サンプルコードにおけるSideBorder, FullBorderクラス。

まとめ

透過的インターフェース

  • Decoratorパターンでは飾り枠と中身を同一視している。サンプルコードでは、飾り枠を表すBorderクラスが、中身を表すDisplayクラスのサブクラスになっているところで、その同一視が表現されている。
  • 飾り枠を使って中身を包んでも、インターフェースは「透過的」(隠れていない状態)であるため、飾り枠をたくさん使って包んでも、インターフェースは全く変更されない。

包まれるもの(中身)を変えずに機能追加ができる

Decoratorパターンでは、委譲が使われている。飾り枠に対してやってきた要求はその中身にたらい回し(委譲)される。

動的な機能追加ができる

委譲でクラス間をゆるやかに結合しているため。

関連パターン

増補改訂版Java言語で学ぶデザインパターン入門

増補改訂版Java言語で学ぶデザインパターン入門