【PHPで学ぶデザインパターン入門】第2回 Decoratorパターン

【PHPで学ぶデザインパターン入門】第2回 Decoratorパターン

王

こんにちは、エンジニアの王です。

前回の「【PHPで学ぶデザインパターン入門】第1回 Strategyパターン」に引き続き、今回は「Decorator」 パターンについて説明していきます。ちょっとややこしいのですが、こちらもかなり実用的なパターンです。

では、見ていきましょう!

継承の代替手段

「Strategy パターン」と同じく、「Decorator パターン」もまた、継承の代替手段として用いられます。そして、継承のしすぎ問題を解決する一つの手法でもあります。

今回は、「iMacの値段を算出して、付属品一覧を表示するプログラムを作る」という例で考えてみましょう。

ここでは分かりやすいように、モデルを「21.5インチ」に限定します。
しかし、一口にiMacとは言っても、スペック次第で値段が変わってきますよね? スペックだけでなく付属品の組み合わせ次第でも値段が変化していると思います。

ここでよくありがちなのは、iMac本体を基底クラスにして継承を使い、各組み合わせごとにサブクラスを作ることです。

UML1

図で表すとこんなグチャグチャな感じになるでしょうか・・・。

しかし、そんなことをやってたら、組み合わせのパターン数だけ膨大な数のクラスができてしまいます。
仮にiMacの付属品の数が20個だったら、全部でなんと1,048,575パターン!

これではクラス爆発して話にならないです。
 

ではどうすればいいのかというと、やはり「継承」ではなく「合成」を使うのがいいでしょう。「合成」とは言ってもさまざまなやり方はありますが、「Decorator パターン」は有名です。
ただ、間違っても「合成」=「Decorator パターン」と勘違いしないでください。ケース・バイ・ケースで臨機応変に組み方を変えましょう!

Decoratorパターンの見た目

結論から言うと、最終的に以下のような使い方ができるようにしたら「Decoratorパターン」と言えるでしょう。

// 「1.4GHzデュアルコアIntel Core i5」のiMacのインスタンスを作る
$iMac = new IMac_1_4GHz();

// 「2.9GHzクアッドコアIntel Core i5」のiMacのインスタンスを作る
$iMac2 = new IMac_2_9GHz();

// SuperDrive付きのiMacインスタンスを生成
$iMac = new SuperDrive($iMac);

// 「SuperDrive + FlashStorage」付きのiMacインスタンスを生成
$iMac = new FlashStorage($iMac);

// 「SuperDrive + FlashStorage + AppleCare」付きのiMacインスタンスを生成
$iMac = new AppleCare($iMac);

// 「SuperDrive + FlashStorage + AppleCare」付きのiMacの値段を出す
echo $iMac2->get_cost();





// 以下同様。
//「2.9GHzクアッドコアIntel Core i5」モデルに対して「SuperDrive + FlashStorage」付きのiMacインスタンスを生成している
$iMac2 = new SuperDrive($iMac2);
$iMac2 = new FlashStorage($iMac2);

echo $iMac2->get_cost();

付属品の「AppleCare、SuperDrive、FlashStorage」クラスのコンストラクタに「iMac」オブジェクトを渡して、新しい「iMac」オブジェクトを生成していることが分かると思います。

実装コード

では、上記のような使い方を実現するためのコードを実際に見てみましょう。
コメントに「デコレーターとコンポーネント」という用語がでてきますが、あとで説明しますので気にせず読んでみてください。

<?php

// デコレーターとコンポーネントの共通のインターフェイス(或いは抽象クラス)
interface IMac {
	public function get_cost();

	public function get_description();
}

class IMac_1_4GHz implements IMac {
	public function get_cost() {
		return 116800;
	}

	function get_description() {
		return '1.4GHzデュアルコアIntel Core i5';
	}
}

class IMac_2_7GHz implements IMac {
	public function get_cost() {
		return 138800;
	}

	function get_description() {
		return '2.7GHzクアッドコアIntel Core i5';
	}
}

class IMac_2_9GHz implements IMac {
	public function get_cost() {
		return 158800;
	}

	function get_description() {
		return '2.9GHzクアッドコアIntel Core i5';
	}
}

// デコレーターの抽象クラス
abstract class Options implements IMac {
	/** @type  IMac $iMac */
	protected $iMac, $cost, $description;

	function __construct( IMac $iMac ) {
		$this->iMac = $iMac;
	}

	public function get_cost() {
		return $this->iMac->get_cost() + $this->cost;
	}

	public function get_description() {
		return $this->iMac->get_description() ."<br>". $this->description;
	}
}

class SuperDrive extends Options {
	protected $description = 'Apple USB SuperDrive';
	protected $cost = 7800;

	function __construct( IMac $iMac ) {
		parent::__construct( $iMac );
		// SuperDrive固有の処理
	}
}

class FlashStorage extends Options {
	protected $description = "256GBフラッシュストレージ";
	protected $cost = 24500;

	function __construct( IMac $iMac ) {
		parent::__construct( $iMac );
		// SuperDrive固有の処理
	}
}

class AppleCare extends Options {
	protected $description = "AppleCare Protection Plan for iMac";
	protected $cost = 20800;

	function __construct( IMac $iMac ) {
		parent::__construct( $iMac );
		// SuperDrive固有の処理
	}
}

// iMacを作る
$iMac = new IMac_1_4GHz();
$iMac2 = new IMac_2_9GHz();

$iMac = new SuperDrive($iMac);
$iMac = new FlashStorage($iMac);
$iMac = new AppleCare($iMac);

$iMac2 = new SuperDrive($iMac2);
$iMac2 = new FlashStorage($iMac2);
?>

<body>
<section>
	<h1>iMac1</h1>

	<p>
		<?= $iMac->get_description() ?>
		<br/>
		値段: ¥<?= $iMac->get_cost() ?>
	</p>
</section>

<section>
	<h1>iMac2</h1>

	<p>
		<?= $iMac2->get_description() ?>
		<br/>
		値段: ¥<?= $iMac2->get_cost() ?>
	</p>
</section>

</body>

出力結果:

iMac1

1.4GHzデュアルコアIntel Core i5
Apple USB SuperDrive
256GBフラッシュストレージ
AppleCare Protection Plan for iMac

値段: ¥169900

iMac2

2.9GHzクアッドコアIntel Core i5
Apple USB SuperDrive
256GBフラッシュストレージ

値段: ¥191100

Decoratorパターンの定義

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

上記の例に照らし合わせてみると:

既存のオブジェクト($iMac
新しい機能や振る舞い(->get_cost->get_description
動的に(新しいクラスを作らずに)追加することを可能にする。

「コンポーネント」と「デコレーター」

コンポーネントとは「構成要素」のことです。
上記の例では「iMac、SuperDrive、FlashStorage、AppleCare」がコンポーネントに当たります。これらの要素を使って、最終的なiMacを作り上げます。

ただ、「iMac」の例は少し特別です。なぜなら、iMacなしでは話にならないからです。今回のコンポーネントは「iMac」を指しますが、もっと正確に言うと「SuperDrive、FlashStorage、AppleCare」もコンポーネントであるということを心に留めておいてください。

一方、デコレーターとは「コンポーネントを修飾するもの」のことです。
上記の例では「SuperDrive、FlashStorage、AppleCare」がデコレーターに当たります。ただのiMacではなく、「SuperDriveつきのiMac」だよ!とiMacを「修飾しているように見える」ところから、Decoratorという名がついたのだと思います。

ただ、やっていることはiMacを「ラッピング」しているだけなので、「ラッパ」と同義です。むしろ「ラッパ」の方が本質を突いた名前で、僕的にはしっくりきます・・・。

クラス図

Decoratorパターンのポイントは、同じインターフェイス(Component)をコンポーネントとデコレーターで共有しているところですね。このように同じインターフェイスを持たせることで、コンポーネントとデコレーターで自由にやりとりすることができます(ポリモーフィズム(Wikipedia)を持たせる)。

960px-Decorator_UML_class_diagram

Decoratorパターン以外の方法

実は最初にこのパターンを見たとき、な〜んか気持ち悪い実装&使い方だなと思いました。使うときの記述があまり直感的じゃないので。
それから、メソッドのチェーンができているので、途中でチェーンを断つと、いろいろバグりそうな気もします。

ですから、あまり好んで使いません(あくまでも個人的な好き嫌いの話です)。

そこで、Decoratorパターン以外の方法も考えてみます。

<?php

// 付属品の抽象クラス
abstract class Option {
	protected $description, $cost;

	public function get_cost() {
		return $this->cost;
	}

	public function get_description() {
		return $this->description;
	}
}

// 付属品の具象クラスたち
class SuperDrive extends Option {
	protected $description = "Apple USB SuperDrive";
	protected $cost = 7800;

	function __construct() {
		// SuperDrive固有の処理
	}
}

class FlashStorage extends Option {
	protected $description = "256GBフラッシュストレージ";
	protected $cost = 24500;

	function __construct() {
		// FlashStorage固有の処理
	}
}

class AppleCare extends Option {
	protected $description = "AppleCare Protection Plan for iMac";
	protected $cost = 20800;

	function __construct() {
		// AppleCare固有の処理
	}
}

// iMacの基底クラス
abstract class IMac {
	static $description, $cost;

	private $options = array();

	function __construct( array $options ) {
		$this->options = $options;
	}

	public function get_cost() {
		$cost = static::$cost;
		foreach ( $this->options as $option ) {
			/** @type Option $option */
			$cost += $option->get_cost();
		}

		return $cost;
	}

	public function get_description() {
		$description = static::$description . "<br><br>";
		foreach ( $this->options as $option ) {
			/** @type Option $option */
			$description .= $option->get_description() . "<br>";
		}

		return $description . "値段: ¥" . $this->get_cost();
	}
}

// iMacのCPU別のサブクラスたち
class IMac_1_4GHz extends IMac {
	static $description = '1.4GHzデュアルコアIntel Core i5';
	static $cost = 116800;
}

class IMac_2_7GHz extends IMac {
	static $description = '2.7GHzクアッドコアIntel Core i5';
	static $cost = 138800;
}

class IMac_2_9GHz extends IMac {
	static $description = '2.9GHzクアッドコアIntel Core i5';
	static $cost = 158800;
}

// 必要な付属品のインスタンスを全て渡す(いわゆる「Dependency-Injection」という手法)
$iMac  = new IMac_1_4GHz( [ new SuperDrive, new FlashStorage, new AppleCare ] );
$iMac2 = new IMac_2_9GHz( [ new SuperDrive, new FlashStorage ] );

$description  = $iMac->get_description();
$description2 = $iMac2->get_description();
?>

<body>
<section>
	<h1>iMac1</h1>

	<p>
		<?= $description; ?>
	</p>
</section>

<section>
	<h1>iMac2</h1>

	<p>
		<?= $description2; ?>
	</p>
</section>

</body>

出力結果:

iMac1

1.4GHzデュアルコアIntel Core i5

Apple USB SuperDrive
256GBフラッシュストレージ
AppleCare Protection Plan for iMac
値段: ¥169900

iMac2

2.9GHzクアッドコアIntel Core i5

Apple USB SuperDrive
256GBフラッシュストレージ
値段: ¥191100

同じく合成を使っています。構成はDecoratorさながらですけど、こっちのほうがコードが読みやすいんじゃないかなと>_<。

まとめ

Decoratorパターンは、慣れないうちはなかなか理解しづらいと思いますが、迷ったらパターン名に立ち戻って、原型をとどめつつ、何かを付け加えることを考えればいいと思います。

こんな感じでしょうか。

ookami

狼男である以前に一人の男です! みたいな?!

では、ご参考になれば幸いです!

 

【バッグエンドエンジニア王(キング)】

【PHPで学ぶデザインパターン入門】第1回 Strategyパターン

Web屋ならチェックしておきたい!作業効率が激変するChrome DevToolsの便利な使い方まとめ

Web開発者に革命をもたらす!「Web Components」超入門

Sublime Text3で構文エラーをチェックするプラグインが超絶便利

初心者でも分かる!git rebaseの使い方を解説します

LIGはWebサイト制作を支援しています。ご興味のある方は事業ぺージをぜひご覧ください。

Webサイト制作の実績・料金を見る

この記事のシェア数

LIGの王です。ウェブの全てを学ぶ為、中国は四川省より日本にやってきました。王という名に恥じぬよう、ウェブ業界のKINGとなるべく日々頑張っております。よろしくお願いいたします。

このメンバーの記事をもっと読む