JavaScript-工厂模式

215 阅读11分钟

工厂模式

在了解工厂模式之前,我们需要知道工厂模式解决了什么问题,并且带来的好处。

举一个例子,在传统的面向对象编程中,我们去开发一需求的时候,难免会经历创建多个类的情况,而类与多个模块复用就会导致代码的可阅读性,并且耦合性极强,导致我们去维护代码的时候,付出的成本很大。

比如我有一个需求,在登录的时候去判断用户名的输入校验,而且该校验的规则和注册时的校验相同,并且弹出校验失败的弹出层。

var checkNameAlert = function( context ){
    this.text = context
}
checkNameAlert.prototype.show = function(){
    // ...
}
const loginAlert = new checkNameAlert("用户名输入错误")
loginAlert.show()

可以看到,我在上面的代码中,注册了一个类,并且该类在登录和注册校验中都可以使用,但是如果我在后面的需求中,改变了该类的命名,就会导致登录和注册时不能使用并会抛出异常。

所以我们就需要工厂模式去解决这个耦合性的问题,当然,他的功能不止如此。

概念

工厂模式是创建型的其中一种,用来批量创建对象。我们可以不用关注创建对象的具体逻辑,只需要得到你所需要的对象本身,从而我们可以把这种模式叫做工厂模式。

工厂模式可以根据复杂度分成:简单工厂,工厂方法和抽象工厂。

JavaScript 中,用于 abstart 现在还没有实现,并且保留了关键字。所以我们只需要了解工厂模式的概念即可。

简单工厂

简单工厂也被称作静态工厂,是由工厂来决定创建某一工厂的实例,主要用来创建同一对象。

在前面的例子中我们只讲述了工厂模式可以解决耦合性,现在我们利用一个完善的代码来解释工厂模式的具体运用并且他是如何批量产生对象的。同时我们新增两个需求,在登陆的时候判断用户是否存在,如果不存在我们弹出一个提示框,如果存在弹出欢迎回来框。

代码如下:

var AlertFactory = function( type ) {
    // 检测用户名
    var checkNameAlert = function( context ){
        this.text = context
    }
    checkNameAlert.prototype.show = function(){
        // ...
    }
​
    // 登陆失败提示框
    var loginConfirm = function( text ) {
        this.context = text
    }
    loginConfirm.prototype.show = function() {
        // 登陆失败提示
    }
​
    // 欢迎回来提示框
    var loginBack = function( text ) {
        this.context = text
    }
    loginBack.prototype.show = function() {
        // ...
    }
​
    switch( type ) {
        case "checkName":
            return new checkNameAlert("用户名输入错误")
            break;
        case "fileConfirm":
            return new loginConfirm( "您的用户明不存在,请重新输入" )
            break;
        case "comeBack":
            return new loginBack( "欢迎回来!" )
            break;
        default:
            throw new Error( "该类暂未实现" )
    }
}

上面的代码中,我们实现了 AlertFactory 函数,并在其中根据不同的 type 抛出了3个类,checkNameAlert,loginConfirm,loginBack,此时 AlertFactory 就是一个简单的工厂。

在使用的过程中,我们可以直接调用工厂来获取你想要的类,你不需要关注他实现类的具体过程,你只需要获得你想要的类并实现你当前的逻辑。

// 获取校验用户名的类
var checkName = AlertFactory("checkName")
checkName.show()

简单工厂可以根据你传入的正确的参数来获取你想要的对象,但是他只适合简单的场景,固定的对象数量,如果你需要增加一个对象,我们就要在工厂中去定义一个新的类,并在 switch 中新增一个判断条件。当这个对象的数量越来越庞大的时候,便很难去维护。

所以我们需要一个新的方法去解决这个问题,他就是简单工厂的升级版工厂方法

工厂方法

工厂方法的本意是把创建对象的过程推迟到子类中,这样核心类就变成了抽象类。但是在传统的 JavaScript 面向对象中很难去实现抽象类,所以这里我们只需要借助他的思想即可。

在简单工厂中,我们去扩展的时候会去修改两处地方,一是新增类方法,二是新增判断条件,而现在我们可以借助工厂方法的思想来省略第二步的,从而让工厂自己去生产出我们想要的对象。

function AlertFactory() {}
​
AlertFactory.prototype = {
    // 检测用户名
    checkNameAlert:function( context ) {
        this.text = context
        this.show = function() {
            // ...
        }
    },
​
    // 登陆失败提示框
    loginConfirm:function( context ) {
        this.text = context
        this.show = function() {
            // ...
        }
    },
​
    // 欢迎回来提示框
    loginBack:function( context ) {
        this.text = context
        this.show = function() {
            // ...
        }
    }
}
​
var loginFactory = new AlertFactory()
var checkNameAlert = new loginFactory.checkNameAlert("用户名输入错误")
checkNameAlert.show()

在上面的代码中,我们把创建对象的过程放在了 prototype 下,这样在新增的时候就可以不用去改动两个地方,直接在 prototype 下新增一个对象就可以了,从而减少了维护的成本。

但是这样的写法依旧会导致一些小的问题,如果用户没有去实例化 AlertFactory 类,或者忘记了实例化了下面的工厂方法,就会导致代码不会正常运行。虽然是是使用上面的小问题,但是为了提供便利性,我们还是的进一步优化。

安全模式

安全模式正是解决这类问题的,从上面的代码我们可以看出,在封装工厂方法的过程中,我们是没有任何问题的,但是如果在使用的时候,会因为很难用或者步骤繁琐并且没有安全保障机制,就失去了封装的本质意义。

我们对 AlertFactory 类进行改写,利用 instanceof 来判断是否已经实例化了,如果实例化了,我们取出 prototype 下面的对象即可,反之就是对工厂进行实例化处理,代码如下:

function AlertFactory(type,context) {
    if( this instanceof AlertFactory ){
        var s = new this[type](context)
        return s
    } else {
        return new AlertFactory(type,context)
    }
}

在使用的时候,我们只需要调用正确的参数,来获取你所需的对象即可。这样我们就简化了使用过程,避免了错误使用的异常。

var checkNameAlert = AlertFactory( "checkNameAlert","用户名输入错误" )
checkNameAlert.show()

抽象工厂

上面的两个工厂中,我们只创建了关于登陆时的类,如果存在注册,退出时,我们也需要创建类的情况下,显然简单工厂和工厂方法是不能满足我们需求的。所以接下来我们就需要使用的抽象类。

抽象工厂的定义就是通过类的抽象使得业务适用于一个产品类簇的创建,而不负责某一类产品的实例。

但是在 JavaScriptabstract 目前还是一个保留字,所以不能像传统的面向对象语言那样轻松的创建。抽象类是一种申明式的类,一旦直接使用就会抛出异常,所以我们在 JavaScript 中利用手动抛出异常来模拟抽象类。

function AbstractAlertFactory () {}
AbstractAlertFactory.prototype.getLogin = function() {
    return new Error("抽象类不能直接调用")
}

创建方法

如果创建抽象类的过程中可以有一个很好的提示,那么对于忘记重写子类的这些错误避免是有很大的帮助,这也是抽象类的一个作用,即定义一个产品簇,并声明一些必备的方法,如果子类没有去重写就会抛出异常。

function AbstractAlertFactory ( subType,superType ) {
    if( typeof AbstractAlertFactory[superType] === 'function' ) {
        // 缓存类
        function F(){}
        // 继承父类属性和方法
        F.prototype = new AbstractAlertFactory[superType]()
        // 将子类 constructor 指向子类
        subType.constructor = subType
        // 子类原型继承父类
        subType.prototype = new F()
    } else {
        return new Error("未创建该抽象类")
    }
}
​
AbstractAlertFactory.login = function() {
    this.type = "login"
}
AbstractAlertFactory.login.prototype = {
    checkNameAlert:function() {
        return new Error("抽象方法不能调用")
    },
    loginConfirm:function() {
        return new Error("抽象方法不能调用")
    },
    loginBack:function() {
        return new Error("抽象方法不能调用")
    }
}
​
AbstractAlertFactory.register = function() {
    this.type = "register"
}
AbstractAlertFactory.register.prototype = {
    checkPassAlert:function() {
        return new Error("抽象方法不能调用")
    }
}
​
AbstractAlertFactory.loginOut = function() {
    this.type = "loginOut"
}
AbstractAlertFactory.loginOut.prototype = {
    loginOutSuccess:function() {
        return new Error("抽象方法不能调用")
    }
}

在上面的代码中,我们定义了 AbstractAlertFactory 抽象父类,利用 typeof 方法来判断传入的值在当前的类下是否定义的子类。同时在函数内部中定义了 F 函数来缓存传入的子类,然后继承父类的子类。在 AbstractAlertFactory 定义了 3 中不同的抽象子类,loginregisterloginOut。便让该父类拥有了创建不同类簇能力。

使用方法

抽象工厂用来创建子类,我们需要定义一些子类,让后让子类继承相应的产品簇的抽象类。代码如下:

var loginFactory = function() {}
AbstractAlertFactory( loginFactory,"login" )
loginFactory.prototype.checkNameAlert = function( context ) {
    this.text = context
    this.show = function() {
        console.log(this.text);
        // ...
    }
}
loginFactory.prototype.loginConfirm = function( context ) {
    this.text = context
    this.show = function() {
        // ...
    }
}
loginFactory.prototype.loginBack = function( context ) {
    this.text = context
    this.show = function() {
        // ...
    }
}
​
var registerFactory = function(){}
AbstractAlertFactory( registerFactory,"register" )
registerFactory.prototype.checkPassAlert = function( context ) {
    this.text = context
    this.show = function() {
        // ...
    }
}
​
var loginOutFactory = function(){}
AbstractAlertFactory( loginOutFactory,"loginOut" )
loginOutFactory.prototype.loginOutSuccess = function( context ) {
    this.text = context
    this.show = function() {
        // ...
    }
}
​
var loginAlert = new loginFactory()
var checkNameAlert = new loginAlert.checkNameAlert("用户名输入错误")
checkNameAlert.show()

我们知道 JavaScript 并不强面向对象,也没有提供抽象类(至少目前没有提供),但是可以模拟抽象类。用对 new.target 来判断 new 的类,在父类方法中 throw new Error(),如果子类中没有实现这个方法就会抛错,这样来模拟抽象类:

/* 抽象类,ES6 class 方式 */
class AbstractClass1 {
    constructor() {
        if (new.target === AbstractClass1) {
            throw new Error('抽象类不能直接实例化!')
        }
    }
    /* 抽象方法 */
    operate() { throw new Error('抽象方法不能调用!') }
}
​
/* 抽象类,ES5 构造函数方式 */
var AbstractClass2 = function () {
    if (new.target === AbstractClass2) {
        throw new Error('抽象类不能直接实例化!')
    }
}
/* 抽象方法,使用原型方式添加 */
AbstractClass2.prototype.operate = function(){ throw new Error('抽象方法不能调用!') }

抽象产品类将产品的结构抽象出来,访问者不需要知道产品的具体实现,只需要面向产品的结构编程即可,从产品的具体实现中解耦。

但是扩展新类簇的产品类比较困难,因为需要创建新的抽象产品类,并且还要修改工厂类,违反开闭原则。同时也带来了系统复杂度,增加了新的类,和新的继承关系。

ES6 改写

ES6中给我们提供了class新语法,虽然class本质上是一颗语法糖,并也没有改变JavaScript是使用原型继承的语言,但是确实让对象的创建和继承的过程变得更加的清晰和易读。下面我们使用ES6的新语法来重写上面的例子。

简单工厂

使用ES6重写简单工厂模式时,我们不再使用构造函数创建对象,而是使用class的新语法,并使用static关键字将简单工厂封装到AlertFactory类的静态方法中:

class AlertFactory {
    constructor() {}
​
    static getInstance(type) {
        // 检测用户名
        var checkNameAlert = function (context) {
            this.text = context
        }
        checkNameAlert.prototype.show = function () {
            console.log(111);
            // ...
        }
​
        // 登陆失败提示框
        var loginConfirm = function (text) {
            this.context = text
        }
        loginConfirm.prototype.show = function () {
            // 登陆失败提示
        }
​
        // 欢迎回来提示框
        var loginBack = function (text) {
            this.context = text
        }
        loginBack.prototype.show = function () {
            // ...
        }
​
        switch (type) {
            case "checkName":
                return new checkNameAlert("用户名输入错误")
                break;
            case "fileConfirm":
                return new loginConfirm("您的用户明不存在,请重新输入")
                break;
            case "comeBack":
                return new loginBack("欢迎回来!")
                break;
            default:
                throw new Error(type + "类暂未实现")
        }
    }
}
​
const checkNameAlert = AlertFactory.getInstance( "checkName" )
checkNameAlert.show()

工厂方法

在上文中我们提到,工厂方法模式的本意是将实际创建对象的工作推迟到子类中,这样核心类就变成了抽象类。但是JavaScript的abstract是一个保留字,并没有提供抽象类,所以之前我们只是借鉴了工厂方法模式的核心思想。

虽然ES6也没有实现abstract,但是我们可以使用new.target来模拟出抽象类。new.target指向直接被new执行的构造函数,我们对new.target进行判断,如果指向了该类则抛出错误来使得该类成为抽象类。下面我们来改造代码。

class Alert {
    constructor(type, context) {
        this.text = context
        if (new.target === Alert) {
            throw new Error("抽象类不能实例化")
        }
    }
}
​
class AlertFactory extends Alert {
    constructor(type, context) {
        super(type, context)
        return this[type](context)
    }
​
    checkNameAlert() {
        const _that = this
        return {
            show: function () {
                // ...
            }
        }
    }
​
    loginConfirm() {
        const _that = this
        return {
            show: function () {
                // ...
            }
        }
    }
​
    loginBack() {
        const _that = this
        return {
            show: function () {
                // ...
            }
        }
    }
}
​
const checkNameAlert = new AlertFactory("checkNameAlert", "用户名输入错误")
checkNameAlert.show()

抽象工厂

抽象工厂模式并不直接生成实例, 而是用于对产品类簇的创建。我们同样使用new.target语法来模拟抽象类,并通过继承的方式创建出UserOfWechat, UserOfQq, UserOfWeibo这一系列子类类簇。使用getAbstractUserFactor来返回指定的类簇。

class User {
    constructor(type) {
        if (new.target === User) {
            throw new Error('抽象类不能实例化!')
        }
        this.type = type;
    }
}
​
class UserOfWechat extends User {
    constructor(name) {
        super('wechat');
        this.name = name;
        this.viewPage = ['首页', '通讯录', '发现页']
    }
}
​
class UserOfQq extends User {
    constructor(name) {
        super('qq');
        this.name = name;
        this.viewPage = ['首页', '通讯录', '发现页']
    }
}
​
class UserOfWeibo extends User {
    constructor(name) {
        super('weibo');
        this.name = name;
        this.viewPage = ['首页', '通讯录', '发现页']
    }
}
​
function getAbstractUserFactory(type) {
    switch (type) {
        case 'wechat':
            return UserOfWechat;
            break;
        case 'qq':
            return UserOfQq;
            break;
        case 'weibo':
            return UserOfWeibo;
            break;
        default:
            throw new Error('参数错误, 可选参数:superAdmin、admin、user')
    }
}
​
let WechatUserClass = getAbstractUserFactory('wechat');
let QqUserClass = getAbstractUserFactory('qq');
let WeiboUserClass = getAbstractUserFactory('weibo');
​
let wechatUser = new WechatUserClass('微信小李');
let qqUser = new QqUserClass('QQ小李');
let weiboUser = new WeiboUserClass('微博小李');

总结

工厂模式属于创建型的设计模式。简单工厂模式又叫静态工厂方法,用来创建某一种产品对象的实例,用来创建单一对象。工厂方法模式是将创建实例推迟到子类中进行。抽象工厂模式是对类的工厂抽象用来创建产品类簇,不负责创建某一类产品的实例。

在实际的业务中,需要根据实际的业务复杂度来选择合适的模式。对于非大型的前端应用来说,灵活使用简单工厂其实就能解决大部分问题。