设计模式-创建型

178 阅读27分钟

前言

设计模式(Design Pattern)是前辈们对代码开发经验的总结,代表了最佳的实践,是解决特定问题的一系列解决方案。它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。

image

  设计模式共分为5大类:创建型、结构型、行为型、技巧型、架构型。每一个类型设计模式都有各自共同的特性。学习设计模式对于我们项目代码架构和统一设计思想有很好的帮助,课程的讲解我们会按每一类进行讲解,我们本次课程主要讲解创建型设计模式。

工厂模式

工厂模式分类:

  • 简单工厂模式(Simple Factory)
  • 工厂方法模式(Factory Method)
  • 抽象工厂模式(Abstract Factory)

  工厂模式中简单工厂模式在日常开发中比较常见,工厂模式的作用其实就是隐藏创建实例的复杂度,对外只暴露一个方法,代码结构比较清晰,简单明了。

工厂模式这么常用,那么在我们使用的框架中有什么体现吗?

01. jQurey的$(selector)

jQuery中的('#div')和('.div')这类的操作怎么实现的?就是通过工厂实现的。
jQuery根据参数不同来分析行为。

// jQuery入口源码
jQuery = function( selector, context ) {

	// The jQuery object is actually just the init constructor 'enhanced'
	// Need init if jQuery is called (just allow error to be thrown if not included)
	return new jQuery.fn.init( selector, context );
};

jQuery.fn = jQuery.prototype = {
    jquery: version,
    init: function( selector, context, root ) {
        var match, elem;

		// HANDLE: $(""), $(null), $(undefined), $(false)
		if ( !selector ) {
			return this;
		}

		// Method init() accepts an alternate rootjQuery
		// so migrate can support jQuery.sub (gh-2101)
		root = root || rootjQuery;

		// Handle HTML strings
		if ( typeof selector === "string" ) {
		    ...
		} else if ( selector.nodeType ) {
            ...
    		// HANDLE: $(function)
    		// Shortcut for document ready
		} else if ( typeof selector === "function" ) {
		    ...
		}

		return jQuery.makeArray( selector, this );
    }
}

jQuery.extend = jQuery.fn.extend = function() {
    ...
}

jQuery.extend({
    ...
});

...

var
	// Map over jQuery in case of overwrite
	_jQuery = window.jQuery,

	// Map over the $ in case of overwrite
	_$ = window.$;

jQuery.noConflict = function( deep ) {
	if ( window.$ === jQuery ) {
		window.$ = _$;
	}

	if ( deep && window.jQuery === jQuery ) {
		window.jQuery = _jQuery;
	}

	return jQuery;
};

// Expose jQuery and $ identifiers, even in AMD
// (#7102#comment:10, https://github.com/jquery/jquery/pull/557)
// and CommonJS for browser emulators (#13566)
if ( typeof noGlobal === "undefined" ) {
	window.jQuery = window.$ = jQuery;
}

// jQuery底层抽象为ES6伪代码
class jQuery {
    constructor(selector) {
        super(selector)
    }
    //  ....
}

window.$ = function(selector) {
    return new jQuery(selector)
}

02. Vue的异步组件

通过 promise 的方式 resolve 出来一个组件

// 使用
Vue.component('async-example', function (resolve, reject) {
  setTimeout(function () {
    // 向 `resolve` 回调传递组件定义
    resolve({
      template: '<div>I am async!</div>'
    })
  }, 1000)
});

// 源码
export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {

    ...

  // async component
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
    if (Ctor === undefined) {
      // return a placeholder node for async component, which is rendered
      // as a comment node but preserves all the raw information for the node.
      // the information will be used for async server-rendering and hydration.
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

    ...
    
  // return a placeholder vnode
  const name = Ctor.options.name || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )
}

03. React的createElement()

React的createElement()方法也是一个工厂方法

// 使用
React.createElement(
  type,
  [props],
  [...children]
);

// 源码
class ReactNode(tag, attrs, children) {
    ...
};

React.createElement = (tag, attrs, children) => {
    return new ReactNode(tag, attrs, children);
}

上面是工厂模式在我们日常使用框架中的应用,接下来让我们来结合具体示例来看看这三种工厂模式。

简单工厂模式

简单工厂模式(Simple Factory):又叫静态工厂模式,有一个工厂对象决定创建某一种产品对象类的实例。主要用来创建同一类对象。

简单工厂模式的主要角色如下:

  • 简单工厂(SimpleFactory):核心,负责实现创建所有实例得内部逻辑。
  • 抽象产品(Product):简单工厂创建的所有父类,负责所有实例共有的公共接口。
  • 具体产品(ConcreteProduct):是简单工厂模式的创建目标。

其结构图如下图所示:

image

示例

  假设现在需要做一个登录的功能,当用户输入内容不符合规范是就自定义警示框提示用户,接下来我们封装一个警示框类。

// 登录提示
let LoginAlert = function(text) {
    this.content = text;
};
LoginAlert.prototype.show = function() {
    // 显示警示内容
    ...
};
let alert = new LoginAlert('不规范内容提示');  //实例化
alert.show();

  现在弹窗文案可以根据自定义去定义,但是我们如果需要在警示框内增加跳转按钮(比如: 注册Button),那么我们需要封装另外一个类。

// 登录确认框
let LoginConfirm = function (text) {
    this.content = text;
};
LoginConfirm.prototype.show = function() {
   // 显示确认框内容
   ...
};
let confirm = new LoginConfirm('不规范内容提示');  //实例化
confirm.show();

  登录成功以后如果需要增加一个自定义的提示框,这个是完全自定义的,我们再封装一个自定义类。

// 自定义提示框
var LoginCustom = function(text) {
    this.content = text;
};
LoginCustom.prototype.show = function() {
    // 显示自定义提示框内容
    ...
};
let custom = new LoginCustom('自定义内容');  //实例化
custom.show();

  以上是做登录功能我们需要封装的类,但是当我们要做注册功能时,并且有类似的提示需求时,虽然以上类可以共用,但是比较多,而且还是以Login为前缀,每次创建时还要找到对应的类,很麻烦,其它使用者使用不方便。
  接下来我们改动一下,把多个类封装到一个函数内,每次调用时只需要记住这个函数,然后通过这个函数可以创建我需要的对象,其他人在使用时不需要这些对象依赖了哪个基类,只需要知道这个函数就行了。

// 简单改动
var PopFactory = function(name) {
    switch(name) {
      case 'alert':
        return new LoginAlert();
      case 'confirm':
        return new LoginConfirm();
      case 'custom': {
        return new LoginCustom();
      }
    }
}

  上面函数虽然实现了封装到一个函数体内,但是这3个有很多地方都是相同的,是可以抽象提取出来的。我们可以借助简单工厂模式来实现,简单工厂模式的理念是创建对象,上面的方式是对不同的类进行实例化,不过除此之外简单模式还可以用来创建相似对象,而你创建的这3个类(对象)很多地方都比较类似,比如都有关闭按钮、提示文案等。
  所以可以通过把类似的东西提取,不相似的针对性处理,所以把这3个类改成简单工厂模式也很简单,首先抽象他们的共同点,比如共有属性this.content,原型共有方法show,当然也有不同点,比如确认框与提示框的确定按钮,接下来我们用简单工厂模式实现一下。

// 简单工厂模式
function createPop(type, text) {
    // 创建一个对象,并对对象拓展属性和方法
    var o = new Object();
    o.content = text;
    o.show = function() {
      // 显示方法
    };
    if(type === 'alert') {
      // 警示框差异部分
    } else if(type === 'prompt') {
      // 提示框差异部分
    } else if(type === 'confirm') {
      // 确认框差异部分
    } else {
      // 默认处理
    }
    //返回对象
    return o;
};

var userNameAlert = createPop('alert', 'xxxxx');

上面就是我们利用简单工厂模式来实现的,接下来我们来看看两种方案的对比:

  • 第一种是通过类实例化对象创建的。
  • 第二种时通过创建一个新对象然后包装增强其属性和功能来实现的。

  他们之间的差异性也造成前面通过类创建的对象,如果这些类继承同一个父类,那么他们父类原型上的方法时可以共用的,而后面创建的对象都是一个新的个体,所以他们的方法就不能共用了。
  我们开发过程中一般都是团队协作的方式,我们对于全局变量的把控是比较严格的,尽量少的去创建全局变量。对于同一类对象在不同需求中的重复性使用,很多时候不需要重复创建,代码复用是面向对象编程的一条准则。通过对简单工厂来创建一些对象,可以让这些对象共用一些资源而又私有一些资源,不过对于简单工厂模式,它的使用场景通常也就限制在创建单一对象。

工厂方法模式

工厂方法模式(Factory Method):通过对产品类的抽象使其创建业务主要负责用于创建产品的实例。

工厂方法模式的主要角色如下:

  • 抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法 newProduct() 来创建产品。
  • 具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
  • 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
  • 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。

其结构图如下图所示:

image

示例

  我们项目中详情界面头部需要根据状态配置,状态A用黑色字体、状态B需要用白色字体、蓝色背景。

// StatusA类
var StatusA类 = function(content) {
    // 将内容保存在content里面以备日后使用
    this.content = content;
    // 创建对象时,通过闭包,直接执行,将内容按需求的样式插入到界面内
    (function(content) {
      var div = document.createElement('div');
      div.innerHTML = content;
      div.style.color = 'black';  // 黑色字体
      document.getElementById('container').appendChild(div);
    })(content);
};

// StatusB类
var StatusB = function(content) {
    // 将内容保存在content里面以备日后使用
    this.content = content;
    // 创建对象时,通过闭包,直接执行,将内容按需求的样式插入到界面内
    (function(content) {
      var div = document.createElement('div');
      div.innerHTML = content;
      div.style.color = 'white';  // 白色字体
      div.style.background = "blue";  // 蓝色背景
      document.getElementById('container').appendChild(div);
    })(content);
};

  紧接着又增加了一种状态,状态C需要用橙色背景,这么多种状态,我们可以借助简单工厂模式来实现,日后在增加状态可以直接着工厂方法就好了。

// 简单工厂模式
// 创建StatusA类
var StatusA = function(content) {
    // ....
}
// 创建StatusB类
var StatusB = function(content) {
    // ....
}

// 创建StatusC类
var StatusC = function(content) {
    // 将内容保存在content里面以备日后使用
    this.content = content;
    // 创建对象时,通过闭包,直接执行,将内容按需求的样式插入到界面内
    (function(content) {
      var div = document.createElement('div');
      div.innerHTML = content;
      div.style.background = "orange";  // 橙色背景
      document.getElementById('container').appendChild(div);
    })(content);
}

// 状态工厂
function JobFaction(type, content) {
    switch(type) {
      case 'status_a':
        return new StatusA(content);
      case 'status_b': 
        return new StatusB(content);
      case 'status_c':
        return new StatusC(content);
    }
}

JobFaction('status_a', '我是状态A');

  然后需求又变了,要增加一个状态C,红色边框,和之前的几种状态不太一样,不仅要新增类,还有修改工厂函数 ,需要修改两处地方,容易出来漏改的情况。
  那么有没有改动很小,且不会出现漏改的方式呢?我们可以尝试用一下工厂方法模式来实现,以后每需要增加一个类,只需要添加这个类就行了,其它的不用操心。

工厂方法?这是一个什么样的模式呢?

  工厂方法模式本意是说实际创建对象工作推迟到子类当中。这样核心类就成了抽象类,不过对于javascript不必这么深究,javascript没有像传统创建抽象类那样的方式轻易创建抽象类,所以在javascript中实现工厂方法模式只需要参考它的核心思想即可。所以我们可以将工厂方法看作是一个是实例化对象的工厂类。
  安全起见,我们采用安全模式类,而我们将创建对象的基类放在工厂方法类的原型中即可。
  安全模式类是说可以屏蔽使用这对类的错误使用造成的错误,比如一个类称为Demo类,如果忽略它是一个类,使用的时候忽略new关键字直接执行类就不是我们想要得到的结果。

// 安全模式
var Demo = function() {};
Demo.prototype = {
    show: function() {
      console.log('成功获取');
    }
}

var d = new Demo();
d.show();  //成功获取
var d = Demo();
d.show();  //Uncaught TypeError: Cannot read property 'show' of undefined

  我们为了避免其它同学使用时出现错误,我们可以在构造函数开始先判断当前对象的this指向是不是Demo类,如果是正常执行,如果不是说明类在全局作用域下执行了,this也就指向了window,这样的话我们就重新返回新创建的对象就可以了。

// 解决问题
var Demo = function() {
    if(!(this instanceof Demo)) {
      return new Demo();
    }
}
Demo.prototype = {
    show: function() {
      console.log('成功获取');
    }
}

var d = Demo();
d.show(); //成功获取

  有了安全模式我们就可以将这种技术应用到我们的工厂方法中了。

// 安全的工厂方法
// 安全模式创建的工厂类
var Factory = function(type, content) {
    if(this instanceof Factory) {
      var s = new this[type](content);
      return s;
    } else {
      return new Factory(type, content);
    }
}

// 工厂原型中设置创建所有类型数据对象的基类
Factory.prototype = {
    StatusA: function(content) {
      // ...
    },
    StatusB: function(content) {
      // ...
    },
    StatusC: function(content) {
      // ...
    },
    StatusD: function() {
      // ...
    },
    ... // 新增状态在这里继续新增就好
}

  这样我们以后如果想添加其它类时,是不是只需写在Factory这个工厂类的原型里面就可以了? 不用担心创建时做任何修改,好比你在Factory类原型里面注册了一张名片,以后需要哪种类直接拿着这张名片,查找上面的信息就找到这个类了所以不用担心使用时找不到基类的问题了。

let data = [{
    type: 'StatusA', content: '我是StatusA',
    type: 'StatusB', content: '我是StatusB',
    type: 'StatusC', content: '我是StatusC',
    type: 'StatusD', content: '我是StatusD',
}];
data.forEach(item => {
    Factory(item.type, item.content);
});

  对于创建多类对象,前面学习过的简单工厂模式就不太适用了,通过工厂方法模式我们可以轻松创建多个类的不同实例对象,这样工厂方法对象在创建对象的方式也避免了使用者与对象类之间的轻松耦合,用户不必关心创建该对象的具体类,只需调用工厂方法即可。

抽象工厂模式

抽象工厂模式(Abstract Factory):通过对类的工厂抽象使其业务用于对产品类簇的创建,而不负责创建某一类产品的实例。

抽象工厂模式的主要角色如下:

  • 抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法 newProduct(),可以创建多个不同等级的产品。
  • 具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
  • 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
  • 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间是多对一的关系。

其结构图如下图所示:

image

示例

  在javascript中如何创建抽象类呢?javascript中abstract还是一个保留字,所以目前来说还不能像传统对象语言那样轻松创建。抽象类是一种声明但不能使用的类,当你使用时就会报错。不过javascript是灵活的,所以我们可以在类的方法中手动的抛出错误来模拟抽象类。

//  抽象类:动物,当使用其实例对象的方法时会抛出错误
var Animal = function() {};
Animal.prototype = {
  // 声音
  getSound: function() {
    return new Error('抽象方法不能调用');
  },
}

  我们看到创建的这个Animal类什么都不能做,创建时没有任何属性,然而原型prototype上的方法也不能使用,否则会报错。但在继承上是很有用的,因为定义了一种类,并定义了该类所必备的方法。
  如果在子类中没有重写这些方法,那么当调用时能找到这些方法就会报错,在一些大型应用中,总会有一些子类去继承另一些父类,这些父类经常会定义一些必要的方法,但是没有具体的实现,如Animal中getNaturalEnemy()方法,那么一旦用子类创建了一个对象,该对象总是应该具备一些必要的方法。
  但是如果这些必要的方法从父类中继承过来而没有具体去重写实现,那么实例化对象便会调用父类中这些方法,如果父类能有一个友好的提示,那么对于忘记重写子类的这些错误遗漏的是有帮助的。这也是抽象类的一个作用,既定义一个产品簇,并声明一些必备方法,如果子类没有重写就抛出错误。
  面向对象语言很常见的模式叫抽象工厂模式,都是用来创建具体对象,但是抽象工厂模式可不简单,在Javascript中一般不用来创建具体对象,抽象类中定义的方法只是显性的定义一些功能,没有具体实现,而一个对象是要具有一套完整的功能的,所以用抽象类创建的对象当然也是“抽象的”,不能创建一个真实的对象。接下来我们实现一个抽象工厂方法。

// 抽象工厂方法
var Vehiclefactory = function(subType, superType) {
  // 判断抽象工厂中是否有该抽象类
  if(typeof Vehiclefactory[superType] === 'function') {
    // 缓存类
    function Cache() {};
    // 继承父类属性和方法
    Cache.prototype = new Vehiclefactory[superType]();
    // 将子类conStructor指向子类
    subType.constructor = new Cache();
    // 子类原型继承‘父类’
    subType.prototype = new Cache();
  } else {
    // 不存在该抽象类抛出错误
    throw new Error('未创建该抽象类');
  }
}

// 陆生动物抽象类
Vehiclefactory.Terrestrial = function() {};
Vehiclefactory.Terrestrial.prototype = {
  // 声音
  getSound: function() {
    return new Error('陆生动物抽象方法不能调用');
  },
};

// 水生动物抽象类
Vehiclefactory.Aquatic = function() {};
Vehiclefactory.Aquatic.prototype = {
  // 声音
  getSound: function() {
    return new Error('水生动物抽象方法不能调用');
  },
}

  抽象工程其实是一个实现子类继承父类的方法,在这个方法中我们需要通过传递子类以及要继承父类(抽象类)的名称,并且在抽象工程方法中又增加类一次对抽象类存在性的一个判断,如果存在,则将子类继承父类发的方法,然后子类通过寄生式继承。
  继承父类过程中有一个地方需要注意,就是在对过渡类的原型继承时,我们不是继承父类的原型,而是通过new关键字复制父类的一个实例,这么做时因为过度类不应仅仅继承父类的原型方法,还要继承父类的对象属性,所以要通过new关键字将父类的构造函数执行一遍来复制构造函数的属性和方法。

// 使用示例

// 猫-子类
var Cat = function(sound) {
  this.sound = sound
}

// 抽象工厂实现对Terrestrial抽象类的继承
Vehiclefactory(Cat, 'Terrestrial');
Cat.prototype.getNaturalEnemy = function() {
  return this.sound;
};

let cat = new Cat('我是猫咪,喵喵喵~~~');
cat.getNaturalEnemy();  // 我是猫咪,喵喵喵~~~

  通过抽象工厂,我们就能知道每一个子类到底是属于哪一种类别了,然后他们也具备了类所必备的属性和方法了。
  抽象工厂模式是设计模式中最抽象的一种,也是创建模式中唯一抽象化创建模式。该模式创建出的结果不是一个真实的对象实例,而是一个类簇,它指定了类的结构,这也就区别于简单工厂模式创建单一对象,工厂方法模式创建多类对象。由于Javascript中不支持抽象化创建与虚拟方法,所以导致这种模式不能像其它面向对象语言中应用得那么广泛。

建造者模式

  我们学习建造者模式之前首先来大概了解一下建造者模式的概念以及建造者模式在jQuery中的应用。建造者模式就是把功能分块,各司其职,最终运转在一个大的流程中。

建造者模式在jQuery中的应用

$( "<textarea />" )
      .attr({ "rows": "30", "cols":"10"});
      .appendTo("#textarea_box");

  下面是jQuery源码内部jQuery.prototype方法的一个片段,它将从传递给jQuery()选择器的标记构建jQuery对象。无论是否 document.createElement用于创建新元素,对元素(找到或创建)的引用都会注入到返回的对象中,因此.attr()可以在其后立即使用其他方法。

// 伪代码
jQuery.prototype = {
    jquery() {
        ...
    },
    get() {
        ...
    },
    attr() {
        ...,
    },
    ...
}
// 拓展
jQuery.extend({
    noConflict() {
        ...
    },
    holdReady() {
        ...
    },
    merge() {
        ...
    },
    ...
});

模式的结构

建造者(Builder)模式的主要角色如下。

  • 产品角色(Product)
  • 抽象建造者(Builder)
  • 具体建造者(Concrete Builder)
  • 指挥者(Director)

其结构图如下图所示:

image

接下来让我们结合具体示例来了解一下建造者模式。

建造者模式(Builder):将一个复杂对象的构建层与其表示层相互分离,同样的构建过程可采用不同的表示。 建造者关注创建的过程,创建的细节。

  工厂模式主要是为了创建对象实例或者类簇,关心的是最终产出的是什么。不关心你创建的整个过程,仅仅需要知道你最终创建的结果。所以通过工厂模式我们得到的都是对象实例或者类簇,然后建造者模式在创建时更为复杂一些,虽然目的也是为了创建对象,但是它更关心的是创建这个对象的整个过程,甚至于创建对象的每一个细节,比如创建一个人,我们创建的结果不仅仅是得到人的实例,还要关注创建人的时候,这个人每一个细节。所以建造者模式更注重的是创建的细节。
  下面我们以图书馆系统为例,对外展示图书馆名称以及图书馆馆藏图书分类。

// 图书馆基础类
class LibraryBase {
    constructor(name, subject) {
        // 名字
        this.name = name;
        // 图书分类
        this.subject = subject;
    }
    
    // 获取名字
    getName() {
        return this.name;
    }

    // 获取图书分类
    getSubject() {
        return this.subject;
    }
}

// 图书馆名称
class LibraryName {
    constructor(name) {
        this.fullName = name;
        if(name.indexOf('-') > -1) {
            this.FirstName = name.slice(0, name.indexOf('-'));
            this.secondName = name.slice(name.indexOf('-'));
        }
    }
}

// 图书馆拥有图书学科分类及所在位置
class LibrarySubject {
    constructor(subject) {
        switch (subject) {
            case '军事':
                this.subject = '军事';
                this.subjectGuide = '2楼03';
                break;
            case '语言':
                this.subject = '语言';
                this.subjectGuide = '2楼04';
                break;
            default:
                this.subject = '输入错误';
                this.subjectGuide = '1楼前台';
        }
    }

    // 更换图片分类
    changeSubject(work) {
        this.subject = subject;
    }

    // 修改图书位置指引
    changeGuide(guide) {
        this.subjectGuide = guide;
    }
}

  以上我们创建了抽象出来的3个类:图书馆基础类、图书馆名称解析类、图书馆图书学科类别类。我们首先需要上面抽象的3个类,接下来我们实现一个建造者类,在建造者类中实现对这3个类的组合调用,这样就可以创建出一个完整的应聘者对象了。

 // 图书馆类
class Library {
    constructor(name, subject) {
        var _library = new LibraryBase();
        _library.name = new LibraryName(name);
        _library.subject = new LibrarySubject(subject);
        this._library = _library;
    }
}

let library = new Library('xxx学校-第一图书馆', '语言');
console.log('library', library._library); //{"name":{"fullName":"xxx学校 第一图书馆","FirstName":"xxx学校","secondName":"第一图书馆"},"subject":{"subject":"语言","subjectGuide":"2楼04"}}

library._library.subject.changeGuide = '3楼01';
console.log('library', library._library);  // {"name":{"fullName":"xxx学校 第一图书馆","FirstName":"xxx学校","secondName":"第一图书馆"},"subject":{"subject":"语言","subjectGuide":"2楼04","changeGuide":"3楼01"}}

  建造者分为三个部分来创建一个应聘者对象,首先创建一位应聘者缓存对象,缓存对象需要修饰(添加属性和方法),然后我们向缓存对象添加姓名,添加一个期望岗位,最终我们就可以得到一位完整的应聘者了。
  工厂模式创建出来的是一个对象,它追求的是创建的结果,而建造者不仅可以得到创建的结果,还参与了创建的具体过程,对于实现的一些细节还进行了干涉,可以说创建的对象更加复杂,或者说创建的是符合对象;

原型模式

原型模式(Prototype):用原型实例指向创建对象的类,适用于创建新的对象的类共享原型对象的属性和方法。 原型模式就是将原型对象指向创建对象的类,使这些类共享原型对象的方法与属性。当然Javascript是基于原型链实现对象之间的继承,这种继承是基于一种对属性或者方法的共享,而不是对属性和方法的复制。

  假设我们我们项目中有很多的轮播图,我们为了代码的统一性,我们实现轮播图最好的方式就是通过创建对象来一一实现,所以我们定义一个图片轮播类。

// 图片轮播类
var LoopImages = function(imgArr, container) {
  this.imagesArray = imgArr;  // 轮播图片数组
  this.container = container;  // 轮播图片容器
  this.createImage = function() {}; // 创建轮播图片
  this.changeImage = function() {}; //切换下一张图片
};

  如果一个界面需要实现多样化,抽象出一个基类,让不同特效类去继承这个基类,然后对于差异化的需求通过重写这些继承下来的属性或者方法进行解决。

// 图片轮播类
var LoopImages = function(imgArr, container) {
  this.imagesArray = imgArr;  // 轮播图片数组
  this.container = container;  // 轮播图片容器
  this.createImage = function() {}; // 创建轮播图片
  this.changeImage = function() {}; //切换下一张图片
};

// 上下滑动切换类
var SlideLoopImg = function(imgArr, container) {
    // 构造函数继承图片轮播类
    LoopImages.call(this, imgArr, container);
    // 重写继承的切换下一张图片方法
    this.changeImage = function() {};
};

// 渐隐切换类
var FadeLoopImg = function(imgArr, container, arrow) {
    // 构造函数继承图片轮播类
    LoopImages.call(this, imgArr, container);
    // 切换箭头私有变量
    this.arrow = arrow;
    // 重写继承的切换下一张图片方法
    this.changeImage = function() {};
};

// 我们要创建一个显隐轮播图片测试实例很容易
// 实例化一个渐隐切换图片类
var fadeImg = new FadeLoopImg(['01.jpg','02.jpg'],'slice', ['left.jpg', 'right.jpg']);
fadeImg.changeImage();

  以上有弊端,每次子类继承父类都要创建一次父类,这样每次初始化都会做一些重复性的东西,对性能不友好,为了提升性能,我们需要一种共享机制,这样每次创建基类时,对于每次创建的一些简单而又差异化的属性我们可以放到构造函数中,一些消耗资源比较大的方法我们放到基类的原型中,这样可以避免很多不必要的消耗,这一模式很类似类继承,都是基于原型链。接下来我们改造一下。

// 原型模式
var LoopImages = function(imgArr, container) {
  this.imagesArray = imgArr;
  this.container = container;
}

LoopImages.prototype = {
  createImage: function() {}, // 创建轮播图片
  changeImage: function() {}, //切换下一张图片
}

// 上下滑动类
var SlideLoopImg = function(imgArr, container) {
    // 构造函数继承图片轮播类
    LoopImages.call(this, imgArr, container);
};
SlideLoopImg.prototype = new LoopImages();
// 重写继承的切换下一张图片方法
SlideLoopImg.prototype.changeImage = function(){};

// 渐隐切换类
// 渐隐切换类
var FadeLoopImg = function(imgArr, container, arrow) {
    // 构造函数继承图片轮播类
    LoopImages.call(this, imgArr, container);
    // 切换箭头私有变量
    this.arrow = arrow;
};
FadeLoopImg.prototype = new LoopImages();
// 重写继承的切换下一张图片方法
FadeLoopImg.prototype.changeImage = function(){};

// 测试用例
console.log(fadeImg.container);
fadeImg.changeImage();

  原型对象是一个共享的对象,那么不论是父类的实例对象或者是子类的继承,都是一个指向引用,所以原型对象才会被共享,那么对原型对象的拓展,不论是子类或者父类的对象都会继承下来,我们可以来测试一下。

LoopImages.prototype.getImageLength = function() {
  return this.imagesArray.length;
};
FadeLoopImg.prototype.getContainer = function() {
  return this.container;
};

console.log(fadeImg.getImageLength());  // 4
console.log(fadeImg.getContainer());  // slide

  所以说原型有一个特点就是在任何任何时候都可以对基类或者子类进行方法的拓展,且所有被实例化的对象或者类都能获取这些方法,这样给予我们对功能拓展的自由行,虽然很自由,但是不能随意去做,否则如果修改类的其它属性或者其它方法会影响他人。

  原型模式就是将可复用的,可共享的,耗时大的从基类中提出来然后放到其原型中,然后子类通过组合继承或者寄生组合式继承而将方法和属性继承下来,对于子类中的那些需要重写的方法进行重写,这样子类创建的对象既具有子类的属性和方法也共享了基类的原型方法。

单例模式

单例模式(Singleton):又被成为单体模式,是只允许实例化一次的对象类。有时我们也用一个对象来规划一个命名空间,井井有条的管理对象上的属性与方法。

单例模式的特点

  • 单例类只有一个实例对象;
  • 该单例对象必须由单例类自行创建;
  • 单例类对外提供一个访问该单例的全局访问点。

单例模式的优点

  • 减少内存消耗,只有一个实例对象。
  • 避免资源多重占用。
  • 设置全局访问点,优化和共享资源访问。

单例模式的缺点

  • 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。
  • 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
  • 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。

单例模式在项目中应用

  • jQuery严格意义上也是一个单例。
  • Vuex、Redux中的全局状态管理store,数据保存在单一的store中。
  • Router内置的状态管理、业务处理、数据管理。
  • ...

结构图

image

  在我们项目当中一般都会有一些全局方法和变量,比如user_id、user_name、getUser()、setUser()等全部变量或方法,我们会为这些变量或者方法写一下函数。

// 全局定义
var context = {};
function getUser(key) {
    return context(key);
};

function setUser(key, value) {
    context(key) = value;
};

...

  我们在全局定义了很多变量和全局函数,如果日后维护过程中一些同学不知道这些全局变量及方法,重定义或者重新了些方法,就会起冲突,所以我们最好采用单例模式来进行重写。
  单例模式可能是最常用的一种模式了,这种模式经常为我们提供一个命名空间。常用来定义命名空间,还可以通过单例来管理代码库的各个模块。
  命名空间(namespace)又称名称空间,为了让代码更易懂,日常开发中常常用单词或者拼音定义变量或者方法,但由于单词或者拼音有限,所以不用的人定义的变量可能会重复,此时就需要用命名空间来进行解决这一类问题,比如a同学写的代码可以用user_a定义命名空间,以后使用a同学的变量就是user_a.xxx(xxx表示定义的变量名字)。

// 改动上述代码
let user = {
    context: {},
    getUser(id) {
      return this.context(key);
    },
    setUser(key, value) {
      this.context(key) = value;
    },
    // ...
}

  单例模式除了命名空间外,还有一个作用就是通过单例模式来管理代码库中的各个模块,比如:

pluginA.dom.addClass  // 添加元素类
pluginA.dom.append  // 插入元素
pulginA.event.stopPropagation  // 阻止冒泡
pluginA.enent.trim  // 去除字符串首尾空白字符

  这样可以让我们的代码更加的清晰,使用方便,不过这是代码管理上,其实还有一个功能用单例实现也比较合适,就是管理静态变量。javascrip中并没有静态变量,没有static这类的关键字,所以定义任何变量理论上都是可以修改的,所以实现静态变量就很重要了,但是javascript是比较灵活的,我们可以定义一个特权方法,不提供赋值方法。


var Conf = (function() {
// 私有变量
var conf =  {
  MAX_NUM: 100,
  MIN_NUM: 1,
  COUNT: 10000,
}
return {
  get: function(name) {
    return conf[name] ? conf[name] : null;
  }
}
})();

var count = Conf.get('COUNT');  //10000

  我们习惯把静态变量都大写,在其它编程语言中静态变量都习惯与大写,所以在javascript中虽然模拟的静态变量我们也要尊重这一使用习惯。有些时候对于单例对象需要延迟创建,所以在单例中还存在一种延迟创建的形式,有人称为‘惰性创建’。

// 惰性创建
  var LazySingle = (function() {
    // 单例实例引用
    var _instance = null;
    // 单例
    function Single() {
      // 这里定义私有属性和方法
      return {
        publicMethod: function() {},
        publicProperty: '1.0',
      }
    }
    // 获取单例对象接口
    return function() {
      // 如果为创建单例而创建单例
      if(!_instance) {
        _instance = Single();
      }
      // 返回单例
      return _instance;
    }
  })();
 // 我们测试一下可以看出通过LazySingle对象可以成功获取内部创建的单例对象了

  单例模式有时也被称为单体模式,他是一个只允许实化一次的对象类,有时这么做也是为了节省系统资源。当然Javascript中单例模式经常作为命名空间对象来实现,通过单例对象我们可以将各个模块的代码井井有条的梳理到一起。
  所以如果你想让系统中只存在一个对象,那么单例就是最佳解决方法。

总结

  创造型设计模式是一类处理对象创建的设计模式,通过某种方式控制对象的创建来避免基本对象创建是可能导致设计上的问题或增加设计上的复杂度。在日常开发过程中,我们不能为了用设计模式而用,而是要借鉴设计模式的思想对我们项目架构、设计思想进行优化,开发出可持续、维护成本低的项目。

参考资料

  1. JavaScript 设计模式
  2. C语言中文网-设计模式(c.biancheng.net/view/1338.h…

image