2013年5月17日金曜日

design patterns – Decorator


デコレーター

デコレーターパターンは、他の多くのパターンとはかなり異なります。デコレーターパターンを使用すれば、機能の組み合わせごとにサブクラスを作成することなく、クラスへの機能の追加または変更という問題が解決されます。例えば、デフォルトの機能を備えたcarというクラスがあるとします。carには、車に追加できる多数のオプションの機能があります(自動ロック、パワーウィンドウ、エアコンなど)。サブクラスを使用してこれを処理しようとすると、すべての組み合わせに対応するには合計で8つのクラスが必要になります。
  • Car
  • CarWithPowerLocks
  • CarWithPowerWindows
  • CarWithAc
  • CarWithPowerLocksAndPowerWindows
  • CarWithPowerLocksAndAc
  • CarWithPowerWindowsAndAc
  • CarWithPowerWindowsAndPowerLocksAndAc
この方法は、オプションを1つ増やすだけで8つのサブクラスが追加されるので、収拾がつかなくなりやすいです。これはデコレーターパターンを使用することで解決できます。この方法では、新しいオプションを追加するたびに、1つのクラスを作成するだけで済み、クラスが倍増することはありません。

デコレーターの構造

デコレーターは、基本オブジェクト(Car)を、基本オブジェクトと同じインターフェイスを持つデコレーターオブジェクトでラップすることで機能します。デコレーターには、メソッドの処理方法に関して次のようなオプションがあります。
  1. ラップされているオブジェクトのメソッドを完全にオーバーライドすることができる
  2. デコレーターがメソッドの挙動に影響しない場合、ラップされたオブジェクトに単に渡される
  3. デコレーターは、ラップされたオブジェクトに呼び出しを渡す前または渡した後に挙動を追加できる
デコレーターは基本オブジェクトをラップできるだけでなく、他のデコレーターをラップすることもできます。これは、すべてのデコレーターが同じインターフェイスを実装しているからです。一般的な構造は、図2のような感じになります。
図2.デコレーターパターンの構造
図2.デコレーターパターンの構造

デコレーターパターンの例

上のcarの図をコード化してみましょう。まず、ベースのCarクラスを構築します。
var Car = function() { console.log('Assemble: build frame, add core parts'); }; // The decorators will also need to implement this interface Car.prototype = { start: function() { console.log('The engine starts with roar!'); }, drive: function() { console.log('Away we go!'); }, getPrice: function() { return 11000.00; } };
フレーズをコンソールに単に送信し、何が起こっているかを示すことで、構造をシンプルに保ちます。もちろん、自動車は複雑な機械なので、もっと多くの機能を追加することもできますが、このデモンストレーションではシンプルな構造のまま使用します。
次に、CarDecoratorクラスを作成します。これは抽象化クラスとなるのでクラス自体はインスタンス化しませんが、本格的なデコレーターを作成するためにサブクラス化する必要があります。
var CarDecorator = function(car) { this.car = car; }; // CarDecorator implements the same interface as Car CarDecorator.prototype = { start: function() { this.car.start(); }, drive: function() { this.car.drive(); }, getPrice: function() { return this.car.getPrice(); } };
いくつか重要なポイントがあります。まず、CarDecoratorコンストラクターは、carを取ります。さらに厳密に言うと、Carと同じインターフェイスを実装するオブジェクトを取ります。これには、複数のCarや、CarDecoratorサブクラスが含まれます。また、すべてのメソッドは、ラップされたオブジェクトに要求を渡すだけです。これは、すべてのデコレーターが、変更されていないメソッドに対して使用するデフォルトの挙動です。
次に、デコレーターを全部作成します。CarDecoratorから継承しているので、変更する機能をオーバーライドするだけです。他のすべての点で、CarDecoratorは完全に機能します。
var PowerLocksDecorator = function(car) { // Call Parent Constructor CarDecorator.call(this, car); console.log('Assemble: add power locks'); }; PowerLocksDecorator.prototype = new CarDecorator(); PowerLocksDecorator.prototype.drive = function() { // You can either do this this.car.drive(); // or you can call the parent's drive function: // CarDecorator.prototype.drive.call(this); console.log('The doors automatically lock'); }; PowerLocksDecorator.prototype.getPrice = function() { return this.car.getPrice() + 100; }; var PowerWindowsDecorator = function(car) { CarDecorator.call(this, car); console.log('Assemble: add power windows'); }; PowerWindowsDecorator.prototype = new CarDecorator(); PowerWindowsDecorator.prototype.getPrice = function() { return this.car.getPrice() + 200; }; var AcDecorator = function(car) { CarDecorator.call(this, car); console.log('Assemble: add A/C unit'); }; AcDecorator.prototype = new CarDecorator(); AcDecorator.prototype.start = function() { this.car.start(); console.log('The cool air starts blowing.'); }; AcDecorator.prototype.getPrice = function() { return this.car.getPrice() + 600; };
この例では、オリジナルの機能に追加されるメソッドを追加するたびに、親クラスのメソッドを呼び出すことはせず、単にthis.car.x()を呼び出します。通常は、親のメソッドを呼び出した方が適切なのですが、このコードは非常にシンプルなので、carのプロパティを直接呼び出した方が簡単だと判断しました。このようにすると、もし要求を渡す以外の機能を担わせるためにCarDecoratorに組み込まれたデフォルトの挙動を変更する必要が生じた場合に面倒なことになりますが、これは単なるサンプルなので、その局面は多分ないでしょう。
そういうわけで、必要な要素はすべて揃ったので、実際に作成してみましょう。
var car = new Car(); // log "Assemble: build frame, add core parts" // give the car some power windows car = new PowerWindowDecorator(car); // log "Assemble: add power windows" // now some power locks and A/C car = new PowerLocksDecorator(car); // log "Assemble: add power locks" car = new AcDecorator(car); // log "Assemble: add A/C unit" // let's start this bad boy up and take a drive! car.start(); // log 'The engine starts with roar!' and 'The cool air starts blowing.' car.drive(); // log 'Away we go!' and 'The doors automatically lock'
必要な機能を備えた自動車を作成するのは簡単です。自動車および必要なデコレーターを作成し、順次、最新の装備セットを備えた自動車をデコレーターに追加していきます。特に、CarWithPowerLocksAndPowerWindowsAndAcのような1つのオブジェクトを単にインスタンス化するのと較べると、作成コードは非常に長くなります。しかし心配は無用です。装飾されたオブジェクトを作成するよりスマートな方法を、ファクトリパターンに関する項でご紹介します。デコレーターパターンについてもう少し詳しく知りたい場合は、筆者の個人ブログの「JavaScript Design Patterns: Decorator」の記事をお読みください。

0 件のコメント: