組合模式

本文爲閱讀《Javascript設計模式》一書後,總結部分內容而得。其內部的代碼和截圖都來源自該書。

使用組合模式的一個場景示例

想象一下,現在你需要維護一個個人信息的頁面,當用戶不同時,頁面也可能會發生變化。比如,當用戶爲小明時,展示給他的頁面是這樣的

而當用戶爲小紅時,展示給她的頁面是這樣的

我現在想要實現保存頁面信息的功能,面對各種各樣的可能頁面,頁面上的各種元素保存信息的方式也不同,比如select和input框,其取值方式就完全不同。我要如何通過統一的函數來保存這些頁面信息呢?有一種方法,是通過每次保存頁面時,都遍歷頁面所有元素,判斷所有元素類型,並依據個元素類型執行對應的save函數。但這樣很顯然,會使代碼看上去混亂且臃腫。

用組合模式實現保存功能則你可以用一條簡單的命令,在多個子對象上激發遞歸行爲,將保存功能委託給各個子對象來實現,讓每個子對象都知道如何保存自己本身的信息,父對象只是起到一個傳遞調用的功能。

組合模式

組合對象的結構

一個組合對象由一些別的組合對象和葉對象組成。不再包含子對象的叫葉對象,其下一層級仍然包含子對象的叫組合對象。結構圖如下圖:

什麼情況下使用組合模式

  • 存在一批組織成某種層次體系的對象
  • 希望對這批對象或其中一部分對象實施一個操作

一個使用組合模式的示例----表單信息存儲

現在我們來看,使用組合模式,如何實現在本文最開始提出的那個表單信息存儲功能。
下圖爲表單的結構

明確組合對象及葉對象要具備的函數

首先,我們要明確,想讓組合對象以及葉對象實現哪些接口,即具備哪些方法。在本例中,我想讓他們實現兩個接口Composite和FormItem
var Composite=new Interface('Composite',['add''remove','getChild']);
var FormItem=new Interface('FormItem',['save']);

定義葉對象

所有葉對象,都繼承了Field,具有Field對象所定義的所有方法,即add,remove,getChild以及save
我們先實現該父類Field
var Field = function(id) { // implements Composite, FormItem
  this.id = id;
  this.element;
};

Field.prototype.add = function() {};
Field.prototype.remove = function() {};
Field.prototype.getChild = function() {};

Field.prototype.save = function() {
  setCookie(this.id, this.getValue);
};

Field.prototype.getElement = function() { 
  return this.element; 
};

Field.prototype.getValue = function() { 
  throw new Error('Unsupported operation on the class Field.'); 
};
父類定義好後,我們來定義各個葉對象。在這個應用中,我們假定頁面是由input框、select框以及textarea組成,那麼我們來定義這三個葉對象。
其實三個對象只有getValue方法是不同的。
/* InputField class. */

var InputField = function(id, label) { // implements Composite, FormItem
  Field.call(this, id);

  this.input = document.createElement('input');
  this.input.id = id;

  this.label = document.createElement('label');
  var labelTextNode = document.createTextNode(label);
  this.label.appendChild(labelTextNode);

  this.element = document.createElement('div');
  this.element.className = 'input-field';
  this.element.appendChild(this.label);
  this.element.appendChild(this.input);
};
extend(InputField, Field); // Inherit from Field.

InputField.prototype.getValue = function() { 
  return this.input.value;
};

/* TextareaField class. */

var TextareaField = function(id, label) { // implements Composite, FormItem
  Field.call(this, id);

  this.textarea = document.createElement('textarea');
  this.textarea.id = id;

  this.label = document.createElement('label');
  var labelTextNode = document.createTextNode(label);
  this.label.appendChild(labelTextNode);

  this.element = document.createElement('div');
  this.element.className = 'input-field';
  this.element.appendChild(this.label);
  this.element.appendChild(this.textarea);
};
extend(TextareaField, Field); // Inherit from Field.

TextareaField.prototype.getValue = function() { 
  return this.textarea.value;
};

/* SelectField class. */

var SelectField = function(id, label) { // implements Composite, FormItem
  Field.call(this, id);

  this.select = document.createElement('select');
  this.select.id = id;

  this.label = document.createElement('label');
  var labelTextNode = document.createTextNode(label);
  this.label.appendChild(labelTextNode);

  this.element = document.createElement('div');
  this.element.className = 'input-field';
  this.element.appendChild(this.label);
  this.element.appendChild(this.select);
};
extend(SelectField, Field); // Inherit from Field.

SelectField.prototype.getValue = function() {
  return this.select.options[this.select.selectedIndex].value;
};

定義根級組合對象

(注意,在當前這個實例中,根據要實現的功能,我只將頁面劃分爲了根級組合對象和葉對象,並沒有中間層級的組合對象)
在這段定義中,我們對所有添加進組合對象的葉對象進行了判斷,只有該對象實現了Composite, FormItem兩個接口,才添加進組合對象成爲葉對象。
</pre><pre name="code" class="javascript">var CompositeForm = function(id, method, action) { // implements Composite, FormItem
  this.formComponents = [];

  this.element = document.createElement('form');
  this.element.id = id;
  this.element.method = method || 'POST';
  this.element.action = action || '#';
};

CompositeForm.prototype.add = function(child) {
  Interface.ensureImplements(child, Composite, FormItem);
  this.formComponents.push(child);
  this.element.appendChild(child.getElement());
};

CompositeForm.prototype.remove = function(child) {
  for(var i = 0, len = this.formComponents.length; i < len; i++) {
    if(this.formComponents[i] === child) {
      this.formComponents.splice(i, 1); // Remove one element from the array at 
                                        // position i.
      break;
    }
  }
};

CompositeForm.prototype.getChild = function(i) {
  return this.formComponents[i];
};

CompositeForm.prototype.save = function() {
  for(var i = 0, len = this.formComponents.length; i < len; i++) {
    this.formComponents[i].save();
  }
};

CompositeForm.prototype.getElement = function() { 
  return this.element; 
};

組合對象和葉對象的匯合

兩類對象都定義完了,接下來,就是將他們聯繫到一起,並且實現我們想要實現的功能了。
var contactForm = new CompositeForm('contact-form', 'POST', 'contact.php');

contactForm.add(new InputField('first-name', 'First Name'));
contactForm.add(new InputField('last-name', 'Last Name'));
contactForm.add(new InputField('address', 'Address'));
contactForm.add(new InputField('city', 'City'));
contactForm.add(new SelectField('state', 'State', stateArray)); // var stateArray =
    [{'al', 'Alabama'}, ...]
contactForm.add(new InputField('zip', 'Zip'));
contactForm.add(new TextareaField('comments', 'Comments'));

addEvent(window, 'unload', contactForm.save);

總結下通過該模式實現保存功能的流程,首先,我們對頁面添加了監聽,一旦頁面加載,就調用組合對象的save方法,而該save函數,遍歷了其下的所有葉對象,並調用該對象的save函數來實現其save函數。

向接口添加方法

如果現在有業務更改,除了save函數,我還要在每次頁面加載後對所有對象執行restore操作。爲了增加這一功能,我想要修改FormItem接口,在接口中增加一個函數。如何實現呢?
//修改接口的定義
var FormItem = new Interface('FormItem', ['save', 'restore']);
//爲各個葉對象、組合對象添加對應的函數
Field.prototype.restore = function() {
  this.element.value = getCookie(this.id);
};

CompositeForm.prototype.restore = function() {
  for(var i = 0, len = this.formComponents.length; i < len; i++) {
    this.formComponents[i].restore();
  }
};
//添加觸發函數實現功能
addEvent(window, 'load', contactForm.restore);

更復雜一點的示例

好,第一個示例我們已經實現了,更進一步,如果現在我想對某幾個葉對象的組合執行某些操作呢?比如說, 我想在一定條件被觸發時,對某幾個葉對象執行save函數,而不是對所有葉對象執行該函數。這時,我們就需要添加非根級的組合對象了,也就是說,把這幾個想要操作的葉對象,視爲一個組合對象。

定義非根級組合對象

對於當前的非根級組合對象,並不受到根級組合對象的影響,比如,根級組合對象是通過數組來存儲其下的組合對象和葉對象,各非根級組合對象,可以用數組存儲葉對象,也可以用對象來存儲葉對象。那麼在下面,我們就舉一個用對象來存儲葉對象的代碼示例
var CompositeFieldset = function(id, legendText) { // implements Composite, FormItem
  this.components = {};

  this.element = document.createElement('fieldset');
  this.element.id = id;

  if(legendText) { // Create a legend if the optional second 
                   // argument is set.
    this.legend = document.createElement('legend');
    this.legend.appendChild(document.createTextNode(legendText);
    this.element.appendChild(this.legend);
  }
};

CompositeFieldset.prototype.add = function(child) {
  Interface.ensureImplements(child, Composite, FormItem);
  this.components[child.getElement().id] = child;
  this.element.appendChild(child.getElement());
};

CompositeFieldset.prototype.remove = function(child) {
  delete this.components[child.getElement().id];
};

CompositeFieldset.prototype.getChild = function(id) {
  if(this.components[id] != undefined) {
    return this.components[id];
  }
  else {
    return null;
  }
};

CompositeFieldset.prototype.save = function() {
  for(var id in this.components) {
    if(!this.components.hasOwnProperty(id)) continue;
    this.components[id].save();
  }
};

CompositeFieldset.prototype.restore = function() {
  for(var id in this.components) {
    if(!this.components.hasOwnProperty(id)) continue;
    this.components[id].restore();
  }
};

CompositeFieldset.prototype.getElement = function() { 
  return this.element; 
};

組合對象和葉對象匯合

var contactForm = new CompositeForm('contact-form', 'POST', 'contact.php');
//一個組合對象
var nameFieldset = new CompositeFieldset('name-fieldset');
nameFieldset.add(new InputField('first-name', 'First Name'));
nameFieldset.add(new InputField('last-name', 'Last Name'));
contactForm.add(nameFieldset);
//又一個組合對象
var addressFieldset = new CompositeFieldset('address-fieldset');
addressFieldset.add(new InputField('address', 'Address'));
addressFieldset.add(new InputField('city', 'City'));
addressFieldset.add(new SelectField('state', 'State', stateArray));
addressFieldset.add(new InputField('zip', 'Zip'));
contactForm.add(addressFieldset);

contactForm.add(new TextareaField('comments', 'Comments'));

body.appendChild(contactForm.getElement());

addEvent(window, 'unload', contactForm.save);
addEvent(window, 'load', contactForm.restore);
//對指定組合對象的操作
addEvent('save-button', 'click', nameFieldset.save);
addEvent('restore-button', 'click', nameFieldset.restore);


發佈了109 篇原創文章 · 獲贊 11 · 訪問量 23萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章