JavaScript设计模式系列一之工厂模式(附案例源码)

JavaScript设计模式系列

JavaScript设计模式系列,讲述大概20-30种设计模式在JavaScript中的运用

后面对应的篇幅会陆续更新,欢迎大家提出建议

这是设计模式系列第一篇,讲述工厂模式

注意

JavaScript设计模式系列github地址

深入系列文章部分是有先后顺序的,按照目录结构顺序阅读效果最好。

勘误及提问

如果有疑问或者发现错误,可以在相应的 issues 进行提问或勘误。

如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

工厂模式

首先需要说一下工厂模式。工厂模式根据抽象程度的不同分为三种

  • 简单工厂模式
  • 工厂方法模式
  • 抽象工厂模式

1.简单工厂模式

简单工厂模式:又称为静态工厂方法模式,它属于类创建型模式。

在简单工厂模式中,可以根据参数的不同返回不同类的实例。

由工厂对象决定创建某一种产品对象类的实例。

上🌰

// #简单工厂模式第一种
/**
 * 足球类
 */
var FootBall = function  () {
    this.play = function () {
        console.log('我在踢足球');
    }
}

/**
 * 篮球类
 */
var BasketBall = function  () {
    this.play = function () {
        console.log('我在打篮球');
    }
}

var football = new FootBall();
football.play();
var basketball = new BasketBall();
basketball.play();

/**
 * 球类工厂
 */
var Ball = function(name) {
    switch (name) {
        case '足球':
            return new FootBall();
        break;
        case '篮球':
            return new BasketBall();
        break;
    }
}
var football =  Ball('足球');
football.play();
var basketball =  Ball('篮球');
basketball.play();

// #简单工厂模式第一种end

这段案例可以简单的这么去理解,假设我们需要多个球,我们希望在使用球

的时候,只需要告诉管理员我们需要的球的类型,不需要一个个去找对应的球

这个管理员就相对于工厂函数。

简单讲就是使用简单工厂模式,那么你就不需要关心它的具体实现,

你只需要知道你要使用的类型,那么工厂函数会自动帮你去做对应的事情

再看一个🌰

// #简单工厂模式第二种

/**
 * 球类工厂
 */
var Ball = function(name) {
    // 创建一个对象,对对象扩展扩展属性还有方法
    var o = new Object();
    o.name = name;
    //默认的方法 如果在加上一个羽毛球类,这时候就不需要补充play方法
    o.play = function () {
        console.log('我在打'+name);
    }
    if (name === '足球') {
        o.play = function () {
            console.log('我在踢'+name);
        }
    }else if (name === '篮球') {
        o.play = function () {
            console.log('我在打'+name);
        }
    }
    // 将对象返回
    return o;
}
var football =  Ball('足球');
football.play();
var basketball =  Ball('篮球');
basketball.play();

// #简单工厂模式第二种end

这段案例是用对象的方式代替多个类,把相同点抽离出来,

不同点在一一做类型判断,这样也是简单工厂模式实现的另一种方式

简单工厂模式的优点:

  • 工厂类含有必要的判断逻辑,可以决定在什么时候创建哪一个产品类的实例,客户端可以免除直接创建产品对象的责任,而仅仅“消费”产品;简单工厂模式通过这种做法实现了对责任的分割,它提供了专门的工厂类用于创建对象

  • 客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可,对于一些复杂的类名,通过简单工厂模式可以减少使用者的记忆量。

简单工厂模式的缺点

  • 由于工厂类集中了所有产品创建逻辑,一旦不能正常工作,整个系统都要受到影响。
  • 使用简单工厂模式将会增加系统中类的个数,在一定程序上增加了系统的复杂度和理解难度。
  • 系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护。

简单工厂模式的适用情况

在以下情况下可以使用简单工厂模式:

  • 工厂类负责创建的对象比较少:由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。
  • 客户端只知道传入工厂类的参数,对于如何创建对象不关心:客户端既不需要关心创建细节,甚至连类名都不需要记住,只需要知道类型所对应的参数。

简单工厂模式总结

  • 简单工厂模式的要点在于:当你需要什么,只需要传入一个正确的参数,就可以获取你所需要的对象,而无须知道其创建细节。
  • 简单工厂模式最大的优点在于实现对象的创建和对象的使用分离,将对象的创建交给专门的工厂类负责,但是其最大的缺点在于工厂类不够灵活,增加新的具体产品需要修改工厂类的判断逻辑代码,而且产品较多时,工厂方法代码将会非常复杂。

2.工厂方法模式

工厂方法模式:又称为工厂模式,也叫虚拟构造器模式或者多态工厂模式

它属于类创建型模式。在工厂方法模式中,工厂父类负责定义创建产品对

象的公共接口,而工厂子类则负责生成具体的产品对象,

这样做的目的是将产品类的实例化操作延迟到工厂子类中完成,

即通过工厂子类来确定究竟应该实例化哪一个具体产品类

这样解释可能会有点抽象,不急,下面来看一段🌰

// # 工厂方法模式
// 安全模式创建工厂类
var Ball = function (type,name) {
    /**
     * 安全模式 Ball也可以运行处new Ball的效果
     */
    if(this instanceof Ball) {
        var s = new this[type](name);
        return s;
    }else {
        return new Ball(type,name);
    }
}
// 工厂原型中设置创建所有类型数据对象的基类
Ball.prototype = {
    basketBall: function(name) {
        this.play = function() {
            console.log('我在打'+name);
        }
    },
    footBall: function(name) {
        this.play = function() {
            console.log('我在踢'+name);
        }
    },
    badmintonBall: function(name) {
        this.play = function() {
            console.log('我在打'+name);
        }
    },
    // ....
}
var football = new Ball('footBall','足球');
football.play();
var basketball = new Ball('basketBall','篮球');
basketball.play();
var badmintonball = new Ball('badmintonBall','羽毛球');
badmintonball.play();

// # 工厂方法模式end

这段案例是这么去理解的,我们先创建一个球类工厂,

这个球类工厂是一个抽象的,不做具体的实现,然后我们在这个球类工厂里

面在去定义对应的球类实现,如篮球,羽毛球,足球等实现,

在工厂方法模式中,抽象类工厂只是负责定义一个对外的公共接口,

而工厂子类则负责生成具体的产品对象。

这样做的目的是将产品类的实例化操作延迟到工厂子类中完成,即通过

工厂子类来确定究竟应该实例化哪一个具体产品类

如果这时候在出现一个新的球类运动,只需要为这种新类型的球类创建

一个具体的球类实现就可以,这一特点无疑使得工厂方法模式具有

超越简单工厂模式的优越性,更加符合“开闭原则”

上面案例包含了一个安全模式的知识点

// 安全模式创建工厂类
var Ball = function (type,name) {
    /**
     * 安全模式 Ball也可以运行处new Ball的效果
     */
    if(this instanceof Ball) {
        var s = new this[type](name);
        return s;
    }else {
        return new Ball(type,name);
    }
}

这段代码主要解决的问题是,有些同学使用工厂类的时候,

忘记使用关键字new,得不到预期想要的效果

这边的解决方案就是,在构造函数开始时先判断当前对象this指代

是不是当前工厂类,如果不是则通过new关键字创建对象返回,

这样就可以实现不使用new关键词也可以达到相同的效果了

工厂方法模式的优点:

  • 在工厂方法模式中,工厂方法用来创建客户所需要的产品,同时还向客户隐藏了哪种具体产品类将被实例化这一细节,用户只需要关心所需产品对应的工厂,无须关心创建细节,甚至无须知道具体产品类的类名。
  • 基于工厂角色和产品角色的多态性设计是工厂方法模式的关键。它能够使工厂可以自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部。工厂方法模式之所以又被称为多态工厂模式,是因为所有的具体工厂类都具有同一抽象父类。
  • 使用工厂方法模式的另一个优点是在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其他的具体工厂和具体产品,而只要添加一个具体工厂和具体产品就可以了。这样,系统的可扩展性也就变得非常好,完全符合“开闭原则”。

工厂方法模式的缺点:

  • 在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。
  • 由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度

工厂方法模式的适用情况

在以下情况下可以使用工厂方法模式:

  • 一个类不知道它所需要的对象的类:在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建;客户端需要知道创建具体产品的工厂类。
  • 一个类通过其子类来指定创建哪个对象:在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。
  • 将创建对象的任务委托给多个工厂子类中的某一个,客户端在使用时可以无须关心是哪一个工厂子类创建产品子类,需要时再动态指定,可将具体工厂类的类名存储在配置文件或数据库中。

工厂方法模式总结

  • 工厂方法模式是简单工厂模式的进一步抽象和推广。由于使用了面向对象的多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。在工厂方法模式中,核心的工厂类不再负责所有产品的创建,而是将具体创建工作交给子类去做。这个核心类仅仅负责给出具体工厂必须实现的接口,而不负责产品类被实例化这种细节,这使得工厂方法模式可以允许系统在不修改工厂角色的情况下引进新产品。
  • 工厂方法模式的主要优点是增加新的产品类时无须修改现有系统,并封装了产品对象的创建细节,系统具有良好的灵活性和可扩展性;其缺点在于增加新产品的同时需要增加新的工厂,导致系统类的个数成对增加,在一定程度上增加了系统的复杂性。

3.抽象工厂模式

抽象工厂模式:通过对类的工厂抽象使其业务用于对产品类簇的创建,

而不是负责创建某一类产品的实例,属于对象创建型模式。

抽象类一直出现在我们的文章中,那么这边先来解释一下什么是抽象类

抽象类是一种声明但不能使用的类,当你使用时就会报错,

在JavaScript中abstract还是一个保留字,所以目前来说还不能像

传统面向对象语言那么轻松的去创建抽象类.

不过JavaScript有自己的实现方式,可以模拟出抽象类

来一段代码

// 抽象类的介绍
var Ball = function () {}
Ball.prototype = {
    play: function () {
        return new Error('抽象方法不能调用');
    }
}

解释:

我们可以看到创建的Ball类其实什么都不能做,创建时没有任何属性,

原型定义的方法也不能使用,否则就会报错。但是在继承上却是很有用的,

因为定义了一种类,并定义了该类所具备的方法,

如果没有在子类中重写这写方法,那么调用的时候就会报错。

这一特点可以很好的提醒子类去重写这一方法,不然会在调用的时候提示错误

那么在了解了什么是抽象类的情况下,我们在来比较一下工厂方法模式与抽象工厂模式的不同点,以方便我们更好的去理解抽象工厂模式

工厂方法模式与抽象工厂模式的对比

在工厂方法模式中具体工厂负责生产具体的产品,每一个具体工厂对应一种具体产品,工厂方法也具有唯一性,一般情况下,一个具体工厂中只有一个工厂方法或者一组重载的工厂方法。但是有时候我们需要一个工厂可以提供多个产品对象,而不是单一的产品对象。

为了更清晰地理解抽象工厂模式,需要先引入两个概念:

  • 产品等级结构 :产品等级结构即产品的继承结构,

如一个抽象类是电视机,其子类有海尔电视机、海信电视机、

TCL电视机,则抽象电视机与具体品牌的电视机之间构成了

一个产品等级结构,抽象电视机是父类,而具体品牌的电视机是其子类。

  • 产品族 :在抽象工厂模式中,产品族是指由同一个工厂生产的,

位于不同产品等级结构中的一组产品,如海尔电器工厂生产的

海尔电视机、海尔电冰箱,海尔电视机位于电视机产品等级结构中,

海尔电冰箱位于电冰箱产品等级结构中。

抽象工厂模式与工厂方法模式最大的区别在于,工厂方法模式针对的是一个产品等级结构,而抽象工厂模式则需要面对多个产品等级结构,一个工厂等级结构可以负责多个不同产品等级结构中的产品对象的创建 。当一个工厂等级结构可以创建出分属于不同产品等级结构的一个产品族中的所有对象时,抽象工厂模式比工厂方法模式更为简单、有效率。

这句话比较简单的理解方式就是:如果一个工厂只需要生产一个类型的

产品比如说电视机,那么用工厂方法模式是比较合理的,

如果这个工厂又需要成产电视机,又需要生产冰箱之类的,

那么这时候用工厂抽象模式就是最合适的。

上🌰,再解释

// # 抽象工厂模式
var Sport = function(subType, superType) {
    if( typeof Sport[superType] === 'function'){
        // 缓存类
        function F() {};
        // 继承父类属性和方法
        F.prototype = new Sport[superType]();
        // 将子类constructor 指向子类
        subType.constructor = subType;
        // 子类原型继承 “父类”
        subType.prototype = new F(); 
    }else {
        // 不存在抽象类则抛出错误
        throw new Error('未创建该抽象类');
    }
}

// 球类运动抽象类
Sport.Ball = function () {
    this.type = 'ball';
}
Sport.Ball.prototype = {
    play: function () {
        return new Error('抽象方法不能调用');
    }
}

// 力量型运动抽象类
Sport.Power = function () {
    this.type = 'power';
}
Sport.Power.prototype = {
    play: function () {
        return new Error('抽象方法不能调用');
    }
}

// 速度型运动抽象类
Sport.Speed = function () {
    this.type = 'speed';
}
Sport.Speed.prototype = {
    play: function () {
        return new Error('抽象方法不能调用');
    }
}

// 篮球类
var BasketBall = function (name) {
    this.name = name;
};
// 抽象工厂实现对球类运动的继承
Sport(BasketBall,'Ball');
BasketBall.prototype.play = function () {
    console.log('我在玩'+this.name);
}


// 举重类
var WeightLifting = function (name) {
    this.name = name;
};
// 抽象工厂实现对力量型运动的继承
Sport(WeightLifting,'Power');
WeightLifting.prototype.play = function () {
    console.log('我在玩'+this.name);
}

// 跑步类
var Running = function (name) {
    this.name = name;
};
// 抽象工厂实现对速度运动的继承
Sport(Running,'Speed');
Running.prototype.play = function () {
    console.log('我在'+this.name);
}

// 抽象工厂模式实现
var basketBall = new BasketBall('篮球');
console.log(basketBall.type);//ball
basketBall.play();
var weightLifting = new WeightLifting('举重');
console.log(weightLifting.type);//power
weightLifting.play();
var running = new Running('跑步');
console.log(running.type);//ball
running.play();

/** 输出结果
 * ball
 * 我在玩篮球
 * power
 * 我在玩举重
 * speed
 * 我在跑步
 */


// # 抽象工厂模式end

这段栗子先是创建一个运动类的抽象工厂,通过这个暴露外部调用的接口,

传递2个参数,一个是subType,当前实例化的对象,也就是子类,

一个是superType,需要继承的父类(抽象类)的名称,

在工厂函数中实现了子类对父类的继承。

在继承过程中有一个地方需要注意,就是在对过渡类继承的时候,

我们不是继承父类原型,而是通过new关键字复制父类的一个实列,

这样做的目的是过渡类不仅仅继承父类的原型方法,还需要继承

父类的对象属性,所以通过new关键字的方式实现了继承。

然后通过在抽象工厂类上面进行扩展对应的抽象类,

也就是我们需要通过继承的父类,我这边添加了3个抽象类

Ball,Power,Speed,分别给抽象类指定了type属性,还有方法play,

既然创建了抽象类,那么下面就是开始使用抽象工厂去创建子类,

这边分别对Ball,Power,Speed 3个抽象类进行了实现

创建了BasketBall(球类),WeightLifting(力量),Running(速度)3个子类

3个子类分别对play方法进行了实现

最后就是对于子类的调用实现,在实现子类调用的时候,

我们可以获取到继承的父类当中对应的type

抽象工厂模式的优点:

  • 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。

  • 增加新的具体工厂和产品族很方便,无须修改已有系统,符合“开闭原则”。

抽象工厂模式的缺点:

  • 开闭原则的倾斜性(增加新的工厂和产品族容易,增加新的产品等级结构麻烦)。

  • 增加新的产品等级结构:对于增加新的产品等级结构,需要修改所有的工厂角色,包括抽象工厂类,在所有的工厂类中都需要增加生产新产品的方法,不能很好地支持“开闭原则”。

抽象工厂模式的适用情况:

在以下情况下可以使用抽象工厂模式:

  • 一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是重要的。

  • 系统中有多于一个的产品族,而每次只使用其中某一产品族。
    属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来。

  • 系统提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于具体实现。

抽象工厂模式总结

  • 抽象工厂模式是所有形式的工厂模式中最为抽象和最具一般性的一种形态。抽象工厂模式与工厂方法模式最大的区别在于,工厂方法模式针对的是一个产品等级结构,而抽象工厂模式则需要面对多个产品等级结构。

  • 抽象工厂模式适用情况包括:一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节;系统中有多于一个的产品族,而每次只使用其中某一产品族;属于同一个产品族的产品将在一起使用;系统提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于具体实现。

4.三大工厂模式的关联性

当抽象工厂模式中每一个具体工厂类只创建一个产品对象,

也就是只存在一个产品等级结构时,抽象工厂模式转换成工厂方法模式;

当工厂方法模式中抽象工厂与具体工厂合并,提供一个统一的工厂

来创建产品对象,并将创建对象的工厂方法设计为静态方法时,

工厂方法模式退化成简单工厂模式。

注:根据实际适用情况去选择对应的工厂模式

注意

JavaScript设计模式系列github地址

深入系列文章部分是有先后顺序的,按照目录结构顺序阅读效果最好。

勘误及提问

如果有疑问或者发现错误,可以在相应的 issues 进行提问或勘误。

如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

推荐阅读更多精彩内容