十二、JavaScript装飾モード

12589 ワード

装飾モード


装飾器は再利用性能を向上させるための構造的な設計モデルである.Mixinと同様に、サブクラス分割を適用する別の価値のあるオプションと見なすことができる.
典型的な装飾器は、システム内の既存のクラスに動作を動的に追加する能力を提供する.そのアイデアは、装飾自体がクラスに関心を持たない基礎機能であり、それ自体をスーパークラスにコピーするだけである.
これらは、それらを使用するオブジェクトの依存コードを深く変更する必要がない前提で、機能を追加したい既存のシステムを変更するために使用することができる.開発者がそれらを使用する一般的な理由の1つは、それらのアプリケーションが互いにコヒーレントでないタイプのオブジェクトを大量に必要とする特性を含んでいる可能性があるからである.Javascriptゲームのような100以上の異なるオブジェクトのコンストラクタを定義しなければならないことを想像してみてください.
オブジェクトコンストラクタは、異なるプレーヤータイプを表すことができ、各タイプには異なる機能があります.領主指輪というゲームには、ホビット人、魔術師、獣人、巨獣、精霊、峠巨人、乱世陸地などの対象の構造器が必要で、これらの数は百を超えやすい.また、各タイプのコンピテンシーの組合せにサブクラスを作成することも考えられます.
例えば、指輪付きのホビット人、剣付きのホビット人、宝剣がいっぱい入った陸地などです.これは非常に実用的ではありません.異なる能力の数が増加していることを考慮すると、最後には制御できないに違いありません.
モディファイヤモードは、オブジェクトがどのように作成されたかに深く依存するのではなく、機能を拡張することに集中します.プロトタイプ継承のみに依存するのとは異なり、追加機能を提供する装飾オブジェクトを単純なベースオブジェクトに徐々に追加します.サブクラス区分とは異なり、ベースオブジェクトに属性やメソッドを追加(装飾)するので、より軽量になります.
Javascriptのオブジェクトに新しいプロパティを追加するのは、非常に直接的なプロセスです.そのため、この特定のことを心に刻み、非常に簡単な装飾器は次のように実現できます.

例1:新しい機能を備えた装飾コンストラクタ

// A vehicle constructor
function vehicle( vehicleType ){

    // some sane defaults
    this.vehicleType = vehicleType || "car";
    this.model = "default";
    this.license = "00000-000";

}

// Test instance for a basic vehicle
var testInstance = new vehicle( "car" );
console.log( testInstance );

// Outputs:
// vehicle: car, model:default, license: 00000-000

// Lets create a new instance of vehicle, to be decorated
var truck = new vehicle( "truck" );

// New functionality we're decorating vehicle with
truck.setModel = function( modelName ){
    this.model = modelName;
};

truck.setColor = function( color ){
    this.color = color;
};

// Test the value setters and value assignment works correctly
truck.setModel( "CAT" );
truck.setColor( "blue" );

console.log( truck );

// Outputs:
// vehicle:truck, model:CAT, color: blue

// Demonstrate "vehicle" is still unaltered
var secondInstance = new vehicle( "car" );
console.log( secondInstance );

// Outputs:
// vehicle: car, model:default, license: 00000-000

このタイプの簡単な実装は実用的であるが,装飾が貢献できるすべての潜在能力を本当に示していない.これのために、まず私のCoffeeの例とFreeman、SierraとBatesのHead First Design Patternsという優秀な本の中でMackbookショップをめぐって構築されたモデルを区別します.この2つの違いがあります.

例2:複数の装飾を施した装飾オブジェクト

// The constructor to decorate
function MacBook() {

  this.cost = function () { return 997; };
  this.screenSize = function () { return 11.6; };

}

// Decorator 1
function Memory( macbook ) {

  var v = macbook.cost();
  macbook.cost = function() {
    return v + 75;
  };

}

// Decorator 2
function Engraving( macbook ){

  var v = macbook.cost();
  macbook.cost = function(){
    return  v + 200;
  };

}

// Decorator 3
function Insurance( macbook ){

  var v = macbook.cost();
  macbook.cost = function(){
     return  v + 250;
  };

}

var mb = new MacBook();
Memory( mb );
Engraving( mb );
Insurance( mb );

// Outputs: 1522
console.log( mb.cost() );

// Outputs: 11.6
console.log( mb.screenSize() );

上記の例では、我々の装飾器はスーパークラスオブジェクトMacBook()のobjectをリロードする.cost()関数は、返されるMacbookの現在の価格にカスタマイズされたアップグレードされた価格を加算します.
これは、元のMacbookオブジェクトコンストラクタメソッドの装飾と見なされ、書き換えられていません(例えば、screenSize())、私たちが定義したMacbookの他の属性も変わらず、完璧です.
上記の例では、インタフェースが本当に定義されていません.また、作成者から受信者への移動時に、オブジェクトがインタフェースに対応していることを確認する責任も移行しました.

ぎじこぶんきかざり


Dustin DiazとRoss Harmesの共著で初めて見られるPro Javascript Design Patterns(PJDP)の装飾器の変体を試してみましょう.
以前の例とは異なり、DiazとHarmsは、JavaやC++などの他のプログラミング言語がどのように「インタフェース」の概念を使用して装飾器を実現するかに近いことを堅持しており、間もなく詳細に定義します.
注意:装飾モードのこの特殊な変体は参考に提供されています.複雑すぎる場合は、前のより簡単な実装を選択することをお勧めします.

インタフェース


PJDPに記述されるデコレーションは、同一のインターフェースを備えたオブジェクトを透明にカプセル化するためのオブジェクトである.インタフェースは、オブジェクトがどのようなメソッドを持つべきかを定義する方法ですが、実際には、それらのメソッドがどのように実装されるべきかは指定されていません.
メソッドにはどのようなパラメータがあるべきかを宣言することもできますが、これはオプションと見なされます.
では、なぜJavascriptでインタフェースを使用するのでしょうか.この考え方は、ドキュメントの自己説明特性を持たせ、再利用性を促進することを意味します.理論的には、インタフェースは、それらが変更されることを確保するとともに、オブジェクトにこれらの変更を実現させることによって、コードをより安定させる.
次に、Javascriptでアヒルのタイプを使用してインタフェースを実装する例を示します.アヒルのタイプは、実装された方法に基づいて、オブジェクトがコンストラクタ/オブジェクトのエンティティであるかどうかを判定する方法です.
// Create interfaces using a pre-defined Interface
// constructor that accepts an interface name and
// skeleton methods to expose.

// In our reminder example summary() and placeOrder()
// represent functionality the interface should
// support
var reminder = new Interface( "List", ["summary", "placeOrder"] );

var properties = {
  name: "Remember to buy the milk",
  date: "05/06/2016",
  actions:{
    summary: function (){
      return "Remember to buy the milk, we are almost out!";
   },
    placeOrder: function (){
      return "Ordering milk from your local grocery store";
    }
  }
};

// Now create a constructor implementing the above properties
// and methods

function Todo( config ){

  // State the methods we expect to be supported
  // as well as the Interface instance being checked
  // against

  Interface.ensureImplements( config.actions, reminder );

  this.name = config.name;
  this.methods = config.actions;

}

// Create a new instance of our Todo constructor

var todoItem = Todo( properties );

// Finally test to make sure these function correctly

console.log( todoItem.methods.summary() );
console.log( todoItem.methods.placeOrder() );

// Outputs:
// Remember to buy the milk, we are almost out!
// Ordering milk from your local grocery store

上記のコードでは、インタフェースは、インタフェースコンストラクタのインタフェースコードとここで見つけることができる厳格な機能検査を提供することを保証する.
インタフェースを使用する最大の問題は、Javascriptに内蔵されているサポートではないため、別の言語を真似しようとする特性がありますが、見ていると完全に適切ではありません.このようなリスクです.しかしながら、あまり性能消費がない軽量レベルのインタフェースは使用可能であり、以下に示す抽象装飾器も同様にこの概念を使用している.

抽象装飾者


このバージョンの装飾者モードの構造を明らかにするために、スーパークラスがあるのか、Macbookモデルがあるのか、storeがあるのかを想像し、追加費用を費やした多くの強化でMacbookを「装飾」することができます.
拡張には、4 GBまたは8 GBにアップグレードされたRam、彫刻、または類似のケースが含まれる.各拡張オプションの組合せについて、個別のサブクラスを使用してモデリングする場合は、次のように見えます.
var Macbook = function(){
        //...
};

var  MacbookWith4GBRam =  function(){},
     MacbookWith8GBRam = function(){},
     MacbookWith4GBRamAndEngraving = function(){},
     MacbookWith8GBRamAndEngraving = function(){},
     MacbookWith8GBRamAndParallels = function(){},
     MacbookWith4GBRamAndParallels = function(){},
     MacbookWith8GBRamAndParallelsAndCase = function(){},
     MacbookWith4GBRamAndParallelsAndCase = function(){},
     MacbookWith8GBRamAndParallelsAndCaseAndInsurance = function(){},
     MacbookWith4GBRamAndParallelsAndCaseAndInsurance = function(){};

などなど.
これは実際の解決策ではありません.新しいサブクラスには、可能な拡張の組合せが必要になる可能性があります.物事を簡単に保つ傾向があるため、巨大なサブクラスの集合を維持したくないので、装飾者を使ってこの問題をよりよく解決する方法を見てみましょう.
私たちが前に見たすべての組み合わせは必要ありません.私たちは簡単に5つの新しい装飾者クラスを作成するだけです.これらの拡張クラスのメソッド呼び出しは、Macbookクラスに渡されます.
次の例では、装飾者はそれらのコンポーネントを透明に包装し、興味深いことに、同じインタフェースで交換することができる.
ここではMacbookに定義したインタフェースです.
var Macbook = new Interface( "Macbook",
  ["addEngraving",
  "addParallels",
  "add4GBRam",
  "add8GBRam",
  "addCase"]);

// A Macbook Pro might thus be represented as follows:
var MacbookPro = function(){
    // implements Macbook
};

MacbookPro.prototype = {
    addEngraving: function(){
    },
    addParallels: function(){
    },
    add4GBRam: function(){
    },
    add8GBRam:function(){
    },
    addCase: function(){
    },
    getPrice: function(){
      // Base price
      return 900.00;        
    }
};

後で必要なより多くのオプションを追加しやすくするために、Mackbookインタフェースを実現するために使用されるデフォルトの方法を備えた抽象装飾器方法が定義され、残りのオプションはサブクラス分割されます.抽象的な装飾器は、できるだけ多くの異なる組み合わせに必要な装飾器から独立して、基礎クラスを装飾することができることを確保しています(以前の例を覚えていますか?)各可能な組合せのためにクラスを駆動する必要はありません.
// Macbook decorator abstract decorator class

var MacbookDecorator = function( macbook ){

    Interface.ensureImplements( macbook, Macbook );
    this.macbook = macbook; 

};

MacbookDecorator.prototype = {
    addEngraving: function(){
        return this.macbook.addEngraving();
    },
    addParallels: function(){
        return this.macbook.addParallels();
    },
    add4GBRam: function(){
        return this.macbook.add4GBRam();
    },
    add8GBRam:function(){
        return this.macbook.add8GBRam();
    },
    addCase: function(){
        return this.macbook.addCase();
    },
    getPrice: function(){
        return this.macbook.getPrice();
    }       
};

上記の例では、Macbookアクセサリーがコンポーネントのように1つのオブジェクトを使用しています.以前に定義したMacbookインタフェースを使用し、各メソッドについてコンポーネント上の同じメソッドを呼び出します.Macbookアクセラレータのみを使用してオプションクラスを作成できます.スーパークラスのコンストラクタを簡単に呼び出し、必要に応じて再ロードできる方法です.
var CaseDecorator = function( macbook ){

   // call the superclass's constructor next
   this.superclass.constructor( macbook );  

};

// Let's now extend the superclass
extend( CaseDecorator, MacbookDecorator );

CaseDecorator.prototype.addCase = function(){
    return this.macbook.addCase() + "Adding case to macbook";  
};

CaseDecorator.prototype.getPrice = function(){
    return this.macbook.getPrice() + 45.00; 
};

私たちが見ているように、ほとんどは対応する直接的な実現です.我々は装飾が必要なaddCase()とgetPrise()メソッドを再ロードし,まずコンポーネントのメソッドを実行してからそれに追加することで目的を達成した.
これまで本節で紹介した情報が1斤相当に多いことを考慮して、私たちが学んだことを際立たせるために、それをすべて別の例に置いてみましょう.
// Instantiation of the macbook
var myMacbookPro = new MacbookPro(); 

// Outputs: 900.00
console.log( myMacbookPro.getPrice() );

// Decorate the macbook
myMacbookPro = new CaseDecorator( myMacbookPro );

// This will return 945.00
console.log( myMacbookPro.getPrice() );

モディファイヤは、オブジェクトを動的に変更できるため、既存のシステムを変更する理想的なモードです.場合によっては、1つのオブジェクトと、各オブジェクトタイプに対して個別のサブクラス区分を維持するために発生する煩わしさを単純に囲んで、装飾器を作成するだけです.これにより、サブクラス分割オブジェクトをより顕著に直接維持するために、多くのサブクラス分割オブジェクトを必要とするアプリケーションが必要になる可能性があります.

アクセサリーとjQuery


我々がカバーする他のモードとともに、jQueryを用いて実装できる装飾モードの例も多い.jQuery.extend()を使用すると、2つ以上のオブジェクト(およびそのプロパティ)を、実行時または後で動的に1つのオブジェクトに拡張(またはブレンド)できます.
このシーンでは、ターゲットオブジェクトがソース/スーパークラスの既存のメソッドを中断または再ロードする必要はありません(これは可能ですが)、新しい機能を使用して装飾することができます.次の例では、デフォルト、オプション、設定の3つのオブジェクトを定義します.タスクの目的は、オプションで見つけた追加機能でデフォルトのオブジェクトを装飾することです.
[既定値](Default)をタッチできない状態に配置します.ここでは、後で発見されるプロパティとメソッドにアクセスする能力を失うことはありません.[オプション](Options)で装飾されたプロパティと関数を見つける能力を得ることができます.
var decoratorApp = decoratorApp || {};

// define the objects we're going to use
decoratorApp = {

    defaults: {
        validate: false,
        limit: 5,
        name: "foo",
        welcome: function () {
            console.log( "welcome!" );
        }
    },

    options: {
        validate: true,
        name: "bar",
        helloWorld: function () {
            console.log( "hello world" );
        }
    },

    settings: {},

    printObj: function ( obj ) {
        var arr = [],
            next;
        $.each( obj, function ( key, val ) {
            next = key + ": ";
            next += $.isPlainObject(val) ? printObj( val ) : val;
            arr.push( next );
        } );

        return "{ " + arr.join(", ") + " }";
    }

};

// merge defaults and options, without modifying defaults explicitly
decoratorApp.settings = $.extend({}, decoratorApp.defaults, decoratorApp.options);

// what we have done here is decorated defaults in a way that provides
// access to the properties and functionality it has to offer (as well as
// that of the decorator "options"). defaults itself is left unchanged

$("#log")
    .append( decoratorApp.printObj(decoratorApp.settings) + 
          + decoratorApp.printObj(decoratorApp.options) +
          + decoratorApp.printObj(decoratorApp.defaults));

// settings -- { validate: true, limit: 5, name: bar, welcome: function (){ console.log( "welcome!" ); },
// helloWorld: function (){ console.log("hello!"); } }
// options -- { validate: true, name: bar, helloWorld: function (){ console.log("hello!"); } }
// defaults -- { validate: false, limit: 5, name: foo, welcome: function (){ console.log("welcome!"); } }

メリット&デメリット


透明に使用でき、かなり柔軟なので、開発者はこのモードを喜んで使用しています.私たちが見ているように、オブジェクトは新しい行為でカプセル化したり、「装飾」したりして、基礎的なオブジェクトが変更される心配はありません.より広範な範囲では,このモードも同様の効果を達成するために大量のサブクラスに依存することを回避した.
しかし、このモードを実現する際には、私たちが意識すべき欠点もあります.管理に乏しい場合は、多くの微小で似たようなオブジェクトがネーミングスペースに導入されているため、アプリケーションアーキテクチャが著しく複雑になります.ここで懸念されるのは,管理がますます難しくなるほか,このモデルを熟練していない開発者も,その使用理由を把握する困難な時期がある可能性があることである.
十分な注釈やモデルの研究は、これに役立つはずですが、私たちが私たちのプログラムのどの範囲でこのモデルを使用するかをコントロールすれば、両方を改善することができます.