Hatena::ブログ(Diary)

めざせ言語マスター このページをアンテナに追加 RSSフィード

2010-07-21

Decoratorパターン

目的

オブジェクトに責任を動的に追加する。

動機

一文を表示する責任を持ったクラスに、表示に装飾を施す責任を追加していきます。

f:id:knowledgetree:20100719215431p:image

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

    final public function show() {
        for ($i = 0, $m = $this->getRows(); $i < $m; $i++) {
            echo $this->getRowText($i) . '<br />';
        }
    }
}

class StringDisplay extends Display {
    private $_string;

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

    public function getColumns() {
        return strlen($this->_string);
    }

    public function getRows() {
        return 1;
    }

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

        return null;
    }
}

abstract class Border extends Display {
    protected $display;

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

class SideBorder extends Border {
    private $_borderChar;

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

    public function getColumns() {
        return $this->display->getColumns() + 2;
    }

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

    public function getRowText($row) {
        return $this->_borderChar
            . $this->display->getRowText($row)
            . $this->_borderChar;
    }
}

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

    public function getColumns() {
        return $this->display->getColumns() + 2;
    }

    public function getRows() {
        return $this->display->getRows() + 2;
    }

    public function getRowText($row) {
        if ($row === 0 or $row === ($this->display->getRows() + 1)) {
            return '+' . $this->_makeLine('-', $this->display->getColumns()) . '+';
        }

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

    private function _makeLine($ch, $count) {
        $str = '';

        for ($i = 0; $i < $count; $i++) {
            $str .= $ch;
        }

        return $str;
    }
}

以下クライアントコード。

<?php
require_once('string-display.php');
require_once('side-border.php');
require_once('full-border.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();

実行結果。

Hello, world.
#Hello, world.#
+---------------+
|#Hello, world.#|
+---------------+
/+------------+/
/|+----------+|/
/||*+------+*||/
/||*|こんにちは。|*||/
/||*+------+*||/
/|+----------+|/
/+------------+/
  1. Displayクラスは、複数行の文字列を表示する抽象クラスです。このときのshowメソッドはテンプレートメソッドとなっています。
  2. StringDisplayクラスは、Displayクラスの実装であり、1行の文字列を表示するという責任を持ったクラスです。
  3. StringDisplayクラスのインスタンス化時に文字列を渡すことによって、showメソッドを呼び出したときに文字列を表示することができます。
  4. この文字列表示機能を拡張して、文字列の左右に任意の文字を表示するという責任を持たせたものが、SideBorderクラスです。
  5. クライアントから、文字列表示機能($b1)の責任をSideBorderクラスに委譲します。
  6. また、任意の文字列を渡してインスタンス化($b2)し、showメソッドを呼び出せば、拡張された状態で表示されます。
  7. FullBorderクラスでは、委譲したオブジェクトが表示する文字列の周りに枠を表示するという責任を持っています。
  8. サンプルコードのように、表示する責任、左右に文字を付与する責任、枠を表示する責任それぞれを使い、様々な組み合わせで拡張できます。

このように、任意のクラスに対して、責任を動的に追加したり、取り外しを行ないたいときにDecoratorパターンが使えます。

たくさんの独立した拡張が予想され、他のオブジェクトに影響を与えず(透明)に拡張する必要があるときに有効的です。

構造

f:id:knowledgetree:20100720001205j:image

Component

責任を動的に追加できる側のオブジェクトインターフェース

ConcreteComponent

責任を追加できるオブジェクト

Decorator

Componentと一致したインターフェースを定義する

ConcreteDecorator

Componentに責任を追加するオブジェクト

利点

拡張が継承よりも柔軟

機能拡張は、継承することでも行えますが、Decoratorパターンではより柔軟に拡張ができます。

ConcreteDecoratorの組み合わせで様々な責任を作り出すことができるし、一つの責任の中で一つのConcreteDecoratorを再利用して複数回利用することも可能です。

継承で機能拡張を行う場合、責任を追加するたびにクラスを作成する必要があり、システムが複雑になっていきます。

ConcreteDecoratorを組み合わせて作り上げた責任ごとにクラスを作るイメージです。

必要最低限の責任を作ることができる

Decoratorパターンでは、責任を必要最低限の部品を組み合わせて作ることができるため、その責任で不必要な部分はありません。

Decoratorパターンを使わずに、汎用性の高い複雑なクラスを作っておく方法だと、それを使うたびに不必要な部分まで必然的に取り込むことになります。

また、そのクラスを拡張しようとすると、追加しようとしている責任とは無関係な部分まで見ることになり、混乱を招く結果となります。

インターフェースが透過的

ConcreteDecoratorでいくら中身(ConcreteComponent)を装飾していっても、インターフェースが隠れてしまうことはありません。

これは、Componentでメソッドが保証されているからです。

このような状態をインターフェースが透過的であると言えます。

透過的であり実行メソッドが全て同じなため、Compositeパターンの一例にあったような再帰的な構造にできます。

ただ、Decoratorパターンでは外枠に機能を追加していくことに重点を置いています。

欠点

decoratorと装飾されたcomponentは同一ではない

装飾されたcomponentはcomponent自体と同一ではないため、decoratorを利用するときは、装飾したオブジェクトに依存したコードは書かないようにしましょう。

似たような小さなオブジェクトがたくさんできる

ConcreteDecoratorは、Decoratorを実装しているため、それぞれのメソッドが同じなのは当然のこと、変数も同じである小さなオブジェクトがたくさんできてしまいます。

構造を理解していないとそれらのカスタマイズやデバッグが困難となります。

実装

抽象クラスDecoratorの省略

責任を一つしか追加する必要がないときには、抽象クラスDecoratorを定義する必要はありません。

Componentクラスを軽く保つ

Componentクラスではインターフェースの定義に焦点を当てます。

Componentクラスが複雑になると、decoratorが重くなりすぎて、多くのdecoratorを使うことができなくなってしまいます。

また、Componentクラスで多くの機能を定義すると、具象サブクラスで不必要な機能まで取り込むことになってしまいます。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/knowledgetree/20100721/1279716340