JavaScript 12种设计模式汇总

设计模式简介:

设计模式是可重用的用于解决软件设计中通常问题的方案。设计模式如此让人着迷,以致在任何编程语言中都有对其进行的探索。其中一个缘由是它可让咱们站在巨人的肩膀上,得到前人全部的经验,保证咱们以优雅的方式组织咱们的代码,知足咱们解决问题所须要的条件。设计模式一样也为咱们描述问题提供了通用的词汇。这比咱们经过代码来向别人传达语法和语义性的描述更为方便。下面介绍一些JavaScript里用到的设计模式:

一、构造器模式

在面向对象编程中,构造器是一个当新建对象的内存被分配后,用来初始化该对象的一个特殊函数。在JavaScript中几乎全部的东西都是对象,咱们常常会对对象的构造器十分感兴趣。对象构造器是被用来建立特殊类型的对象的,首先它要准备使用的对象,其次在对象初次被建立时,经过接收参数,构造器要用来对成员的属性和方法进行赋值。

1.1建立对象

  •  
// 第一种方式let obj = {};// 第二种方式let obj2 = Object.create( null );// 第三种方式let obj3 = new Object();

1.2设置对象的属性和方法

  •  
// 1. “点号”法// 设置属性obj.firstKey = "Hello World";// 获取属性let key = obj.firstKey;// 2. “方括号”法// 设置属性obj["firstKey"] = "Hello World";// 获取属性let key = newObject["firstKey"];// 方法1和2的区别在于用方括号的方式内能够写表达式// 3. Object.defineProperty方式// 设置属性Object.defineProperty(obj, "firstKey", {    value: "hello world",// 属性的值,默认为undefined    writable: true, // 是否可修改,默认为false    enumerable: true,// 是否可枚举(遍历),默认为false    configurable: true // 表示对象的属性是否能够被删除,以及除 value 和 writable 特性外的其余特性是否能够被修改。});// 若是上面的方式你感到难以阅读,能够简短的写成下面这样:let defineProp = function ( obj, key, value ){  let config = {};  config.value = value;  Object.defineProperty( obj, key, config );};// 4. Object.defineProperties方式(同时设置多个属性)// 设置属性Object.defineProperties( obj, {  "firstKey": {     value: "Hello World",     writable: true   },  "secondKey": {     value: "Hello World2",     writable: false   }});

1.3建立构造器

Javascript不支持类的概念,但它有一种与对象一块儿工做的构造器函数。使用new关键字来调用该函数,咱们能够告诉Javascript把这个函数当作一个构造器来用,它能够用本身所定义的成员来初始化一个对象。javascript

在这个构造器内部,关键字this引用到刚被建立的对象。回到对象建立,一个基本的构造函数看起来像这样:html

  •  
function Car( model, year, miles ) {  this.model = model;  this.year = year;  this.miles = miles;  this.toString = function () {    return this.model + " has done " + this.miles + " miles";  };}// 使用:// 咱们能够示例化一个Carlet civic = new Car( "Honda Civic", 2009, 20000 );let mondeo = new Car( "Ford Mondeo", 2010, 5000 );// 打开浏览器控制台查看这些对象toString()方法的输出值// output of the toString() method being called on// these objectsconsole.log( civic.toString() );console.log( mondeo.toString() );

上面是简单版本的构造器模式,但它仍是有些问题。一个是难以继承,另外一个是每一个Car构造函数建立的对象中,toString()之类的函数都被从新定义。这不是很是好,理想的状况是全部Car类型的对象都应该引用同一个函数。 java

在Javascript中函数有一个prototype的属性。当咱们调用Javascript的构造器建立一个对象时,构造函数prototype上的属性对于所建立的对象来讲都看见。照这样,就能够建立多个访问相同prototype的Car对象了。下面,咱们来扩展一下原来的例子:数据库

  •  
function Car( model, year, miles ) {  this.model = model;  this.year = year;  this.miles = miles;}Car.prototype.toString = function () {  return this.model + " has done " + this.miles + " miles";};// 使用:var civic = new Car( "Honda Civic", 2009, 20000 );var mondeo = new Car( "Ford Mondeo", 2010, 5000 );console.log( civic.toString() );console.log( mondeo.toString() );

经过上面代码,单个toString()实例被全部的Car对象所共享了。编程

二、模块化模式

模块是任何健壮的应用程序体系结构不可或缺的一部分,特色是有助于保持应用项目的代码单元既能清晰地分离又有组织。设计模式

在JavaScript中,实现模块有几个选项,他们包括:数组

  • 模块化模式浏览器

  • 对象表示法安全

  • AMD模块数据结构

  • Commonjs 模块

  • ECMAScript Harmony 模块

2.1对象字面值

对象字面值不要求使用新的操做实例,可是不可以在结构体开始使用,由于打开"{"可能被解释为一个块的开始。

  •  
let myModule = {  myProperty: "someValue",  // 对象字面值包含了属性和方法(properties and methods).  // 例如,咱们能够定义一个模块配置进对象:  myConfig: {    useCaching: true,    language: "en"  },  // 很是基本的方法  myMethod: function () {    console.log( "Where in the world is Paul Irish today?" );  },  // 输出基于当前配置configuration的一个值  myMethod2: function () {    console.log( "Caching is:" + ( this.myConfig.useCaching ) ? "enabled" : "disabled" );  },  // 重写当前的配置(configuration)  myMethod3: function( newConfig ) {    if ( typeof newConfig === "object" ) {      this.myConfig = newConfig;      console.log( this.myConfig.language );    }  }};myModule.myMethod();// Where in the world is Paul Irish today?myModule.myMethod2();// enabledmyModule.myMethod3({  language: "fr",  useCaching: false});// fr

2.2模块化模式

模块化模式最初被定义为一种对传统软件工程中的类提供私有和公共封装的方法。

在JavaScript中,模块化模式用来进一步模拟类的概念,经过这样一种方式:咱们能够在一个单一的对象中包含公共/私有的方法和变量,从而从全局范围中屏蔽特定的部分。

这个结果是能够减小咱们的函数名称与在页面中其余脚本区域定义的函数名称冲突的可能性。

模块模式使用闭包的方式来将"私有信息",状态和组织结构封装起来。提供了一种将公有和私有方法,变量封装混合在一块儿的方式,这种方式防止内部信息泄露到全局中,从而避免了和其它开发者接口发生冲图的可能性。

在这种模式下只有公有的API 会返回,其它将所有保留在闭包的私有空间中。

这种方法提供了一个比较清晰的解决方案,在只暴露一个接口供其它部分使用的状况下,将执行繁重任务的逻辑保护起来。这个模式很是相似于当即调用函数式表达式(IIFE-查看命名空间相关章节获取更多信息),可是这种模式返回的是对象,而当即调用函数表达式返回的是一个函数。

须要注意的是,在javascript事实上没有一个显式的真正意义上的"私有性"概念,由于与传统语言不一样,javascript没有访问修饰符。从技术上讲,变量不能被声明为公有的或者私有的,所以咱们使用函数域的方式去模拟这个概念。

在模块模式中,由于闭包的缘故,声明的变量或者方法只在模块内部有效。在返回对象中定义的变量或者方法能够供任何人使用。

  •  
let testModule = (function () {  let counter = 0;  return {    incrementCounter: function () {      return counter++;    },    resetCounter: function () {      console.log( "counter value prior to reset: " + counter );      counter = 0;    }  };})();testModule.incrementCounter();testModule.resetCounter();

在这里咱们看到,其它部分的代码不能直接访问咱们的incrementCounter() 或者 resetCounter()的值。counter变量被彻底从全局域中隔离起来了,所以其表现的就像一个私有变量同样,它的存在只局限于模块的闭包内部,所以只有两个函数能够访问counter。

咱们的方法是有名字空间限制的,所以在咱们代码的测试部分,咱们须要给全部函数调用前面加上模块的名字(例如"testModule")。

当使用模块模式时,咱们会发现经过使用简单的模板,对于开始使用模块模式很是有用。下面是一个模板包含了命名空间,公共变量和私有变量。

  •  
let myNamespace = (function () {  let myPrivateVar, myPrivateMethod;  myPrivateVar = 0;  myPrivateMethod = function( foo ) {      console.log( foo );  };  return {    myPublicVar: "foo",    myPublicFunction: function( bar ) {      myPrivateVar++;      myPrivateMethod( bar );    }  };})();

看一下另一个例子,下面咱们看到一个使用这种模式实现的购物车。这个模块彻底自包含在一个叫作basketModule 全局变量中。

模块中的购物车数组是私有的,应用的其它部分不能直接读取。只存在与模块的闭包中,所以只有能够访问其域的方法能够访问这个变量。

  •  
let basketModule = (function () {  let basket = [];  function doSomethingPrivate() {    //...  }  function doSomethingElsePrivate() {    //...  }  return {    addItem: function( values ) {      basket.push(values);    },    getItemCount: function () {      return basket.length;    },    doSomething: doSomethingPrivate,    getTotal: function () {      let q = this.getItemCount(),          p = 0;      while (q--) {        p += basket[q].price;      }      return p;    }  };}());

上面的方法都处于basketModule 的名字空间中。

请注意在上面的basket模块中 域函数是如何在咱们全部的函数中被封装起来的,以及咱们如何当即调用这个域函数,而且将返回值保存下来。这种方式有如下的优点:

  • 能够建立只能被咱们模块访问的私有函数。这些函数没有暴露出来(只有一些API是暴露出来的),它们被认为是彻底私有的。

  • 当咱们在一个调试器中,须要发现哪一个函数抛出异常的时候,能够很容易的看到调用栈,由于这些函数是正常声明的而且是命名的函数。

  • 这种模式一样可让咱们在不一样的状况下返回不一样的函数。我见过有开发者使用这种技巧用于执行测试,目的是为了在他们的模块里面针对IE专门提供一条代码路径,可是如今咱们也能够简单的使用特征检测达到相同的目的。

2.3Import mixins(导入混合)

这个变体展现了如何将全局(例如 jQuery, Underscore)做为一个参数传入模块的匿名函数。这种方式容许咱们导入全局,而且按照咱们的想法在本地为这些全局起一个别名。

  •  
let myModule = (function ( jQ, _ ) {    function privateMethod1(){        jQ(".container").html("test");    }    function privateMethod2(){      console.log( _.min([10, 5, 100, 2, 1000]) );    }    return{        publicMethod: function(){            privateMethod1();                       }               };}( jQuery, _ ));// 将JQ和lodash导入myModule.publicMethod();

2.4Exports(导出)

这个变体容许咱们声明全局对象而不用使用它们。

  •  
let myModule = (function () {  let module = {},    privateVariable = "Hello World";  function privateMethod() {    // ...  }  module.publicProperty = "Foobar";  module.publicMethod = function () {    console.log( privateVariable );  };  return module;}());

2.5其它框架特定的模块模式实现

Dojo:

Dojo提供了一个方便的方法 dojo.setObject() 来设置对象。这须要将以"."符号为第一个参数的分隔符,如:myObj.parent.child 是指定义在"myOjb"内部的一个对象“parent”,它的一个属性为"child"。

使用setObject()方法容许咱们设置children 的值,能够建立路径传递过程当中的任何对象即便这些它们根本不存在。

例如,若是咱们声明商店命名空间的对象basket.coreas,可使用以下方式:

  •  
let store = window.store || {};
if ( !store["basket"] ) { store.basket = {};}
if ( !store.basket["core"] ) { store.basket.core = {};}
store.basket.core = { key:value,};

Extjs:

  •  
// create namespaceExt.namespace("myNameSpace");// create applicationmyNameSpace.app = function () {  // do NOT access DOM from here; elements don't exist yet  // private variables  let btn1,      privVar1 = 11;  // private functions  let btn1Handler = function ( button, event ) {      console.log( "privVar1=" + privVar1 );      console.log( "this.btn1Text=" + this.btn1Text );    };  // public space  return {    // public properties, e.g. strings to translate    btn1Text: "Button 1",    // public methods    init: function () {      if ( Ext.Ext2 ) {        btn1 = new Ext.Button({          renderTo: "btn1-ct",          text: this.btn1Text,          handler: btn1Handler        });      } else {        btn1 = new Ext.Button( "btn1-ct", {          text: this.btn1Text,          handler: btn1Handler        });      }    }  };}();

jQuery:

由于jQuery编码规范没有规定插件如何实现模块模式,所以有不少种方式能够实现模块模式。Ben Cherry 之间提供一种方案,由于模块之间可能存在大量的共性,所以经过使用函数包装器封装模块的定义。

在下面的例子中,定义了一个library 函数,这个函数声明了一个新的库,而且在新的库(例如 模块)建立的时候,自动将初始化函数绑定到document的ready上。

  •  
function library( module ) {  $( function() {    if ( module.init ) {      module.init();    }  });  return module;}let myLibrary = library(function () {  return {    init: function () {      // module implementation    }  };}());

优势:

既然咱们已经看到单例模式颇有用,为何仍是使用模块模式呢?首先,对于有面向对象背景的开发者来说,至少从javascript语言上来说,模块模式相对于真正的封装概念更清晰。

其次,模块模式支持私有数据-所以,在模块模式中,公共部分代码能够访问私有数据,可是在模块外部,不能访问类的私有部分(没开玩笑!感谢David Engfer 的玩笑)。

缺点:

模块模式的缺点是由于咱们采用不一样的方式访问公有和私有成员,所以当咱们想要改变这些成员的可见性的时候,咱们不得不在全部使用这些成员的地方修改代码。

咱们也不能在对象以后添加的方法里面访问这些私有变量。也就是说,不少状况下,模块模式颇有用,而且当使用正确的时候,潜在地能够改善咱们代码的结构。

其它缺点包括不能为私有成员建立自动化的单元测试,以及在紧急修复bug时所带来的额外的复杂性。根本没有可能能够对私有成员打补丁。

相反地,咱们必须覆盖全部的使用存在bug私有成员的公共方法。开发者不能简单的扩展私有成员,所以咱们须要记得,私有成员并不是它们表面上看上去那么具备扩展性。

三、单例模式

单例模式之因此这么叫,是由于它限制一个类只能有一个实例化对象。经典的实现方式是,建立一个类,这个类包含一个方法,这个方法在没有对象存在的状况下,将会建立一个新的实例对象。若是对象存在,这个方法只是返回这个对象的引用。

在JavaScript语言中, 单例服务做为一个从全局空间的代码实现中隔离出来共享的资源空间是为了提供一个单独的函数访问指针。

咱们能像这样实现一个单例:

  •  
let mySingleton = (function () {  // Instance stores a reference to the Singleton  let instance;  function init() {    // 单例    // 私有方法和变量    function privateMethod(){        console.log( "I am private" );    }    let privateVariable = "Im also private";    let privateRandomNumber = Math.random();    return {      // 共有方法和变量      publicMethod: function () {        console.log( "The public can see me!" );      },      publicProperty: "I am also public",      getRandomNumber: function() {        return privateRandomNumber;      }    };  };  return {    // 若是存在获取此单例实例,若是不存在建立一个单例实例    getInstance: function () {      if ( !instance ) {        instance = init();      }      return instance;    }  };})();
let myBadSingleton = (function () { // 存储单例实例的引用 var instance; function init() { // 单例 let privateRandomNumber = Math.random(); return { getRandomNumber: function() { return privateRandomNumber; } }; }; return { // 老是建立一个新的实例 getInstance: function () { instance = init(); return instance; } };})();
// 使用:let singleA = mySingleton.getInstance();let singleB = mySingleton.getInstance();console.log( singleA.getRandomNumber() === singleB.getRandomNumber() ); // true
let badSingleA = myBadSingleton.getInstance();let badSingleB = myBadSingleton.getInstance();console.log( badSingleA.getRandomNumber() !== badSingleB.getRandomNumber() ); // true

建立一个全局访问的单例实例 (一般经过 MySingleton.getInstance()) 由于咱们不能(至少在静态语言中) 直接调用 new MySingleton() 建立实例. 这在JavaScript语言中是不可能的。

在GoF的书里面,单例模式的应用描述以下:

  • 每一个类只有一个实例,这个实例必须经过一个广为人知的接口,来被客户访问。

  • 子类若是要扩展这个惟一的实例,客户能够不用修改代码就能使用这个扩展后的实例。

关于第二点,能够参考以下的实例,咱们须要这样编码:

  •  
mySingleton.getInstance = function(){  if ( this._instance == null ) {    if ( isFoo() ) {       this._instance = new FooSingleton();    } else {       this._instance = new BasicSingleton();    }  }  return this._instance;};

在这里,getInstance 有点相似于工厂方法,咱们不须要去更新每一个访问单例的代码。FooSingleton能够是BasicSinglton的子类,而且实现了相同的接口。

尽管单例模式有着合理的使用需求,可是一般当咱们发现本身须要在javascript使用它的时候,这是一种信号,代表咱们可能须要去从新评估本身的设计。

这一般代表系统中的模块要么紧耦合要么逻辑过于分散在代码库的多个部分。单例模式更难测试,由于可能有多种多样的问题出现,例如隐藏的依赖关系,很难去建立多个实例,很难清理依赖关系,等等。

四、观察者模式

观察者模式是这样一种设计模式:一个被称做被观察者的对象,维护一组被称为观察者的对象,这些对象依赖于被观察者,被观察者自动将自身的状态的任何变化通知给它们。

当一个被观察者须要将一些变化通知给观察者的时候,它将采用广播的方式,这条广播可能包含特定于这条通知的一些数据。

当特定的观察者再也不须要接受来自于它所注册的被观察者的通知的时候,被观察者能够将其从所维护的组中删除。在这里说起一下设计模式现有的定义颇有必要。这个定义是与所使用的语言无关的。

经过这个定义,最终咱们能够更深层次地了解到设计模式如何使用以及其优点。在gof的《设计模式:可重用的面向对象软件的元素》这本书中,是这样定义观察者模式的:

一个或者更多的观察者对一个被观察者的状态感兴趣,将自身的这种兴趣经过附着自身的方式注册在被观察者身上。当被观察者发生变化,而这种即可也是观察者所关心的,就会产生一个通知,这个通知将会被送出去,最后将会调用每一个观察者的更新方法。当观察者不在对被观察者的状态感兴趣的时候,它们只须要简单的将自身剥离便可。

咱们如今能够经过实现一个观察者模式来进一步扩展咱们刚才所学到的东西。这个实现包含一下组件:

  • 被观察者:维护一组观察者, 提供用于增长和移除观察者的方法。

  • 观察者:提供一个更新接口,用于当被观察者状态变化时,获得通知。

  • 具体的被观察者:状态变化时广播通知给观察者,保持具体的观察者的信息。

  • 具体的观察者:保持一个指向具体被观察者的引用,实现一个更新接口,用于观察,以便保证自身状态老是和被观察者状态一致的。

首先,让咱们对被观察者可能有的一组依赖其的观察者进行建模:

  •  
function ObserverList(){  this.observerList = [];}ObserverList.prototype.Add = function( obj ){  return this.observerList.push( obj );};ObserverList.prototype.Empty = function(){  this.observerList = [];};ObserverList.prototype.Count = function(){  return this.observerList.length;};ObserverList.prototype.Get = function( index ){  if( index > -1 && index < this.observerList.length ){    return this.observerList[ index ];  }};ObserverList.prototype.Insert = function( obj, index ){  let pointer = -1;  if( index === 0 ){    this.observerList.unshift( obj );    pointer = index;  }else if( index === this.observerList.length ){    this.observerList.push( obj );    pointer = index;  }  return pointer;};ObserverList.prototype.IndexOf = function( obj, startIndex ){  let i = startIndex, pointer = -1;  while( i < this.observerList.length ){    if( this.observerList[i] === obj ){      pointer = i;    }    i++;  }  return pointer;};ObserverList.prototype.RemoveAt = function( index ){  if( index === 0 ){    this.observerList.shift();  }else if( index === this.observerList.length -1 ){    this.observerList.pop();  }};// Extend an object with an extensionfunction extend( extension, obj ){  for ( let key in extension ){    obj[key] = extension[key];  }}

接着,咱们对被观察者以及其增长,删除,通知在观察者列表中的观察者的能力进行建模:

  •  
function Subject(){  this.observers = new ObserverList();}Subject.prototype.AddObserver = function( observer ){  this.observers.Add( observer );}; Subject.prototype.RemoveObserver = function( observer ){  this.observers.RemoveAt( this.observers.IndexOf( observer, 0 ) );}; Subject.prototype.Notify = function( context ){  let observerCount = this.observers.Count();  for(let i=0; i < observerCount; i++){    this.observers.Get(i).Update( context );  }};

咱们接着定义创建新的观察者的一个框架。这里的update 函数以后会被具体的行为覆盖。

  •  
// The Observerfunction Observer(){  this.Update = function(){    // ...  };}

在咱们的样例应用里面,咱们使用上面的观察者组件,如今咱们定义:

  • 一个按钮,这个按钮用于增长新的充当观察者的选择框到页面上

  • 一个控制用的选择框 , 充当一个被观察者,通知其它选择框是否应该被选中

  • 一个容器,用于放置新的选择框

咱们接着定义具体被观察者和具体观察者,用于给页面增长新的观察者,以及实现更新接口。经过查看下面的内联的注释,搞清楚在咱们样例中的这些组件是如何工做的。

html

  •  
<button id="addNewObserver">Add New Observer checkbox</button><input id="mainCheckbox" type="checkbox"/><div id="observersContainer"></div>

Javascript

  •  
// 咱们DOM 元素的引用let controlCheckbox = document.getElementById("mainCheckbox"),  addBtn = document.getElementById( "addNewObserver" ),  container = document.getElementById( "observersContainer" );// 具体的被观察者//Subject 类扩展controlCheckbox 类extend( new Subject(), controlCheckbox );//点击checkbox 将会触发对观察者的通知controlCheckbox["onclick"] = new Function("controlCheckbox.Notify(controlCheckbox.checked)");addBtn["onclick"] = AddNewObserver;// 具体的观察者function AddNewObserver(){  //创建一个新的用于增长的checkbox  let check  = document.createElement( "input" );  check.type = "checkbox";  // 使用Observer 类扩展checkbox  extend( new Observer(), check );  // 使用定制的Update函数重载  check.Update = function( value ){    this.checked = value;  };  // 增长新的观察者到咱们主要的被观察者的观察者列表中  controlCheckbox.AddObserver( check );  // 将元素添加到容器的最后  container.appendChild( check );}

在这个例子里面,咱们看到了如何实现和配置观察者模式,了解了被观察者,观察者,具体被观察者,具体观察者的概念。

观察者模式和发布/订阅模式的不一样

观察者模式确实颇有用,可是在javascript时间里面,一般咱们使用一种叫作发布/订阅模式的变体来实现观察者模式。这两种模式很类似,可是也有一些值得注意的不一样。

观察者模式要求想要接受相关通知的观察者必须到发起这个事件的被观察者上注册这个事件。

发布/订阅模式使用一个主题/事件频道,这个频道处于想要获取通知的订阅者和发起事件的发布者之间。

这个事件系统容许代码定义应用相关的事件,这个事件能够传递特殊的参数,参数中包含有订阅者所须要的值。这种想法是为了不订阅者和发布者之间的依赖性。

这种和观察者模式之间的不一样,使订阅者能够实现一个合适的事件处理函数,用于注册和接受由发布者广播的相关通知。

这里给出一个关于如何使用发布者/订阅者模式的例子,这个例子中完整地实现了功能强大的publish(), subscribe() 和 unsubscribe()。

  •  
// 一个很是简单的邮件处理器// 接受的消息的计数器let mailCounter = 0;// 初始化一个订阅者,这个订阅者监听名叫"inbox/newMessage" 的频道// 渲染新消息的粗略信息let subscriber1 = subscribe( "inbox/newMessage", function( topic, data ) {  // 日志记录主题,用于调试  console.log( "A new message was received: ", topic );  // 使用来自于被观察者的数据,用于给用户展现一个消息的粗略信息  $( ".messageSender" ).html( data.sender );  $( ".messagePreview" ).html( data.body );});// 这是另一个订阅者,使用相同的数据执行不一样的任务// 更细计数器,显示当前来自于发布者的新信息的数量let subscriber2 = subscribe( "inbox/newMessage", function( topic, data ) {  $('.newMessageCounter').html( mailCounter++ );});publish( "inbox/newMessage", [{  sender:"hello@google.com",  body: "Hey there! How are you doing today?"}]);// 在以后,咱们可让咱们的订阅者经过下面的方式取消订阅来自于新主题的通知// unsubscribe( subscriber1,  );// unsubscribe( subscriber2 );

这个例子的更广的意义是对松耦合的原则的一种推崇。不是一个对象直接调用另一个对象的方法,而是经过订阅另一个对象的一个特定的任务或者活动,从而在这个任务或者活动出现的时候的获得通知。

优势

观察者和发布/订阅模式鼓励人们认真考虑应用不一样部分之间的关系,同时帮助咱们找出这样的层,该层中包含有直接的关系,这些关系能够经过一些列的观察者和被观察者来替换掉。

这中方式能够有效地将一个应用程序切割成小块,这些小块耦合度低,从而改善代码的管理,以及用于潜在的代码复用。

使用观察者模式更深层次的动机是,当咱们须要维护相关对象的一致性的时候,咱们能够避免对象之间的紧密耦合。例如,一个对象能够通知另一个对象,而不须要知道这个对象的信息。

两种模式下,观察者和被观察者之间均可以存在动态关系。这提供很好的灵活性,而当咱们的应用中不一样的部分之间紧密耦合的时候,是很难实现这种灵活性的。

尽管这些模式并非万能的灵丹妙药,这些模式仍然是做为最好的设计松耦合系统的工具之一,所以在任何的JavaScript 开发者的工具箱里面,都应该有这样一个重要的工具。

缺点

事实上,这些模式的一些问题实际上正是来自于它们所带来的一些好处。在发布/订阅模式中,将发布者共订阅者上解耦,将会在一些状况下,致使很难确保咱们应用中的特定部分按照咱们预期的那样正常工做。

例如,发布者能够假设有一个或者多个订阅者正在监听它们。好比咱们基于这样的假设,在某些应用处理过程当中来记录或者输出错误日志。若是订阅者执行日志功能崩溃了(或者由于某些缘由不能正常工做),由于系统自己的解耦本质,发布者没有办法感知到这些事情。

另一个这种模式的缺点是,订阅者对彼此之间存在没有感知,对切换发布者的代价无从得知。由于订阅者和发布者之间的动态关系,更新依赖也很能去追踪。

让咱们看一下最小的一个版本的发布/订阅模式实现。这个实现展现了发布,订阅的核心概念,以及如何取消订阅。

  •  
let pubsub = {};(function(q) {    let topics = {},        subUid = -1;    q.publish = function( topic, args ) {        if ( !topics[topic] ) {            return false;        }        let subscribers = topics[topic],            len = subscribers ? subscribers.length : 0;        while (len--) {            subscribers[len].func( topic, args );        }        return this;    };    q.subscribe = function( topic, func ) {        if (!topics[topic]) {            topics[topic] = [];        }        let token = ( ++subUid ).toString();        topics[topic].push({            token: token,            func: func        });        return token;    };    q.unsubscribe = function( token ) {        for ( let m in topics ) {            if ( topics[m] ) {                for ( let i = 0, j = topics[m].length; i < j; i++ ) {                    if ( topics[m][i].token === token) {                        topics[m].splice( i, 1 );                        return token;                    }                }            }        }        return this;    };}( pubsub ));

咱们如今可使用发布实例和订阅感兴趣的事件,例如:

  •  
let messageLogger = function ( topics, data ) {    console.log( "Logging: " + topics + ": " + data );};let subscription = pubsub.subscribe( "inbox/newMessage", messageLogger );pubsub.publish( "inbox/newMessage", "hello world!" );// orpubsub.publish( "inbox/newMessage", ["test", "a", "b", "c"] );// orpubsub.publish( "inbox/newMessage", {  sender: "hello@google.com",  body: "Hey again!"});// We cab also unsubscribe if we no longer wish for our subscribers// to be notified// pubsub.unsubscribe( subscription );pubsub.publish( "inbox/newMessage", "Hello! are you still there?" );

观察者模式在应用设计中,解耦一系列不一样的场景上很是有用,若是你没有用过它,我推荐你尝试一下今天提到的以前写到的某个实现。这个模式是一个易于学习的模式,同时也是一个威力巨大的模式。

五、中介者模式

若是系统组件之间存在大量的直接关系,就多是时候,使用一个中心的控制点,来让不一样的组件经过它来通讯。中介者经过将组件之间显式的直接的引用替换成经过中心点来交互的方式,来作到松耦合。这样能够帮助咱们解耦,和改善组件的重用性。

在现实世界中,相似的系统就是,飞行控制系统。一个航站塔(中介者)处理哪一个飞机能够起飞,哪一个能够着陆,由于全部的通讯(监听的通知或者广播的通知)都是飞机和控制塔之间进行的,而不是飞机和飞机之间进行的。一个中央集权的控制中心是这个系统成功的关键,也正是中介者在软件设计领域中所扮演的角色。

5.1基础的实现

中间人模式的一种简单的实现能够在下面找到,publish()和subscribe()方法都被暴露出来使用:

  •  
let mediator = (function(){    let topics = {};    let subscribe = function( topic, fn ){        if ( !topics[topic] ){          topics[topic] = [];        }        topics[topic].push( { context: this, callback: fn } );        return this;    };    let publish = function( topic ){        let args;        if ( !topics[topic] ){          return false;        }        args = Array.prototype.slice.call( arguments, 1 );        for ( let i = 0, l = topics[topic].length; i < l; i++ ) {            let subscription = topics[topic][i];            subscription.callback.apply( subscription.context, args );        }        return this;    };    return {        publish: publish,        subscribe: subscribe,        installTo: function( obj ){            obj.subscribe = subscribe;            obj.publish = publish;        }    };}());

优势 & 缺点

中间人模式最大的好处就是,它节约了对象或者组件之间的通讯信道,这些对象或者组件存在于从多对多到多对一的系统之中。因为解耦合水平的因素,添加新的发布或者订阅者是相对容易的。

也许使用这个模式最大的缺点是它能够引入一个单点故障。在模块之间放置一个中间人也可能会形成性能损失,由于它们常常是间接地的进行通讯的。因为松耦合的特性,仅仅盯着广播很难去确认系统是如何作出反应的。

这就是说,提醒咱们本身解耦合的系统拥有许多其它的好处,是颇有用的——若是咱们的模块互相之间直接的进行通讯,对于模块的改变(例如:另外一个模块抛出了异常)能够很容易的对咱们系统的其它部分产生多米诺连锁效应。这个问题在解耦合的系统中不多须要被考虑到。

在一天结束的时候,紧耦合会致使各类头痛,这仅仅只是另一种可选的解决方案,可是若是获得正确实现的话也可以工做得很好。

六、原型模式

原型模式是指经过克隆的方式基于一个现有对象的模板建立对象的模式。

咱们可以将原型模式认做是基于原型的继承中,咱们建立做为其它对象原型的对象.原型对象自身被当作构造器建立的每个对象的蓝本高效的使用着.若是构造器函数使用的原型包含例如叫作name的属性,那么每个经过同一个构造器建立的对象都将拥有这个相同的属性。

咱们能够在下面的示例中看到对这个的展现:

  •  
let myCar = {  name: "Ford Escort",  drive: function () {    console.log( "Weeee. I'm driving!" );  },  panic: function () {    console.log( "Wait. How do you stop this thing?" );  }};let yourCar = Object.create( myCar );console.log( yourCar.name );// Ford Escort

Object.create也容许咱们简单的继承先进的概念,好比对象可以直接继承自其它对象,这种不一样的继承.咱们早先也看到Object.create容许咱们使用 供应的第二个参数来初始化对象属性。例如:

  •  
let vehicle = {  getModel: function () {    console.log( "The model of this vehicle is.." + this.model );  }};let car = Object.create(vehicle, {  "id": {    value: "1",    // writable:false, configurable:false by default    enumerable: true  },  "model": {    value: "Ford",    enumerable: true  }});

这里的属性能够被Object.create的第二个参数来初始化,使用一种相似于Object.defineProperties和Object.defineProperties方法所使用语法的对象字面值。

在枚举对象的属性,和在一个hasOwnProperty()检查中封装循环的内容时,原型关系会形成麻烦,这一事实是值得咱们关注的。

若是咱们但愿在不直接使用Object.create的前提下实现原型模式,咱们能够像下面这样,按照上面的示例,模拟这一模式:

  •  
let vehiclePrototype = {  init: function ( carModel ) {    this.model = carModel;  },  getModel: function () {    console.log( "The model of this vehicle is.." + this.model);  }};function vehicle( model ) {  function F() {};  F.prototype = vehiclePrototype;  let f = new F();  f.init( model );  return f;}let car = vehicle( "Ford Escort" );car.getModel();

注意:这种可选的方式不容许用户使用相同的方式定义只读的属性(由于若是不当心的话vehicle原型可能会被改变)。

原型模式的最后一种可选实现能够像下面这样:

  •  
let beget = (function () {    function F() {}    return function ( proto ) {        F.prototype = proto;        return new F();    };})();

七、命令模式

命名模式的目标是将方法的调用,请求或者操做封装到一个单独的对象中,给咱们酌情执行同时参数化和传递方法调用的能力.另外,它使得咱们能将对象从实现了行为的对象对这些行为的调用进行解耦,为咱们带来了换出具体的对象这一更深程度的总体灵活性。

具体类是对基于类的编程语言的最好解释,而且同抽象类的理念联系紧密。抽象类定义了一个接口,但并不须要提供对它的全部成员函数的实现。它扮演着驱动其它类的基类角色.被驱动类实现了缺失的函数而被称为具体类.。命令模式背后的通常理念是为咱们提供了从任何执行中的命令中分离出发出命令的责任,取而代之将这一责任委托给其它的对象。

实现明智简单的命令对象,将一个行为和对象对调用这个行为的需求都绑定到了一块儿.它们始终都包含一个执行操做(好比run()或者execute()).全部带有相同接口的命令对象可以被简单地根据须要调换,这被认为是命令模式的更大的好处之一。

为了展现命令模式,咱们建立一个简单的汽车购买服务:

  •  
(function(){  let CarManager = {      requestInfo: function( model, id ){        return "The information for " + model + " with ID " + id + " is foobar";      },      buyVehicle: function( model, id ){        return "You have successfully purchased Item " + id + ", a " + model;      },      arrangeViewing: function( model, id ){        return "You have successfully booked a viewing of " + model + " ( " + id + " ) ";      }    };})();

看一看上面的这段代码,它也许是经过直接访问对象来琐碎的调用咱们CarManager的方法。在技术上咱们也许都会都会对这个没有任何失误达成谅解.它是彻底有效的Javascript然而也会有状况不利的状况。

例如,想象若是CarManager的核心API会发生改变的这种状况.这可能须要全部直接访问这些方法的对象也跟着被修改.这能够被当作是一种耦合,明显违背了OOP方法学尽可能实现松耦合的理念.取而代之,咱们能够经过更深刻的抽象这些API来解决这个问题。

如今让咱们来扩展咱们的CarManager,以便咱们这个命令模式的应用程序获得接下来的这种效果:接受任何能够在CarManager对象上面执行的方法,传送任何能够被使用到的数据,如Car模型和ID。

这里是咱们但愿可以实现的样子:

  •  
CarManager.execute( "buyVehicle", "Ford Escort", "453543" );

按照这种结构,咱们如今应该像下面这样,添加一个对于"CarManager.execute()"方法的定义:

  •  
CarManager.execute = function ( name ) {    return CarManager[name] && CarManager[name].apply( CarManager, [].slice.call(arguments, 1) );};

最终咱们的调用以下所示:

  •  
CarManager.execute( "arrangeViewing", "Ferrari", "14523" );CarManager.execute( "requestInfo", "Ford Mondeo", "54323" );CarManager.execute( "requestInfo", "Ford Escort", "34232" );CarManager.execute( "buyVehicle", "Ford Escort", "34232" );

八、外观模式

当咱们提出一个门面,咱们要向这个世界展示的是一个外观,这一外观可能藏匿着一种很是不同凡响的真实。这就是咱们即将要回顾的模式背后的灵感——门面模式。

这一模式提供了面向一种更大型的代码体提供了一个的更高级别的温馨的接口,隐藏了其真正的潜在复杂性。

把这一模式想象成要是呈现给开发者简化的API,一些老是会提高使用性能的东西。

为了在咱们所学的基础上进行构建,门面模式同时须要简化一个类的接口,和把类同使用它的代码解耦。这给予了咱们使用一种方式直接同子系统交互的能力,这一方式有时候会比直接访问子系统更加不容易出错。

门面的优点包括易用,还有经常实现起这个模式来只是一小段路,不费力。

让咱们经过实践来看看这个模式。这是一个没有通过优化的代码示例,可是这里咱们使用了一个门面来简化跨浏览器事件监听的接口。咱们建立了一个公共的方法来实现,此方法可以被用在检查特性的存在的代码中,以便这段代码可以提供一种安全和跨浏览器兼容方案。

  •  
let addMyEvent = function( el,ev,fn ){   if( el.addEventListener ){       el.addEventListener( ev,fn, false );   }else if(el.attachEvent){       el.attachEvent( "on" + ev, fn );   }else{       el["on" + ev] = fn;   }};

门面不只仅只被用在它们本身身上,它们也可以被用来同其它的模式诸如模块模式进行集成。如咱们在下面所看到的,咱们模块模式的实体包含许多被定义为私有的方法。门面则被用来提供访问这些方法的更加简单的API:

  •  
let module = (function() {    let _private = {        i:5,        get : function() {            console.log( "current value:" + this.i);        },        set : function( val ) {            this.i = val;        },        run : function() {            console.log( "running" );        },        jump: function(){            console.log( "jumping" );        }    };    return {        facade : function( args ) {            _private.set(args.val);            _private.get();            if ( args.run ) {                _private.run();            }        }    };}());module.facade( {run: true, val:10} );// "current value: 10" and "running"

在这个示例中,调用module.facade()将会触发一堆模块中的私有方法。但再一次,用户并不须要关心这些。咱们已经使得对用户而言不须要担忧实现级别的细节就能消受一种特性。

九、工厂模式

工厂模式是另一种关注对象建立概念的建立模式。它的领域中同其它模式的不一样之处在于它并无明确要求咱们使用一个构造器。

取而代之,一个工厂能提供一个建立对象的公共接口,咱们能够在其中指定咱们但愿被建立的工厂对象的类型。

下面咱们经过使用构造器模式逻辑来定义汽车。这个例子展现了Vehicle 工厂可使用工厂模式来实现。

  •  
function Car( options ) {  this.doors = options.doors || 4;  this.state = options.state || "brand new";  this.color = options.color || "silver";
}function Truck( options){ this.state = options.state || "used"; this.wheelSize = options.wheelSize || "large"; this.color = options.color || "blue";}function VehicleFactory() {}VehicleFactory.prototype.vehicleClass = Car;VehicleFactory.prototype.createVehicle = function ( options ) { if( options.vehicleType === "car" ){ this.vehicleClass = Car; }else{ this.vehicleClass = Truck; } return new this.vehicleClass( options );
};let carFactory = new VehicleFactory();let car = carFactory.createVehicle( { vehicleType: "car", color: "yellow", doors: 6 } );console.log( car );

什么时候使用工厂模式

当被应用到下面的场景中时,工厂模式特别有用:

  • 当咱们的对象或者组件设置涉及到高程度级别的复杂度时。

  • 当咱们须要根据咱们所在的环境方便的生成不一样对象的实体时。

  • 当咱们在许多共享同一个属性的许多小型对象或组件上工做时。

  • 当带有其它仅仅须要知足一种API约定(又名鸭式类型)的对象的组合对象工做时.这对于解耦来讲是有用的。

什么时候不要去使用工厂模式

当被应用到错误的问题类型上时,这一模式会给应用程序引入大量没必要要的复杂性.除非为建立对象提供一个接口是咱们编写的库或者框架的一个设计上目标,不然我会建议使用明确的构造器,以免没必要要的开销。

因为对象的建立过程被高效的抽象在一个接口后面的事实,这也会给依赖于这个过程可能会有多复杂的单元测试带来问题。

抽象工厂

了解抽象工厂模式也是很是实用的,它的目标是以一个通用的目标将一组独立的工厂进行封装.它将一堆对象的实现细节从它们的通常用例中分离。

抽象工厂应该被用在一种必须从其建立或生成对象的方式处独立,或者须要同多种类型的对象一块儿工做,这样的系统中。

简单且容易理解的例子就是一个发动机工厂,它定义了获取或者注册发动机类型的方式。抽象工厂会被命名为AbstractVehicleFactory。抽象工厂将容许像"car"或者"truck"的发动机类型的定义,而且构造工厂将仅实现知足发动机合同的类.(例如:Vehicle.prototype.driven和Vehicle.prototype.breakDown)。

  •  
let AbstractVehicleFactory = (function () {    let types = {};    return {        getVehicle: function ( type, customizations ) {            var Vehicle = types[type];            return (Vehicle ? new Vehicle(customizations) : null);        },        registerVehicle: function ( type, Vehicle ) {            let proto = Vehicle.prototype;            // only register classes that fulfill the vehicle contract            if ( proto.drive && proto.breakDown ) {                types[type] = Vehicle;            }            return AbstractVehicleFactory;        }    };})();
AbstractVehicleFactory.registerVehicle( "car", Car );AbstractVehicleFactory.registerVehicle( "truck", Truck );
let car = AbstractVehicleFactory.getVehicle( "car" , { color: "lime green", state: "like new" } );
let truck = AbstractVehicleFactory.getVehicle( "truck" , { wheelSize: "medium", color: "neon yellow" } );

十、Mixin 模式

mixin模式指一些提供可以被一个或者一组子类简单继承功能的类,意在重用其功能。

子类划分

子类划分是一个参考了为一个新对象继承来自一个基类或者超类对象的属性的术语.在传统的面向对象编程中,类B可以从另一个类A处扩展。这里咱们将A看作是超类,而将B看作是A的子类。如此,全部B的实体都从A处继承了其A的方法,然而B仍然可以定义它本身的方法,包括那些重载的本来在A中的定义的方法。

B是否应该调用已经被重载的A中的方法,咱们将这个引述为方法链.B是否应该调用A(超类)的构造器,咱们将这称为构造器链。

为了演示子类划分,首先咱们须要一个可以建立自身新实体的基对象。

  •  
let Person =  function( firstName , lastName ){  this.firstName = firstName;  this.lastName =  lastName;  this.gender = "male";};

接下来,咱们将制定一个新的类(对象),它是一个现有的Person对象的子类.让咱们想象咱们想要加入一个不一样属性用来分辨一个Person和一个继承了Person"超类"属性的Superhero.因为超级英雄分享了通常人类许多共有的特征(例如:name,gender),所以这应该颇有但愿充分展现出子类划分是如何工做的。

  •  
let clark = new Person( "Clark" , "Kent" );let Superhero = function( firstName, lastName , powers ){    Person.call( this, firstName, lastName );    this.powers = powers;};SuperHero.prototype = Object.create( Person.prototype );let superman = new Superhero( "Clark" ,"Kent" , ["flight","heat-vision"] );console.log( superman );

Superhero构造器建立了一个自Peroson降低的对象。这种类型的对象拥有链中位于它之上的对象的属性,并且若是咱们在Person对象中设置了默认的值,Superhero可以使用特定于它的对象的值覆盖任何继承的值。

Mixin(织入目标类)

在Javascript中,咱们会将从Mixin继承看做是经过扩展收集功能的一种途径.咱们定义的每个新的对象都有一个原型,从其中它能够继承更多的属性.原型能够从其余对象继承而来,可是更重要的是,可以为任意数量的对象定义属性.咱们能够利用这一事实来促进功能重用。

Mix容许对象以最小量的复杂性从它们那里借用(或者说继承)功能.做为一种利用Javascript对象原型工做得很好的模式,它为咱们提供了从不止一个Mix处分享功能的至关灵活,但比多继承有效得多得多的方式。

它们能够被看作是其属性和方法能够很容易的在其它大量对象原型共享的对象.想象一下咱们定义了一个在一个标准对象字面量中含有实用功能的Mixin,以下所示:

  •  
let myMixins = {
moveUp: function(){ console.log( "move up" ); },
moveDown: function(){ console.log( "move down" ); },
stop: function(){ console.log( "stop! in the name of love!" ); }
};

而后咱们能够方便的扩展示有构造器功能的原型,使其包含这种使用一个 以下面的score.js_.extends()方法辅助器的行为:

  •  
function carAnimator(){  this.moveLeft = function(){    console.log( "move left" );  };}function personAnimator(){  this.moveRandomly = function(){ /*..*/ };}_.extend( carAnimator.prototype, myMixins );_.extend( personAnimator.prototype, myMixins );let myAnimator = new carAnimator();myAnimator.moveLeft();myAnimator.moveDown();myAnimator.stop();

如咱们所见,这容许咱们将通用的行为轻易的"混"入至关普通对象构造器中。

在接下来的示例中,咱们有两个构造器:一个Car和一个Mixin.咱们将要作的是静Car参数化(另一种说法是扩展),以便它可以继承Mixin中的特定方法,名叫driveForwar()和driveBackward().这一次咱们不会使用Underscore.js。

取而代之,这个示例将演示如何将一个构造器参数化,以便在无需重复每个构造器函数过程的前提下包含其功能。

  •  
let Car = function ( settings ) {    this.model = settings.model || "no model provided";    this.color = settings.color || "no colour provided";};// Mixinlet Mixin = function () {};Mixin.prototype = {    driveForward: function () {        console.log( "drive forward" );    },    driveBackward: function () {        console.log( "drive backward" );    },    driveSideways: function () {        console.log( "drive sideways" );    }};function augment( receivingClass, givingClass ) {    if ( arguments[2] ) {        for ( var i = 2, len = arguments.length; i < len; i++ ) {            receivingClass.prototype[arguments[i]] = givingClass.prototype[arguments[i]];        }    }else {        for ( let methodName in givingClass.prototype ) {            if ( !Object.hasOwnProperty(receivingClass.prototype, methodName) ) {                receivingClass.prototype[methodName] = givingClass.prototype[methodName];            }        }    }}augment( Car, Mixin, "driveForward", "driveBackward" );let myCar = new Car({    model: "Ford Escort",    color: "blue"});myCar.driveForward();myCar.driveBackward();
augment( Car, Mixin );let mySportsCar = new Car({ model: "Porsche", color: "red"});mySportsCar.driveSideways();

优势 & 缺点

Mixin支持在一个系统中降解功能的重复性,增长功能的重用性.在一些应用程序也许须要在全部的对象实体共享行为的地方,咱们可以经过在一个Mixin中维护这个共享的功能,来很容易的避免任何重复,而所以专一于只实现咱们系统中真正彼此不一样的功能。

也就是说,对Mixin的反作用是值得商榷的.一些开发者感受将功能注入到对象的原型中是一个坏点子,由于它会同时致使原型污染和必定程度上的对咱们原有功能的不肯定性.在大型的系统中,极可能是有这种状况的。

可是,强大的文档对最大限度的减小对待功能中的混入源的迷惑是有帮助的,并且对于每一种模式而言,若是在实现过程当中当心行事,咱们应该是没多大问题的。

十一、装饰器模式

装饰器是旨在提高重用性能的一种结构性设计模式。同Mixin相似,它能够被看做是应用子类划分的另一种有价值的可选方案。

典型的装饰器提供了向一个系统中现有的类动态添加行为的能力。其创意是装饰自己并不关心类的基础功能,而只是将它自身拷贝到超类之中。

装饰器模式并不去深刻依赖于对象是如何建立的,而是专一于扩展它们的功能这一问题上。不一样于只依赖于原型继承,咱们在一个简单的基础对象上面逐步添加可以提供附加功能的装饰对象。它的想法是,不一样于子类划分,咱们向一个基础对象添加(装饰)属性或者方法,所以它会是更加轻巧的。

向Javascript中的对象添加新的属性是一个很是直接了当的过程,所以将这一特定牢记于心,一个很是简单的装饰器能够实现以下:

示例1:带有新功能的装饰构造器

  •  
function vehicle( vehicleType ){    this.vehicleType = vehicleType || "car";    this.model = "default";    this.license = "00000-000";}let testInstance = new vehicle( "car" );console.log( testInstance );// vehicle: car, model:default, license: 00000-000
let truck = new vehicle( "truck" );truck.setModel = function( modelName ){ this.model = modelName;};truck.setColor = function( color ){ this.color = color;};truck.setModel( "CAT" );truck.setColor( "blue" );console.log( truck );// vehicle:truck, model:CAT, color: blue
let secondInstance = new vehicle( "car" );console.log( secondInstance );// vehicle: car, model:default, license: 00000-000

示例2:带有多个装饰器的装饰对象

  •  
function MacBook() {  this.cost = function () { return 997; };  this.screenSize = function () { return 11.6; };}function Memory( macbook ) {  let v = macbook.cost();  macbook.cost = function() {    return v + 75;  };}function Engraving( macbook ){  let v = macbook.cost();  macbook.cost = function(){    return  v + 200;  };}function Insurance( macbook ){  let v = macbook.cost();  macbook.cost = function(){     return  v + 250;  };}
let mb = new MacBook();Memory( mb );Engraving( mb );Insurance( mb );console.log( mb.cost() );// 1522console.log( mb.screenSize() );// 11.6

在上面的示例中,咱们的装饰器重载了超类对象MacBook()的 object.cost()函数,使其返回的Macbook的当前价格加上了被定制后升级的价格。

这被看作是对原来的Macbook对象构造器方法的装饰,它并无将其重写(例如,screenSize()),咱们所定义的Macbook的其它属性也保持不变,无缺完好。

优势 & 缺点

由于它能够被透明的使用,而且也至关的灵活,所以开发者都挺乐意去使用这个模式——如咱们所见,对象能够用新的行为封装或者“装饰”起来,然后继续使用,并不用去担忧基础的对象被改变。在一个更加普遍的范围内,这一模式也避免了咱们去依赖大量子类来实现一样的效果。

然而在实现这个模式时,也存在咱们应该意识到的缺点。若是穷于管理,它也会因为引入了许多微小可是类似的对象到咱们的命名空间中,从而显著的使得咱们的应用程序架构变得复杂起来。这里所担心的是,除了渐渐变得难于管理,其余不能熟练使用这个模式的开发者也可能会有一段要掌握它被使用的理由的艰难时期。

足够的注释或者对模式的研究,对此应该有助益,而只要咱们对在咱们的应程序中的多大范围内使用这一模式有所掌控的话,咱们就能让两方面都获得改善。

十二、亨元模式

享元模式是一个优化重复、缓慢和低效数据共享代码的经典结构化解决方案。它的目标是以相关对象尽量多的共享数据,来减小应用程序中内存的使用(例如:应用程序的配置、状态等)。

此模式最早由Paul Calder 和 Mark Linton在1990提出,并用拳击等级中少于112磅体重的等级名称来命名。享元(“Flyweight”英语中的轻量级)的名称自己是从以帮以助咱们完成减小重量(内存标记)为目标的重量等级推导出的。

实际应用中,轻量级的数据共享采集被多个对象使用的类似对象或数据结构,并将这些数据放置于单个的扩展对象中。咱们能够把它传递给依靠这些数据的对象,而不是在他们每一个上面都存储一次。

使用享元

有两种方法来使用享元。第一种是数据层,基于存储在内存中的大量相同对象的数据共享的概念。第二种是DOM层,享元模式被做为事件管理中心,以免将事件处理程序关联到咱们须要相同行为父容器的全部子节点上。享元模式一般被更多的用于数据层,咱们先来看看它。

享元和数据共享

对于这个应用程序而言,围绕经典的享元模式有更多须要咱们意识到的概念。享元模式中有一个两种状态的概念——内在和外在。内在信息可能会被咱们的对象中的内部方法所须要,它们绝对不能够做为功能被带出。外在信息则能够被移除或者放在外部存储。

带有相同内在数据的对象能够被一个单独的共享对象所代替,它经过一个工厂方法被建立出来。这容许咱们去显著下降隐式数据的存储数量。

个中的好处是咱们可以留心于已经被初始化的对象,让只有不一样于咱们已经拥有的对象的内在状态时,新的拷贝才会被建立。

咱们使用一个管理器来处理外在状态。如何实现能够有所不一样,但针对此的一种方法就是让管理器对象包含一个存储外在状态以及它们所属的享元对象的中心数据库。

经典的享元实现

近几年享元模式已经在Javascript中获得了深刻的应用,咱们会用到的许多实现方式其灵感来自于Java和C++的世界。

咱们来看下来自维基百科的针对享元模式的 Java 示例的 Javascript 实现。

在这个实现中咱们将要使用以下所列的三种类型的享元组件:

  • 享元对应的是一个接口,经过此接口可以接受和控制外在状态。

  • 构造享元来实际的实际的实现接口,并存储内在状态。构造享元须是可以被共享的,而且具备操做外在状态的能力。

  • 享元工厂负责管理享元对象,而且也建立它们。它确保了咱们的享元对象是共享的,而且能够对其做为一组对象进行管理,这一组对象能够在咱们须要的时候查询其中的单个实体。若是一个对象已经在一个组里面建立好了,那它就会返回该对象,不然它会在对象池中新建立一个,而且返回之。

这些对应于咱们实现中的以下定义:

  • CoffeeOrder:享元

  • CoffeeFlavor:构造享元

  • CoffeeOrderContext:辅助器

  • CoffeeFlavorFactory:享元工厂

  • testFlyweight:对咱们享元的使用

鸭式冲减的 “implements”

鸭式冲减容许咱们扩展一种语言或者解决方法的能力,而不须要变动运行时的源。因为接下的方案须要使用一个Java关键字“implements”来实现接口,而在Javascript本地看不到这种方案,那就让咱们首先来对它进行鸭式冲减。

Function.prototype.implementsFor 在一个对象构造器上面起做用,而且将接受一个父类(函数—)或者对象,而从继承于普通的继承(对于函数而言)或者虚拟继承(对于对象而言)均可以。

  •  
// Simulate pure virtual inheritance/"implement" keyword for JS Function.prototype.implementsFor = function( parentClassOrObject ){    if ( parentClassOrObject.constructor === Function ) {        // Normal Inheritance        this.prototype = new parentClassOrObject();         this.prototype.constructor = this;         this.prototype.parent = parentClassOrObject.prototype;    } else {        // Pure Virtual Inheritance        this.prototype = parentClassOrObject; this.prototype.constructor = this; this.prototype.parent = parentClassOrObject;    }    return this;};

咱们能够经过让一个函数明确的继承自一个接口来弥补implements关键字的缺失。下面,为了使咱们得以去分配支持一个对象的这些实现的功能,CoffeeFlavor实现了CoffeeOrder接口,而且必须包含其接口的方法。

  •  
let CoffeeOrder = {    // Interfaces    serveCoffee:function(context){},    getFlavor:function(){}};function CoffeeFlavor( newFlavor ){    let flavor = newFlavor;    if( typeof this.getFlavor === "function" ){      this.getFlavor = function() {          return flavor;      };    }    if( typeof this.serveCoffee === "function" ){      this.serveCoffee = function( context ) {        console.log("Serving Coffee flavor "+ flavor+" to table number "+ context.getTable());      };    }}CoffeeFlavor.implementsFor( CoffeeOrder );function CoffeeOrderContext( tableNumber ) {   return{      getTable: function() {         return tableNumber;     }   };}function CoffeeFlavorFactory() {    let flavors = {},    length = 0;    return {        getCoffeeFlavor: function (flavorName) {            let flavor = flavors[flavorName];            if (flavor === undefined) {                flavor = new CoffeeFlavor(flavorName);                flavors[flavorName] = flavor;                length++;            }            return flavor;        },        getTotalCoffeeFlavorsMade: function () {            return length;        }    };}function testFlyweight(){  let flavors = new CoffeeFlavor(),    tables = new CoffeeOrderContext(),    ordersMade = 0,    flavorFactory;  function takeOrders( flavorIn, table) {     flavors[ordersMade] = flavorFactory.getCoffeeFlavor( flavorIn );     tables[ordersMade++] = new CoffeeOrderContext( table );  }   flavorFactory = new CoffeeFlavorFactory();   takeOrders("Cappuccino", 2);   takeOrders("Cappuccino", 2);   takeOrders("Frappe", 1);   takeOrders("Frappe", 1);   takeOrders("Xpresso", 1);   takeOrders("Frappe", 897);   takeOrders("Cappuccino", 97);   takeOrders("Cappuccino", 97);   takeOrders("Frappe", 3);   takeOrders("Xpresso", 3);   takeOrders("Cappuccino", 3);   takeOrders("Xpresso", 96);   takeOrders("Frappe", 552);   takeOrders("Cappuccino", 121);   takeOrders("Xpresso", 121);   for (var i = 0; i < ordersMade; ++i) {       flavors[i].serveCoffee(tables[i]);   }   console.log("total CoffeeFlavor objects made: " +  flavorFactory.getTotalCoffeeFlavorsMade());}

转换代码为使用享元模式

接下来,让咱们经过实现一个管理一个图书馆中全部书籍的系统来继续观察享元。分析得知每一本书的重要元数据以下:

  • ID

  • 标题

  • 做者

  • 类型

  • 总页数

  • 出版商ID

  • ISBN

咱们也将须要下面一些属性,来跟踪哪个成员是被借出的一本特定的书,借出它们的日期,还有预计的归还日期。

  • 借出日期

  • 借出的成员

  • 规定归还时间

  • 可用性

  •  
let Book = function( id, title, author, genre, pageCount,publisherID, ISBN, checkoutDate, checkoutMember, dueReturnDate,availability ){   this.id = id;   this.title = title;   this.author = author;   this.genre = genre;   this.pageCount = pageCount;   this.publisherID = publisherID;   this.ISBN = ISBN;   this.checkoutDate = checkoutDate;   this.checkoutMember = checkoutMember;   this.dueReturnDate = dueReturnDate;   this.availability = availability;};
Book.prototype = { getTitle: function () { return this.title; }, getAuthor: function () { return this.author; }, getISBN: function (){ return this.ISBN; }, updateCheckoutStatus: function( bookID, newStatus, checkoutDate , checkoutMember, newReturnDate ){ this.id = bookID; this.availability = newStatus; this.checkoutDate = checkoutDate; this.checkoutMember = checkoutMember; this.dueReturnDate = newReturnDate; }, extendCheckoutPeriod: function( bookID, newReturnDate ){ this.id = bookID; this.dueReturnDate = newReturnDate; }, isPastDue: function(bookID){ let currentDate = new Date(); return currentDate.getTime() > Date.parse( this.dueReturnDate ); }};

这对于最初小规模的藏书可能工做得还好,然而当图书馆扩充至每一本书的多个版本和可用的备份,这样一个大型的库存,咱们会发现管理系统的运行随着时间的推移会愈来愈慢。使用成千上万的书籍对象可能会压倒内存,而咱们能够经过享元模式的提高来优化咱们的系统。

如今咱们能够像下面这样将咱们的数据分离成为内在和外在的状态:同书籍对象(标题,版权归属)相关的数据是内在的,而借出数据(借出成员,规定归还日期)则被看作是外在的。这实际上意味着对于每一种书籍属性的组合仅须要一个书籍对象。这仍然具备至关大的数量,但相比以前已经获得大大的缩减了。

下面的书籍元数据组合的单一实体将在全部带有一个特定标题的书籍拷贝中共享。

  •  
let Book = function ( title, author, genre, pageCount, publisherID, ISBN ) {    this.title = title;    this.author = author;    this.genre = genre;    this.pageCount = pageCount;    this.publisherID = publisherID;    this.ISBN = ISBN;};

如咱们所见,外在状态已经被移除了。从图书馆借出所要作的一切都被转移到一个管理器中,因为对象数据如今是分段的,工厂能够被用来作实例化。

一个基本工厂

如今让咱们定义一个很是基本的工厂。咱们用它作的工做是,执行一个检查来看看一本给定标题的书是否是以前已经在系统内建立过了;若是建立过了,咱们就返回它 - 若是没有,一本新书就会被建立并保存,使得之后能够访问它。

这确保了为每一条本质上惟一的数据,咱们只建立了一份单一的拷贝:

  •  
let BookFactory = (function () {  let existingBooks = {}, existingBook;  return {    createBook: function ( title, author, genre, pageCount, publisherID, ISBN ) {      existingBook = existingBooks[ISBN];      if ( !!existingBook ) {        return existingBook;      } else {        let book = new Book( title, author, genre, pageCount, publisherID, ISBN );        existingBooks[ISBN] = book;        return book;      }    }  };});

管理外在状态

下一步,咱们须要将那些从Book对象中移除的状态存储到某一个地方——幸运的是一个管理器(咱们会将其定义成一个单例)能够被用来封装它们。书籍对象和借出这些书籍的图书馆成员的组合将被称做书籍借出记录。

这些咱们的管理器都将会存储,而且也包含咱们在对Book类进行享元优化期间剥离的同借出相关的逻辑。

  •  
let BookRecordManager = (function () {  let bookRecordDatabase = {};  return {    addBookRecord: function ( id, title, author, genre, pageCount, publisherID, ISBN, checkoutDate, checkoutMember, dueReturnDate, availability ) {      let book = bookFactory.createBook( title, author, genre, pageCount, publisherID, ISBN );      bookRecordDatabase[id] = {        checkoutMember: checkoutMember,        checkoutDate: checkoutDate,        dueReturnDate: dueReturnDate,        availability: availability,        book: book      };    },    updateCheckoutStatus: function ( bookID, newStatus, checkoutDate, checkoutMember, newReturnDate ) {      let record = bookRecordDatabase[bookID];      record.availability = newStatus;      record.checkoutDate = checkoutDate;      record.checkoutMember = checkoutMember;      record.dueReturnDate = newReturnDate;    },    extendCheckoutPeriod: function ( bookID, newReturnDate ) {      bookRecordDatabase[bookID].dueReturnDate = newReturnDate;    },    isPastDue: function ( bookID ) {      let currentDate = new Date();      return currentDate.getTime() > Date.parse( bookRecordDatabase[bookID].dueReturnDate );    }  };});

这些改变的结果是全部从Book类中撷取的数据如今被存储到了BookManager单例(BookDatabase)的一个属性之中——与咱们之前使用大量对象相比能够被认为是更加高效的东西。同书籍借出相关的方法也被设置在这里,由于它们处理的数据是外在的而不内在的。

这个过程确实给咱们最终的解决方法增长了一点点复杂性,然而同已经明智解决的数据性能问题相比,这只是一个小担心,若是咱们有同一本书的30份拷贝,如今咱们只须要存储它一次就够了。

每个函数也会占用内存。使用享元模式这些函数只在一个地方存在(就是在管理器上),而且不是在每个对象上面,这节约了内存上的使用。

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

相关文章
相关标签/搜索