メインコンテンツへ移動

デコレーターパターン_JavaScript デザインパターン 11

無料2015-08-12#JS#Design_Pattern#JavaScript装饰者模式#伪经典装饰者#抽象装饰者

デコレーターパターンの核心は「ラッピング」で、複数の選択可能なデコレータータイプを提供し、初期タイプオブジェクトに必要なデコレータータイプをラッピングすることで初期タイプの機能を拡張し、タイプの大爆発を回避する。本文は JS で実装されたデコレーター(Decorator)パターンを詳細に紹介

##一.最もシンプルなデコレーター実装

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();

シンプルさは十分にシンプルですが、いくつかの問題が存在します:

  1. デコレーターパターンを使う必要はないのでは?機能をすべて Dog に入れればよいのでは?

上記の例から見れば確かにその通りですが、もし Dog が直接修改できない第三者コンポーネントなら、この時デコレーターパターンを通じて機能を拡張するのは非常に適切です。この角度から見ると、デコレーターパターンとファサードパターンは非常に似ており、唯一の違いは目的が異なることで、前者は新機能を拡張するため、後者は既存インターフェースの使いやすさを追求します

  1. どの機能をデコレータータイプとして存在させるべきか?

基礎的、必要的な機能は Dog の構成部分であるべきで、選択可能、追加的、あまり使用されない機能はデコレータータイプが提供すべきです

  1. デコレーターが不注意で既存の属性を上書きしたらどうするか?

確かに属性が上書きされるリスクが存在します。なぜなら私たちはタイプ上の何の制約も行っておらず、各デコレーター間も相対的に独立しており、他のデコレーターが追加した属性をカバーしてしまう可能性もあるからです。私たちはより信頼できる(後文で紹介する)デコレーター実装でこれらのリスクを回避する必要があります

##二.疑似クラシックデコレーター

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 つのパターンの目的は既存コンポーネントの機能を拡張することで、厳密に言えば、jQueryextend はより Mixin パターンに似ています(いくつかのコンポーネントを合併して新コンポーネントを生成し、もしこのメカニズムがデコレーターパターンと算えるなら、無理やり说得过去ます)

jQueryextend に関するより多くの情報は以下を参照:jQuery.extend 関数詳解

##五.デコレーターパターンの優劣点

優点

  1. 基礎機能と選択可能機能(デコレーター機能)を分離

  2. オブジェクトの機能を動的に拡張でき、基本オブジェクトを意外に修改することはない

劣点

  1. 大量の小タイプを導入し、命名空間の混乱を引き起こす可能性がある

  2. 管理が不適切だとアプリケーションの構造をより複雑にし、特に継承メカニズムに基づく実装では、深層継承は可読性を极大地に低下させる

参考資料

  • 『JavaScript デザインパターン』

コメント

コメントはまだありません

コメントを書く