Decorator
一言でいうと
Decorator (デコレーター、 装飾器) は、 構造に関するデザインパターンの一つで、 ある振る舞いを含む特別なラッパー・オブジェクトの中にオブジェクトを配置することで、 それらのオブジェクトに新しい振る舞いを付け加えます。
![Decorator デザインパターン](/images/patterns/content/decorator/decorator.png?id=710c66670c7123e0928d3b3758aea79e)
問題
プログラムが重要なイベントについてユーザーに通知できる、 通知ライブラリーを開発しているとします。
ライブラリーの初期バージョンは、 少しのフィールドとコンストラクターと単一の send
メソッドからなる Notifier
クラスを基本にしてました。 このメソッドは、 クライアントからメッセージの引数を受け取り、 コンストラクターを介して通知オブジェクトに渡されたメールアドレスのリストに、 そのメッセージを送信することができました。 他社制作のアプリがクライアントで、 通知オブジェクトを一度だけ構築して設定し、 何か重要なことが起こるたびにそのオブジェクトを使用することになっていました。
![Decorator パターン適用前のライブラリーの構造](/images/patterns/diagrams/decorator/problem1-ja.png?id=a3e32153fa25e772014e5f3adfed41da)
プログラムは、 通知クラスを使用して、 重要なイベントに関する通知を事前設定されえた固定のメール・アドレスのリストに送信することができた。
ある時、 ライブラリーのユーザーは、 電子メール通知以上のものを期待していることに気づきます。 多数のユーザーは、 重要事項については、 SMS を受信することを望んでいます。 Facebook で通知を受けたい人もいます。 そして勿論、 企業ユーザーは、 Slack の通知を望んでいます。
![他の通知タイプを実装した後のライブラリーの構造](/images/patterns/diagrams/decorator/problem2.png?id=ba5d5e106ea8c4848d60e230feca9135)
通知タイプ一つずつを、 サブクラスとして実装する。
簡単そうですね? 単に Notifier
クラスを拡張して、 新しいサブクラスに追加の通知メソッドをいくつか追加すればいいです。 クライアントは、 希望する通知クラスのインスタンスを作り、 それを以降の全部の通知で使うことになっていました。
しかし、 誰かが理にかなった質問をします。 「何で一度に複数の通知タイプを使用できないんですか? 家が火事になったら、 全チャンネルを通して通知されたいでしょう。」
そこで、 これを解決しようと、 複数の通知メソッドを含む新しい特別なサブクラスを作成してみました。 しかし、 このやり方では、 ライブラリーのコードだけでなく、 クライアントのコードも非常に膨れ上がってしまうことがすぐに明らかになりました。
![クラスの組み合わせを作成した後のライブラリーの構造](/images/patterns/diagrams/decorator/problem3.png?id=f3b3e7a107d870871f2c3167adcb7ccb)
サブクラスの組み合わせの爆発
通知機能の構造を作るにあたって、 サブクラスの数が間違ってギネス記録を破ることのないような、 別の方法を見つける必要があります。
解決策
オブジェクトの振る舞いを変更する必要がある場合、 クラス拡張は、 最初に思い浮かぶことです。 ただし、 継承にはいくつかの深刻な注意すべき点があります。
- 継承は静的です。 既存のオブジェクトの振る舞いは実行時に変更できません。 オブジェクト全体を別のサブクラスから作成された別のものと置き換えることのみできます。
- サブクラスは、 親クラスを一つしか持てません。 ほとんどの言語では、 同時に複数のクラスから振る舞いを継承することはできません。
このような注意点を克服する方法の一つとしては、 継承 の代わりに集約 (Aggregation) または合成 (Composition) を使用することです。 どちらの選択肢もほぼ同じように機能します。 あるオブジェクトが別のオブジェクト参照を 一つ保持 (has-a) し、 そのオブジェクトに仕事を委任します。 継承ではオブジェクト自身が (is) スーパークラスから振る舞いを継承し、 仕事を行います。
この新しいやり方では、 リンクされた 「ヘルパー」 オブジェクトを簡単に別のオブジェクトに置き換えることができ、 実行時のコンテナの振る舞いを変更できす。 オブジェクト一つから複数のオブジェクトへの参照を持ち、 いろいろな種類の作業を委任することにより、 様々なクラスの振る舞いを利用できます。 集約と合成は、 Decorator を含む多くのデザインパターンの背後にある重要な原則です。 というところで、 パターンの議論に戻りましょう。
![継承 対 集約](/images/patterns/diagrams/decorator/solution1-ja.png?id=01ae1d8c8aa8f04d51e0a2e65a3c274a)
継承 対 集約
「Wrapper」 (ラッパー、 包装) は、 Decorator パターンのもう一つのニックネームです。 このパターンの中心となるアイデアを明確に表現しています。 ラッパー は、 何らかの ターゲットオブジェクトにリンクできるオブジェクトです。 ラッパーはターゲットと同じメソッドを含み、 受け取ったすべてのリクエストをターゲットに委任します。 しかし、 ラッパーはリクエストをターゲットに渡す前や後に、 何らかのことを行い結果を操作することができます。
単純なラッパーはいつ真のデコレーターになるのでしょう? さっき述べたように、 ラッパーはラップされたオブジェクトと同じインターフェースを実装しています。 そのため、 クライアントの観点からすると、 これらのオブジェクトは同一です。 ラッパーの参照フィールドが、 そのインターフェースに沿ったあらゆるオブジェクトを受け入れるようにしてください。 オブジェクトを複数のラッパーでカバーし、 すべてのラッパーの振る舞いの組み合わせを追加できるようになります。
先ほどの通知の例では、 電子メール通知の振る舞いは、 Notifier
基底クラスに残し、 他のすべての通知方法をデコレーターに変えてみましょう。
![Decorator パターンを使った解決策](/images/patterns/diagrams/decorator/solution2.png?id=cbee4a27080ce3a0bf773482613e1347)
様々な通知メソッドがデコレーターになる。
クライアント・コードは、 クライアントの設定に一致するデコレーターの組で、 基本的通知ブジェクトをラップする必要があります。 結果として得られるオブジェクトは積み重ねの構造をしています。
![アプリは、通知デコレーターの複雑な積み重ねのように構成するかもしれない。](/images/patterns/diagrams/decorator/solution3-ja.png?id=00cbe8d2c5c3381cd4106a699df66147)
アプリは、 通知デコレーターの複雑な積み重ねのように構成するかもしれない。
積み重ねの中の最後にあるデコレーターが、 クライアントが実際にやりとりするオブジェクトです。 すべてのデコレーターが、 通知の基底クラスと同じインターフェースを実装しているため、 クライアント・コードの残りの部分は、 相手が 「純粋な」 通知オブジェクトなのか、 デコレーターなのかは、 気にしません。
同じやり方を、 メッセージの組み立てや、 受信者リストの作成など、 他の振る舞いにも適用できます。 クライアントは、 同じインターフェースに沿っている限り、 いかなる特製デコレーターを使ってでもオブジェクトを飾ることができます。
現実世界でのたとえ
![Decorator パターンの例](/images/patterns/content/decorator/decorator-comic-1.png?id=80d95baacbfb91f5bcdbdc7814b0c64d)
衣類を重ね着して、 組み合わせの効果を得る。
服を着ることはデコレーター使用の例です。 寒い時は自分をセーターに包み込み込みます。 セーターだけではまだ寒い場合、 上にジャケットを着ることができます。 雨が降っていれば、 レインコートを着られます。 これらの衣服のすべてがあなたの基本的な振る舞いを 「拡張」 しますが、 それはあなたの一部ではありません。 必要がなくなればいつでも簡単に服を脱ぐことができます。
構造
![Decorator デザインパターンの構造](/images/patterns/diagrams/decorator/structure.png?id=8c95d894aecce5315cc1b12093a7ea0c)
![Decorator デザインパターンの構造](/images/patterns/diagrams/decorator/structure-indexed.png?id=09401b230a58f2249e4c9a1195d485a0)
-
コンポーネント (Component) は、 ラッパーとラップされるオブジェクトとの共通インターフェースを宣言します。
-
具象コンポーネント (Concrete Component) は、 ラップされるオブジェクトのクラスで、 デコレーターによって変更できる基本的な振る舞いを定義します。
-
基底デコレーター (Base Decorator) クラスには、 ラップされたオブジェクトを参照するためのフィールドがあります。 フィールドの型は、 具象コンポーネントとデコレーターの両方を含むことができるように、 コンポーネント・インターフェースとして宣言する必要があります。 基底デコレーターは、 すべての操作をラップされたオブジェクトに委任します。
-
具象デコレーター (Concrete Decorator) は、 コンポーネントに動的に追加可能な振る舞いを定義します。 具象デコレーターは、 基底デコレーターのメソッドを上書きし、 親メソッドを呼び出す前または後に追加の振る舞いのコードを実行します。
-
クライアント (Client) は、 コンポーネント・インターフェースを介してすべてのオブジェクトとやりとりする限り、 コンポーネントを複数のデコレーターの層でラップできます。
擬似コード
この例では、 Decorator パターンを適応して、 機密データの圧縮と暗号化を、 このデータを実際に使うコードから独立して行います。
![Decorator パターン例の構造](/images/patterns/diagrams/decorator/example.png?id=eec9dc488f00c85f50e764323baa723e)
暗号化と圧縮のデコレーターの例
アプリケーションは、 元データのオブジェクトを二つのデコレーターで包み込みます。 どちらのラッパーも、 データの書き込み方法とディスクからの読み取り方を以下のように変更します:
-
データをディスクに書き込む直前に、 デコレーターが暗号化と圧縮を行います。 元のクラスは、 暗号化され安全になったデータを、 そのような変更が行われたことを知らずに、 ファイルに書き込みます。
-
データをディスクから読み取った直後に、 データは同じ二つのデコレーターを通過し、 解凍と復号が行われます。
デコレーターとデータ・ソース・クラスは、 同じインターフェースを実装しているため、 クライアント・コード内で入れ替えが可能です。
// コンポーネントのインターフェースは、デコレーターが変更できる操作を定義。
interface DataSource is
method writeData(data)
method readData():data
// 具象コンポーネントは、操作のデフォルトの実装を行う。プログラムには、こ
// れらのクラスのいくつかの異種があるかもしれない。
class FileDataSource implements DataSource is
constructor FileDataSource(filename) { …… }
method writeData(data) is
// データをファイルに書く。
method readData():data is
// データをファイルから読む。
// 基底デコレーター・クラスは、他のコンポーネントと同じインターフェースに
// 従う。このクラスの一次的目的は、すべての具象デコレーターのラップ用イン
// ターフェースを定義すること。ラッパー・コードのデフォルトの実装には、
// ラップされたコンポーネントを格納するためのフィールドとそれを初期化する
// 方法が含まれているかもしれない。
class DataSourceDecorator implements DataSource is
protected field wrappee: DataSource
constructor DataSourceDecorator(source: DataSource) is
wrappee = source
// 基底デコレーターは、ラップされたコンポーネントにすべての作業を単純
// に委任。追加の振る舞いは具象デコレーターに追加可能。
method writeData(data) is
wrappee.writeData(data)
// 具象デコレーターはラップされたオブジェクトを直接呼び出す代わりに、
// 親の実装を呼び出すこともできる。このやり方は、デコレーター・クラス
// の拡張を簡単に行える。
method readData():data is
return wrappee.readData()
// 具象デコレーターはラップされたオブジェクトのメソッドを呼び出さなければ
// ならないが、結果に何かを付け足すことは許される。デコレーターは、ラップ
// されたオブジェクトへの呼び出しの前か後に追加の振る舞いを実行可能。
class EncryptionDecorator extends DataSourceDecorator is
method writeData(data) is
// 1. 渡されたデータを暗号化。
// 2. ラップされたオブジェクトの writeData メソッドに暗号化され
// たデータを渡す。
method readData():data is
// 1. ラップされたオブジェクトの readData メソッドからデータを
// 取得。
// 2. 暗号化されている場合は復号を試みる。
// 3. 結果を返す。
// 複数の層のデコレーターでオブジェクトをラップ可能。
class CompressionDecorator extends DataSourceDecorator is
method writeData(data) is
// 1. 渡されたデータを圧縮。
// 2. ラップされたオブジェクトの writeData メソッドに圧縮した
// データを渡す。
method readData():data is
// 1. ラップされたオブジェクトの readData メソッドからデータを
// 取得。
// 2. 圧縮されている場合は、解凍を試みる。
// 3. 結果を返す。
// オプション 1. デコレーターの組み合わせの単純な例。
class Application is
method dumbUsageExample() is
source = new FileDataSource("somefile.dat")
source.writeData(salaryRecords)
// データはそのままの形で目的のファイルに書き込まれた。
source = new CompressionDecorator(source)
source.writeData(salaryRecords)
// データは圧縮されて目的のファイルに書き込まれた。
source = new EncryptionDecorator(source)
// この時点で source 変数に含まれているのは:
// 暗号化 > 圧縮 > FileDataSource
source.writeData(salaryRecords)
// 圧縮、暗号化されたデータがファイルに書き込まれた。
// オプション 2. クライアント・コードは外部データ・ソースを使用。SalaryManager
// オブジェクトは、データ格納方法の詳細を知らず、関心もない。事前に決められ、
// アプリケーション設定コードから受け取ったデータソースに対して作業を行
// う。
class SalaryManager is
field source: DataSource
constructor SalaryManager(source: DataSource) { …… }
method load() is
return source.readData()
method save() is
source.writeData(salaryRecords)
// 他の便利なメソッドが続く……
// アプリは設定や環境に従い、他のデコレーターの組み合わせを実行時に作成可
// 能。
class ApplicationConfigurator is
method configurationExample() is
source = new FileDataSource("salary.dat")
if (enabledEncryption)
source = new EncryptionDecorator(source)
if (enabledCompression)
source = new CompressionDecorator(source)
logger = new SalaryManager(source)
salary = logger.load()
// ……
適応性
オブジェクトに、 それを使うコードを変更せずに、 追加の振る舞いを実行時に与える必要がある場合に、 Decorator パターンを使用します。
Decorator では、 ビジネス上のロジックを層構造にし、 各層ごとにデコレーターを一つ作成し、 これを実行時に組み合わせます。 クライアントのコードは、 これらのオブジェクトを同じ方法で扱えます。 どれも共通のインターフェースに沿っているからです。
継承を使ってオブジェクトの振る舞いを拡張することが簡単にできない場合や不可能な場合に、 このパターンを使用してください。
多くのプログラミング言語には、 クラスの拡張を防止する final
キーワードが用意されています。 final クラスの場合、 既存の振る舞いを再利用する唯一の方法は、 Decorator パターンを使い、 そのクラスを独自のラッパーで包み込むことです。
実装方法
-
プログラムの処理対象が、 一つの主要コンポーネントと、 それに追加可能な複数の層として表現可能なことを確認してください。
-
主要コンポーネントと追加の層に共通なメソッドは何かを考え出してください。 そのようなメソッドを列挙した共通インターフェースを宣言します。
-
具象コンポーネント・クラスを作成し、 その中で基本動作を定義します。
-
デコレーターの基底クラスを作成します。 ラップされたオブジェクトへの参照を記憶するためのフィールドが必要となります。 このフィールドの型は、 具象コンポーネントとデコレーターのどちらにでもリンクできるよう、 コンポーネント・インターフェースとして宣言される必要があります。 基底デコレーターは、 すべての操作をラップされたオブジェクトに委任します。
-
すべてのクラスがコンポーネント・インターフェースを実装していることを確認してください。
-
デコレーターの基底クラスを拡張して、 具象デコレーターを作成します。 具象デコレーターは、 親のメソッド (親メソッドは常にラップされたオブジェクトに委任) を呼ぶ前か呼んだ後に、 それ自身の追加の振る舞いを実行します。
-
デコレーターを作成し、 クライアントの必要に応じてそれを組み合わせるのは、 クライアント・コードの責任となります。
長所と短所
- 新しいサブクラスを作成せずにオブジェクトの振る舞いの拡張可能。
- オブジェクトへの責任の追加・削除が実行時に可能。
- オブジェクトを複数のデコレーターに包み込み、 振る舞いを組み合わせることが可能。
- 単一責任の原則。 いくつもの可能なすべての振る舞いを実装した一枚岩のクラスを、 いくつかの小さなクラスに分割可能。
- ラッパーの積み重ねから特定のラッパーの削除は困難。
- 振る舞いがデコレーターの積み重ねの順序に依存しないようにデコレーターを実装することは困難。
- デコレーター層初期設定コードは、 きれいとはいえない。
他のパターンとの関係
-
Adapter は既存のオブジェクトのインターフェースを変更するのに対し、 Decorator はインターフェースの変更なしにオブジェクトを強力にします。 さらに、 Decorator は、 再帰的な合成をサポートします。 これは、 Adapter を使用する時には不可能です。
-
Adapter はラップされたオブジェクトに対しては異なるインターフェースを提供し、 Proxy は同じインターフェースを提供し、 Decorator は強化したインターフェースを提供します。
-
Chain of Responsibility と Decorator とは非常によく似た階層構造をしています。 両パターンとも、 一連のオブジェクトを通して実行を渡すために、 再起的合成に依存します。 しかしながら、 いくつかの重要な違いがあります。
CoR ハンドラーは、 互いに独立して任意の処理を実行可能です。 また、 任意の時点でそれ以上リクエストを渡すのを止めることもできます。 一方、 様々な Decorator では、 オブジェクトの振る舞いの拡張をする時、 基底インターフェースとの一貫性を保つ必要があります。 さらに、 デコレーターはリクエストの流れを断ち切ることは許されていません。
-
Composite と Decorator は両方とも、 任意の数のオブジェクトを組織するために再起的合成を使用するので、 似たような構造図をしています。
Decorator は、 Composite に似ていますが、 子コンポーネントは一つしかありません。 もう一つの大きな違いは、 Decorator は内包するオブジェクトに責任を追加するのに対し、 Composite は、 単にその子たちの結果を 「まとめあげる」 だけです。
しかしながら、 これらのパターンは互いに協力できます。 Decorator を使用して、 Composite ツリー内の特定のオブジェクトの振る舞いを拡張できます。
-
Composite と Decorator を多用する設計に対しては、 Prototype の使用が有益かもしれません。 このパターンを適用すると、 複雑な構造を初めから再構築するのではなく、 それをクローンします。
-
Decorator と Proxy とは似たような構造をしていますが、 その意図は非常に異なります。 どちらのパターンも、 合成 (コンポジション) の原則に基づいており、 あるオブジェクトが仕事の一部を別のオブジェクトに委任します。 違いは、 Proxy は通常そのサービス・オブジェクトのライフサイクルの管理を行うのに対し、 Decorators では、 常にクライアントが管理するという点です。