【JavaScript】デザインパターンを知ってみる。デコレータ編
夏に向け今度こそダイエットしたいminamiです。
今回はJavaScriptでデコレータパターンを書いてみようと思います。
なぜ使うのか?
デコレータ(装飾者)パターンは既存のオブジェクトに新しい機能や振る舞いを動的に追加することを目的としたデザインパターンです。
デコレータパターンの特徴としては下記が挙げれらます。
- 既存のオブジェクトに新しい機能や振る舞いを動的に追加する
基本となるものにどんどんトッピングや装飾をしていくイメージに近いです。
どういった使い方が考えられるでしょうか。
たくさんのトッピングが選べるコーヒー店を想定します。
基本のコーヒーに豆乳やホイップやチョコチップをトッピングすると値段も味も変わる場合、下記のように元のコーヒーを基本としたサブクラスを作っていく方法が考えられます。
// 基本のCoffee オブジェクト
var Coffee = function() {
this.value = 300;
this.tasteAry = [];
}
// Coffeeオブジェクト の基本的な機能
Coffee.prototype = {
// 値段を返す
getPrice: function() {
return this.value;
},
// 味を追加
pushTaste: function(taste) {
this.tasteAry.push(taste);
},
// 味見
tasting: function() {
var tastes = '';
var i=0;
while(i < this.tasteAry.length) {
tastes += this.tasteAry[i] + '、';
i += 1;
}
tastes+="の味がする!";
return tastes;
}
}
// Coffee オブジェクトを継承した豆乳入りクラス
var CoffeeAndSoimilk = function() {
Coffee.call(this);
this.value = 400;
this.tasteAry = ['豆乳'];
console.log('豆乳をプラスですね');
}
CoffeeAndSoimilk.prototype = new Coffee();
// Coffee オブジェクトを継承した豆乳とホイップ入りクラス
var CoffeeAndSoimilkAndWhip = function() {
Coffee.call(this);
this.value = 500;
this.tasteAry = ['豆乳','ホイップ'];
console.log('豆乳とホイップをプラスですね');
}
CoffeeAndSoimilkAndWhip.prototype = new Coffee();
// Coffee オブジェクトを継承した豆乳とホイップとチョコチップ入りクラス
var CoffeeAndSoimilkAndWhipAndChocochip = function() {
Coffee.call(this);
this.value = 600;
this.tasteAry = ['豆乳','ホイップ','チョコチップ'];
console.log('豆乳とホイップとチョコチップをプラスですね');
}
CoffeeAndSoimilkAndWhipAndChocochip.prototype = new Coffee();
// 豆乳とホイップとチョコチップ入りCoffeeオブジェクトを作成
var myCoffee = new CoffeeAndSoimilkAndWhipAndChocochip();
// 価格(600円)
console.log(myCoffee.getPrice());
// 味見(豆乳、ホイップ、チョコチップ、の味がする!)
console.log(myCoffee.tasting());
とりあえずはこれでOKに見えますが、ホイップだけ抜きたい時や更に追加でソースを入れたい場合はどうなるでしょうか。
そのたびにサブクラスを一つ増やす必要があり、どんどんメンテナンスが煩雑になっていきます。
また、元のコーヒーに機能をつけるとサブクラス全体にも変更が加わってしまいます。
デコレータを使う
デコレータパターンを使うとこのような問題に対処できます。
まず基本となるコーヒーのクラスを作るところは変わりませんが、トッピングをする際は元のコーヒーが持っているメソッドと同じインターフェースを持つデコレーターオブジェクトに元のコーヒーを渡してやります。
// 基本のCoffee オブジェクト
var Coffee = function() {
this.value = 300;
this.tasteAry = [];
}
// Coffeeオブジェクト の基本的な機能
Coffee.prototype = {
getPrice: function() {
return this.value;
},
pushTaste: function(taste) {
this.tasteAry.push(taste);
},
tasting: function() {
var tastes = '';
var i=0;
while(i < this.tasteAry.length) {
tastes += this.tasteAry[i] + '、';
i += 1;
}
tastes+="の味がする!";
return tastes;
}
}
// デコレータ(抽象化クラス)
// コンストラクタにデコレーションするオブジェクト(Coffee)自体を渡す
var CoffeeDecorator = function(coffee) {
this.coffee = coffee;
}
// デコレータは元のCoffee オブジェクトと同じインターフェースを持つ。
// Coffee オブジェクトの機能を同じ名前で呼ぶだけ
CoffeeDecorator.prototype = {
pushTaste: function(taste) {
this.coffee.pushTaste(taste);
},
getPrice: function() {
return this.coffee.getPrice();
},
tasting: function() {
return this.coffee.tasting();
}
}
// デコレータ詳細
var AddSoyMilkDecorator = function(coffee) {
CoffeeDecorator.call(this,coffee);
this.pushTaste('豆乳');
console.log('豆乳をプラスですね');
}
AddSoyMilkDecorator.prototype = new CoffeeDecorator();
AddSoyMilkDecorator.prototype.getPrice = function() {
return this.coffee.getPrice() + 100
}
var AddWhipDecorator = function(coffee) {
CoffeeDecorator.call(this,coffee);
this.pushTaste('ホイップ');
console.log('ホイップをプラスですね');
}
AddWhipDecorator.prototype = new CoffeeDecorator();
AddWhipDecorator.prototype.getPrice = function() {
return this.coffee.getPrice() + 150
}
var AddChocoChipDecorator = function(coffee) {
CoffeeDecorator.call(this,coffee);
this.pushTaste('チョコチップ');
console.log('チョコチップをプラスですね');
}
AddChocoChipDecorator.prototype = new CoffeeDecorator();
AddChocoChipDecorator.prototype.getPrice = function() {
return this.coffee.getPrice() + 50
}
// Coffee オブジェクトを作成
var myCoffee = new Coffee();
// デコレータでトッピング追加
myCoffee = new CoffeeDecorator(myCoffee);
myCoffee = new AddSoyMilkDecorator(myCoffee); // 豆乳追加
myCoffee = new AddWhipDecorator(myCoffee); // ホイップ追加
myCoffee = new AddChocoChipDecorator(myCoffee); // チョコチップ追加
// 価格 (600円)
console.log(myCoffee.getPrice());
// 味 (豆乳、ホイップ、チョコチップ、の味がする!)
console.log(myCoffee.tasting());
『豆乳とホイップとチョコチップ入りコーヒー』を最初から作るのではなく、最初に作ったコーヒーに後から豆乳を入れて、ホイップを入れて、チョコチップを入れていくと思うとイメージしやすいでしょうか。
この実装方法では更にトッピングの種類を足したい場合も、組み合わせを考えることなく一つサブクラスを追加するだけで対応できます。また、トッピングを抜きたい場合もトッピングの手順を消すだけなので簡単です。
まとめ
デコレータパターンについて調べてみました。
既存の機能に手を入れる際も、コアな部分の書き換えをすることなくメンテナンスができるのがデコレータの優れているところでしょうか。
つける機能が多いと、オブジェクトを作成するだけでも記述が長くなるのが難点ですが、ファクトリパターンなどと組み合わせることでさらに使いやすくなるようです。実装方法もいろいろとあるようなので調べてみたいです。