工厂模式

设计模式——工厂模式
从ES6重新认识JavaScript设计模式

工厂模式主要是为了创建对象实例或者类簇(抽象工厂), 关心的是最终产出(创建)的对象, 而不关心创建的过程。下面我们来看一下之前提到的工厂模式的三种实现方法: 简单工厂模式、工厂方法模式、抽象工厂模式。

简单的工厂模式

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

在实际的项目中,我们常常需要根据用户的权限来渲染不同的页面,高级权限的用户所拥有的页面有些是无法被低级权限的用户所查看。所以我们可以在不同权限等级用户的构造函数中,保存该用户能够看到的页面。在根据权限实例化用户。代码如下:

let UserFactory = function (role) {
    function SuperAdmin() {
      this.name = "超级管理员"
      this.viewPage = ['首页', '通讯录', '发现页', '应用数据', '权限管理']
    }

    function Admin() {
      this.name = "管理员"
      this.viewPage = ['首页', '通讯录', '发现页', '应用数据']
    }

    function NormalUser() {
      this.name = '普通用户'
      this.viewPage = ['首页', '通讯录', '发现页']
    }

    switch (role) {
      case 'superAdmin':
        return new SuperAdmin()
        break
      case 'admin':
        return new Admin()
        break
      case 'user':
        return new NormalUser()
        break
      default:
        throw new Error('参数错误, 可选参数:superAdmin、admin、user')
    }
  }

  //调用
  let superAdmin = UserFactory('superAdmin')
  console.log(superAdmin.name)
  console.log(superAdmin)

UserFactory就是一个简单工厂,在该函数中有3个构造函数分别对应不同的权限的用户。当我们调用工厂函数时,只需要传递superAdmin, admin, user这三个可选参数中的一个获取对应的实例对象。你也许发现,我们的这三类用户的构造函数内部很相识,我们还可以对其进行优化。

let UserFactory = function (role) {
    function User(opt) {
      this.name = opt.name
      this.viewPage = opt.viewPage
    }

    switch (role) {
      case 'superAdmin':
        return new User({name: '超级管理员', viewPage: ['首页', '通讯录', '发现页', '应用数据', '权限管理']})
        break
      case 'admin':
        return new User({name: '管理员', viewPage: ['首页', '通讯录', '发现页', '应用数据']})
        break
      case 'user':
        return new User({name: '普通用户', viewPage: ['首页', '通讯录', '发现页']})
        break
      default:
        throw new Error('参数错误, 可选参数:superAdmin、admin、user')
    }
  }

  //调用
  let superAdmin = UserFactory('superAdmin')
  console.log(superAdmin.name)
  console.log(superAdmin)

简单工厂的优点在于,你只需要一个正确的参数,就可以获取到你所需要的对象,而无需知道其创建的具体细节。但是在函数内包含了所有对象的创建逻辑(构造函数)和判断逻辑的代码,每增加新的构造函数还需要修改判断逻辑代码。

当我们的对象不是上面的3个而是30个或更多时,这个函数会成为一个庞大的超级函数,便得难以维护。所以,简单工厂只能作用于创建的对象数量较少,对象的创建逻辑不复杂时使用。

工厂方法模式

工厂方法模式的本意是将实际创建对象的工作推迟到子类中,这样核心类就变成了抽象类。但是在JavaScript中很难像传统面向对象那样去实现创建抽象类。所以在JavaScript中我们只需要参考它的核心思想即可。我们可以将工厂方法看作是一个实例化对象的工厂类。

<script>
  //安全模式创建的工厂方法函数
  let UserFactory = function (role) {
    if (this instanceof UserFactory) {
      var s = new this[role]()
      return s
    } else {
      return new UserFactory(role)
    }
  }

  //工厂方法函数的原型中设置所有对象的构造函数
  UserFactory.prototype = {
    SuperAdmin: function () {
      this.name = "超级管理员"
      this.viewPage = ['首页', '通讯录', '发现页', '应用数据', '权限管理']
    },
    Admin: function () {
      this.name = "管理员"
      this.viewPage = ['首页', '通讯录', '发现页', '应用数据']
    },
    NormalUser: function () {
      this.name = '普通用户'
      this.viewPage = ['首页', '通讯录', '发现页']
    }
  }

  //调用
  let superAdmin = UserFactory('SuperAdmin')
  console.log(superAdmin)

上面的这段代码就很好的解决了每添加一个构造函数就需要修改两处代码的问题,如果我们需要添加新的角色,只需要在UserFactory.prototype中添加。例如,我们需要添加一个VipUser:

UserFactory.prototype = {
  //....
  VipUser: function() {
    this.name = '付费用户'
    this.viewPage = ['首页', '通讯录', '发现页', 'VIP页']
  }
}

//调用
let vipUser = UserFactory('VipUser')
安全模式的理解
let UserFactory = function(role) {
  if(this instanceof UserFactory) {
    var s = new this[role]()
    return s
  } else {
    return new UserFactory(role)
  }
}

因为我们将SuperAdmin、Admin、NormalUser等构造函数保存到了UserFactory.prototype中,也就意味着我们必须实例化UserFactory函数才能够进行以上对象的实例化。如下面代码所示:

let UserFactory = function() {}

UserFactory.prototype = {
 //...
}

//调用
let factory = new UserFactory()
let superAdmin = new factory.SuperAdmin()

在上面的调用函数的过程中, 一旦我们在任何阶段忘记使用new, 那么就无法正确获取到superAdmin这个对象。但是一旦使用安全模式去进行实例化,就能很好解决上面的问题。

抽象工厂模式

抽象工厂模式一般不用来创建具体对象, 抽象类中定义的方法只是显性地定义一些功能, 但没有具体的实现, 而一个对象需要具有一套完整的功能, 所以用抽象类创建的对象也是抽象的而非真实对象. 因此一般用它作为父类来创建子类。

抽象工厂其实是一个实现子类继承父类的方法, 在这个方法中需要通过传递子类以及要继承父类(抽象类)的名称,并且在抽象工厂方法中又增加了一次对抽象存在性的一次判断, 如果存在, 则将子类继承父类的方法。 然后子类通过寄生式继承。

继承父类的原型中需要注意一点是, 在对过渡类的原型继承时, 不是继承父类的原型, 而是通过new关键字复制父类的一个实例, 这么做事因为过渡类不应仅仅继承父类的原型方法, 还要继承父类的对象属性, 所以要通过new关键字将父类的构造函数执行一遍来复制构造函数中的属性和方法。

抽象工厂添加抽象类比较特殊, 因为抽象工厂是个方法不需要实例化对象, 故只需要一份, 因此直接为抽象工厂添加类的属性即可。

上面例子中的superAdmin,admin,user三种用户角色,其中user可能是使用不同的社交媒体账户进行注册的,例如:wechat,qq,weibo。那么这三类社交媒体账户就是对应的类簇。在抽象工厂中,类簇一般用父类定义,并在父类中定义一些抽象方法,再通过抽象工厂让子类继承父类。所以,抽象工厂其实是实现子类继承父类的方法。

上面提到的抽象方法是指声明但不能使用的方法。在其他传统面向对象的语言中常用abstract进行声明,但是在JavaScript中,abstract是属于保留字,但是我们可以通过在类的方法中抛出错误来模拟抽象类。

let WechatUser = function() {}
WechatUser.prototype = {
  getName: function() {
    return new Error('抽象方法不能调用')
  }
}

上述代码中的getPrice就是抽象方法,我们定义它但是却没有去实现。如果子类继承WechatUser但是并没有去重写getName,那么子类的实例化对象就会调用父类的getName方法并抛出错误提示。

下面我们分别来实现账号管理的抽象工厂方法:

let AccountAbstractFactory = function(subType, superType) {
  //判断抽象工厂中是否有该抽象类
  if(typeof AccountAbstractFactory[superType] === 'function') {
    //缓存类
    function F() {}
    //继承父类属性和方法
    F.prototype = new AccountAbstractFactory[superType] ()
    //将子类的constructor指向子类
    subType.constructor = subType
    //子类原型继承父类
    subType.prototype = new F()

  } else {
    throw new Error('抽象类不存在!')
  }
}

//微信用户抽象类
AccountAbstractFactory.WechatUser = function() {
  this.type = 'wechat'
}
AccountAbstractFactory.WechatUser.prototype = {
  getName: function() {
    return new Error('抽象方法不能调用');
  }
}

//qq用户抽象类
AccountAbstractFactory.QqUser = function() {
  this.type = 'qq'
}
AccountAbstractFactory.QqUser.prototype = {
  getName: function() {
    return new Error('抽象方法不能调用')
  }
}

//新浪微博用户抽象类
AccountAbstractFactory.WeiboUser = function() {
  this.type = 'weibo'
}
AccountAbstractFactory.WeiboUser.prototype = {
  getName: function() {
    return new Error('抽象方法不能调用')
  }
}

下面我们来定义普通用户的子类:

//普通微信用户子类
function UserOfWechat(name) {
  this.name = name
  this.viewPage = ['首页', '通讯录', '发现页']
}
//抽象工厂实现WechatUser类的继承
AccountAbstractFactory(UserOfWechat, 'WechatUser')
//子类中重写抽象方法
UserOfWechat.prototype.getName = function() {
  return this.name
}

//普通qq用户子类
function UserOfQq(name) {
  this.name = name
  this.viewPage = ['首页', '通讯录', '发现页']
}
//抽象工厂实现QqUser类的继承
AccountAbstractFactory(UserOfQq, 'QqUser')
//子类中重写抽象方法
UserOfQq.prototype.getName = function() {
  return this.name
}

//普通微博用户子类
function UserOfWeibo(name) {
  this.name = name
  this.viewPage = ['首页', '通讯录', '发现页']
}
//抽象工厂实现WeiboUser类的继承
AccountAbstractFactory(UserOfWeibo, 'WeiboUser')
//子类中重写抽象方法
UserOfWeibo.prototype.getName = function() {
  return this.name
}

上述代码我们分别定义了UserOfWechat,UserOfQq,UserOfWeibo三种类。这三个类作为子类通过抽象工厂方法实现继承。特别需要注意的是,调用抽象工厂方法后不要忘记重写抽象方法,否则在子类的实例中调用抽象方法会报错。

我们来分别对这三种类进行实例化,检测抽象工厂方法是实现了类簇的管理。

//实例化微信用户
let wechatUserA = new UserOfWechat('微信小李')
console.log(wechatUserA.getName(), wechatUserA.type) //微信小李 wechat
let wechatUserB = new UserOfWechat('微信小王')
console.log(wechatUserB.getName(), wechatUserB.type) //微信小王 wechat

//实例化qq用户
let qqUserA = new UserOfQq('QQ小李')
console.log(qqUserA.getName(), qqUserA.type) //QQ小李 qq
let qqUserB = new UserOfQq('QQ小王')
console.log(qqUserB.getName(), qqUserB.type) //QQ小王 qq

//实例化微博用户
let weiboUserA =new UserOfWeibo('微博小李')
console.log(weiboUserA.getName(), weiboUserA.type) //微博小李 weibo
let weiboUserB =new UserOfWeibo('微博小王')
console.log(weiboUserB.getName(), weiboUserB.type) //微博小王 weibo

从打印结果上看,AccountAbstractFactory这个抽象工厂很好的实现了它的作用,将不同用户账户按照社交媒体这一个类簇进行了分类。这就是抽象工厂的作用,它不直接创建实例,而是通过类的继承进行类簇的管理。

ES6重写抽象工厂模式

抽象工厂模式并不直接生成实例, 而是用于对产品类簇的创建。我们同样使用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('微博小李')
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351