##一.最もシンプルなデコレーター実装
JS は動的拡張において生来の優位性を持ち、簡単にデコレーターを実装できます:
// 初期タイプ
function Dog() {
console.log('I am a dog');
}
// デコレータータイプ
function CanRun(dog) {
dog.run = function() {
console.log('I can run');
}
return dog;
}
function CanWalk(dog) {
dog.walk = function() {
console.log('I can walk');
}
return dog;
}
function CanBark(dog) {
dog.bark = function() {
console.log('I can bark');
}
return dog;
}
// ...
// test
var dog = new Dog();
CanWalk(CanBark(CanRun(dog))); // 「ラッピング」を通じて dog の機能を拡張
dog.run();
dog.bark();
dog.walk();
シンプルさは十分にシンプルですが、いくつかの問題が存在します:
- デコレーターパターンを使う必要はないのでは?機能をすべて
Dogに入れればよいのでは?
上記の例から見れば確かにその通りですが、もし Dog が直接修改できない第三者コンポーネントなら、この時デコレーターパターンを通じて機能を拡張するのは非常に適切です。この角度から見ると、デコレーターパターンとファサードパターンは非常に似ており、唯一の違いは目的が異なることで、前者は新機能を拡張するため、後者は既存インターフェースの使いやすさを追求します
- どの機能をデコレータータイプとして存在させるべきか?
基礎的、必要的な機能は Dog の構成部分であるべきで、選択可能、追加的、あまり使用されない機能はデコレータータイプが提供すべきです
- デコレーターが不注意で既存の属性を上書きしたらどうするか?
確かに属性が上書きされるリスクが存在します。なぜなら私たちはタイプ上の何の制約も行っておらず、各デコレーター間も相対的に独立しており、他のデコレーターが追加した属性をカバーしてしまう可能性もあるからです。私たちはより信頼できる(後文で紹介する)デコレーター実装でこれらのリスクを回避する必要があります
##二.疑似クラシックデコレーター
JS は Interface サポートを提供していないため、インターフェースを通じてタイプを制約し、その信頼性を高めることはできませんが、自分で Interface を実装してタイプを制約できます。シンプルな Interface はこのようになるかもしれません:
function Interface(strName, arrMethodNames) {
this.name = strName;
this.strMethods = arrMethodNames;
}
Interface.ensureImplements = function(obj, interface) {
for(var i = 0; i < interface.strMethods.length; i++) {
if (!(interface.strMethods[i] in obj)) {
throw new TypeError('Interface.ensureImplements: no ' + interface.strMethods[i] + '\'s here');
}
}
}
カスタムの Interface を利用してタイプ制約を実現し、デコレーターパターンはこのようになります:
// デコレーターオブジェクトに相当する作用
var spec = {
attr: 'value',
actions: {
fun1: function() {
console.log('fun1');
},
fun2: function() {
console.log('fun2');
}
}
}
var myInterface = new Interface('myInterface', ['fun1', 'fun2']);
// コンストラクタ
function MyObject(spec) {
// インターフェース検査
Interface.ensureImplements(spec.actions, myInterface);
this.attr = spec.attr;
this.methods = spec.actions;
}
// test
var obj = new MyObject(spec);
obj.methods.fun1();
obj.methods.fun2();
インターフェースを利用してタイプ制約を実現しましたが、構造が明確ではなく、管理に不便です。最も管理しやすいのはもちろん階層構造で、つまり下面の抽象デコレーター中の継承メカニズムです
##三.抽象デコレーター
選択可能機能をまず抽象デコレータークラス中に定義しますが、実装を提供せず、具体的デコレーターサブクラスが実装を提供し、インターフェースを利用してタイプ制約を実現します。サンプルコードは以下の通り:
// インターフェースを定義
var iCoffee = new Interface('coffee', ['addMilk', 'addSalt', 'addSugar']);
// 基本クラスを定義
function Coffee() {
console.log('make a cup of coffee');
}
Coffee.prototype = {
addMilk: function() {},
addSalt: function() {},
addSugar: function() {},
getPrice: function() {
// 原味価格
return 30;
}
}
// 抽象デコレータークラスを定義
function CoffeeDecorator(coffee) {
Interface.ensureImplements(coffee, iCoffee);
this.coffee = coffee;
}
CoffeeDecorator.prototype = {
addMilk: function() {
return this.coffee.addMilk();
},
addSalt: function() {
return this.coffee.addSalt();
},
addSugar: function() {
return this.coffee.addSugar();
},
getPrice: function() {
return this.coffee.getPrice();
}
}
// デコレーターサブクラス
function MilkDecorator(coffee) {
// 親クラスコンストラクタを呼び出す
this.superType(coffee);
}
// 継承を定義
function extend(subType, superType) {
var F = function() {};
F.prototype = superType.prototype;
subType.prototype = new F(); // プロトタイプ属性を継承
subType.prototype.superType = superType;
console.log(subType.prototype.superType);
}
extend(MilkDecorator, CoffeeDecorator); // 継承
// 親クラスメソッドを再書き込み(拡張)
MilkDecorator.prototype.addMilk = function() {
console.log('add some milk');
}
MilkDecorator.prototype.getPrice = function() {
return this.coffee.getPrice() + 8;
}
// ...他のデコレーターサブクラスを定義
// test
var coffee = new Coffee();
console.log(coffee.getPrice()); // 30
coffee = new MilkDecorator(coffee);
console.log(coffee.getPrice()); // 38
この実装方式の優点は構造が明確なことで、欠点は複雑さが増加したことです。言語自体が提供していない特性を手動でシミュレートするのは潜在的なリスクがある可能性があり、私たちはインターフェースと継承メカニズムをシミュレートし、他の隠れた危険を埋め込む可能性があります
##四.jQuery が提供するデコレーターメカニズム
ええ、また $.extend() です。[Mixin パターン_JavaScript デザインパターン 10](/articles/mixin パターン-javascript デザインパターン 10/) で $.extend() が Mixin パターンの実装を提供すると述べ、ここでまたデコレーターパターンの実装を提供すると言っていますが、矛盾しません。なぜならこれらの 2 つのパターンの目的は既存コンポーネントの機能を拡張することで、厳密に言えば、jQuery の extend はより Mixin パターンに似ています(いくつかのコンポーネントを合併して新コンポーネントを生成し、もしこのメカニズムがデコレーターパターンと算えるなら、無理やり说得过去ます)
jQuery の extend に関するより多くの情報は以下を参照:jQuery.extend 関数詳解
##五.デコレーターパターンの優劣点
優点
-
基礎機能と選択可能機能(デコレーター機能)を分離
-
オブジェクトの機能を動的に拡張でき、基本オブジェクトを意外に修改することはない
劣点
-
大量の小タイプを導入し、命名空間の混乱を引き起こす可能性がある
-
管理が不適切だとアプリケーションの構造をより複雑にし、特に継承メカニズムに基づく実装では、深層継承は可読性を极大地に低下させる
参考資料
- 『JavaScript デザインパターン』
コメントはまだありません