JS设计模式 - 工厂模式与抽象工厂模式

1,512 阅读8分钟

这是我参与更文挑战的第19天,活动详情查看更文挑战

1. 构造函数模式

(1) JS中创建新对象的三种常用方法

var newObject = {};
 
var newObject = Object.create( Object.prototype );
 
var newObject = new Object();

//上面的三中方式创建出来的新对象等同,Object.create(null)是一个简单的对象,不具有其他任何的属性。

(2) 创建新属性的四中方式

newObject.someKey = "Hello World";
 
newObject["someKey"] = "Hello World";
 
Object.defineProperty( newObject, "someKey", {
    value: "for more control of the property's behavior",
    writable: true,
    enumerable: true,
    configurable: true
});
 
Object.defineProperties( newObject, {
  "someKey": {
    value: "Hello World",
    writable: true
  },
 
  "anotherKey": {
    value: "Foo bar",
    writable: false
  }
 
});


(3) 用法

普通方式(最实用)

function Car(model, year, miles) {
    this.model = model;
    this.year = year;
    this.miles = miles;
    this.output= function () {
        return this.model + "走了" + this.miles + "公里";
    };
}
 
var car_benchi = new Car('benchi', '2017', '300');
var car_aodi = new Car('aodi', '2016', '600');
 
console.log(car_benchi.output === car_benchi.output); //false

为什么最后判断函数是否为同一个对象,因为之前在网上看到文章有说到这种方式在创建新对象的时候会创建多个匿名函数,造成内存问题。注意这里说的是这个匿名函数,不是output变量。

现在我们将上面new的方式用普通的函数实现,因为这个是需要用来和prototype & 工厂模式方式进行比较

function Car(model, year, miles) {
    var object = Object.create(null);
 
    /*prototype*/
    Object.defineProperty( object, "__proto__", {
        value: Car.prototype,
        writable: true,
        enumerable: false,
        configurable: false
    })
 
    object.model = model;
    object.year = year;
    object.miles = miles;
 
    object.output = function () {
        return function () {
            return this.model + "走了" + this.miles + "公里";
        }.call(this);
    }
 
    return object;
}
 
var car_benchi =  Car('benchi', '2017', '300');
var car_aodi =  Car('aodi', '2016', '600');
 
console.log(car_benchi.output === car_benchi.output); //false

至于创建的对象使用的是Object.create(null),以及__proto__属性使用Object.defineProperty的原因是什么呢?

因为使用new创建的对象是具有原型链的,所以我需要使用__proto__,然后这个属性是可写,不可枚举,不可配置的。至于为什么是不可配置的,是因为当我使用var object = {} / new Object()的时候,我是用defineProperty是不起效果的。所以用了 Object.create(null)的方式,这个也是今天自己测试的时候注意到的。

Note: 开发过程中一般不允许操作__proto__的,尽管现在大部分浏览器引起都已经支持。

image.png

prototype

解决上面的问题其实可以将output函数提到最外层也可以,只是这样不好维护,而且污染了全局变量。

function Car(model, year, miles) {
    this.model = model;
    this.year = year;
    this.miles = miles;
}
 
Car.prototype.output= function () {
    return this.model + "走了" + this.miles + "公里";
};
 
var car_benchi = new Car('benchi', '2017', '300');
var car_aodi = new Car('aodi', '2016', '600');
 
console.log(car_aodi.output === car_benchi.output); //true

可以看到这里是在Car.prototype上添加了一个新的方法。因为针对多个对象共享只有找到公共点,那就是原型链上的Car.prototype。切记不要重新赋值Car.prototype,因为如果改了的话,原型链会被破坏。

(4) 总结

本人现在用到prototype的地方基本上是为了一些js内置对象的扩展,例如如期格式,数组变化,因为这样可以让我省去时间去为每个对象进行修改。

构造函数的本质我认为还是在它的原型链,是为了找共同点,因为原型链是大家共有的,而且是一个js原生的对象。不用我们自己去维护。


2. 工厂模式

工厂模式类比到现实生活中的工厂,可以产生大量相似的商品,去做同样的事情,实现同样的效果。但是这样的话岂不是感觉完全可以用构造函数模式实现,而且我还可以拥有原型链。

现在假设有一家工厂之前只可以加工生产奔驰一款车,那么它的生产过程是一个工厂模式还是构造函数模式呢。这个很难说,为什么呢?因为如果它每一个阶段只会生产一个型号的车,那么我们是不是可以当作是一个构造函数模式,因为现在所做的事情都是一样的,同样的输入输出。我们是不是可以把这个工厂直接看作是一个new,每一次生产出一个新的奔驰车。说到这里请看一下上面的那个new的方式用普通的函数实现,我们现在的工厂就是在做这样的事情(如果不适用new的话),当前情况下,本人认为二者是相等的。

好了现在,我们的工厂在生产新的产品的时候发现之前的一款车有很好的市场,所以在开发新的汽车型号的时候,还要生产以前的汽车,现在我们在使用之前的方法不行了。因为我需要有不同的输入输出。那么我们先将上面的方式改写一下

function Car(model, year, miles, version) {
    function carBasicInformation() {
        var object = Object.create(null);
 
        /*prototype*/
        Object.defineProperty( object, "__proto__", {
            value: Car.prototype,
            writable: true,
            enumerable: false,
            configurable: false
        })
 
        object.model = model;
        object.year = year;
        object.miles = miles;
 
        object.output = function () {
            return function () {
                return this.model + "走了" + this.miles + "公里";
            }.call(this);
        }
 
        return object;
    }
 
    //0001型号有导航系统
    function Car_0001 () {
        var car_0001 = carBasicInformation(model, year, miles);
        car_0001.omnirange = 'omnirange_01'; 
 
        return car_0001;
    }
 
    //0002型号没有导航系统 
    function Car_0002 () {
        var car_0002 = carBasicInformation(model, year, miles);
 
        return car_0002;
    }
 
    switch (version) {
        case '0001':
            return Car_0001();
        case '0002':
            return Car_0002();
        default:
            break; //当然工厂肯定不会什么都不做。
    }
}
 
var aodi_0001 =  Car('benchi', '2017', '300', '0001');
var aodi_0002 =  Car('aodi', '2016', '600', '0002');
 
console.log(aodi_0001.omnirange); //omnirange_01
console.log(aodi_0002.omnirange); //undeinfed

我认为现在这个应用方式才是工厂模式的意义,在于不同的输入输出。可能会有人提问到,我完全可以在把这些判断逻辑写到以前的new car()的内部去判断,那么我岂不是还是构造函数模式?

确实是这样,那么我们来分析一下这个判断的逻辑是个什么东西。是说需要两种不同的车型,这是一个业务逻辑,我们将上面的代码对应到工厂的角色中,Car是工厂,Car_0001和Car_0002是两个不同的组。判断逻辑是市场部的工作人员。Car_0001和Car_0002是构造函数模式,因为我做的是同一个东西,判断逻辑肯定是市场部的人在分析市场需求得到的。那么跟据面向对象也好,还是模块话开发也好,Car_0001和Car_0002是不是应该只做自己的事情,毕竟每种车生产什么车,肯定是业务逻辑决定代码结构。如果我们将所有的内容放在一起,改了car_0001的东西会不会影响到car_0002的内容,同时也违反了开放-封闭原则,而且以后有了新的型号生产,是不是还需要改已经稳定了两种型号的代码,维护起来很负责。

提到开放-封闭原则:软件实体应该是可扩展,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。那么如果有了新的型号,我们还是需要去修改上面的代码,因为啥,因为业务逻辑判断就是这样。

所以上面的代码也不是完善的,还是需要修改的。相信大家还记的上面的信息prototype。我们可在上面添加新的东西而且不影响其他的功能。


3. 抽象工厂模式

对于工厂模式的进一步就是抽象工厂。现在我们先思考一下上面的需求,需要添加新的version,但是又不想修改已经稳定的代码,但是还是有着共同的东西,总结一句话就是‘求同存异’。

//求同
function basicCarInformation(model, year, miles) { //这个地方其实可以不用这么复杂,只是本人太懒,直接将上面的代码copy了。
    var object = Object.create(null);
 
        /*prototype*/
        Object.defineProperty( object, "__proto__", {
            value: basicCarInformation.prototype,
            writable: true,
            enumerable: false,
            configurable: false
        })
 
        object.model = model;
        object.year = year;
        object.miles = miles;
 
        object.output = function () {
            return function () {
                return this.model + "走了" + this.miles + "公里";
            }.call(this);
        }
 
        return object;
}
 
inheritAbstactFactory.prototype.basicCarInformation = basicCarInformation;
 
//存异
function inheritAbstactFactory(option, version) {
    return function(model, year, miles) {
        var basicInformation = basicCarInformation(model, year, miles);
        basicInformation.option = option;
        basicInformation.version = version;
 
        return basicInformation;
    }
}
 
var CarFactory0001 = inheritAbstactFactory({omnirange : 'omnirange_01'}, 'aodi_0001');
 
var aodi_0001 =  CarFactory0001('benchi', '2017', '300');
aodi_0001.output();

现在我们再有新的version,只需要执行var CarFactory0001 = inheritAbstactFactory({omnirange : 'omnirange_01'}, 'aodi_0001');添加新的version,就好了。

注意: var basicInformation = basicCarInformation(model, year, miles); 这一行的实现可以是内部函数,或者使用寄生构造函数的方式。这里采用prototype是为了对比面向对象语言,现在basicCarInformation就是base类,inheritAbstactFactory抽象类,CarFactory0001继承抽象类的子类,具体的不同可以由子类自己去创建。当然可以使用另外两种方式,毕竟工厂模式的关注点不在这里。

上面的求同是一个构造函数模式,存异是工厂模式(求同是基础)。二者的关注点不同。


4. 抽象工厂的应用

(摘抄与 design pattern)当应用于以下情况时,工厂模式可能特别有用:

  • 当我们的对象或组件设置涉及高度的复杂性时
  • 当我们需要根据我们所处的环境轻松生成不同的对象实例时
  • 当我们正在处理许多共享相同属性的小对象或组件时
  • 使用其他对象的实例组合对象时,只需要满足API合约(又名 duck typing)的工作。这对于解耦是有用的。

分析一下上面的几句话:

  • 高度的负复杂性指的就是上面的逻辑判断
  • 需求的变化
  • 求同存异,许多共享并不是全部共享,因为全部共享的话就直接构造函数模式了
  • 这里的的意义是说care的只是输入输出,至于对象是否与原型链相关不在意,就好像call,apply的用法。就是一种duck typing。