Factory 工厂模式

今天要来聊聊 Creational Patten 当中的工厂模式。

当我们透过类别建立出实例的时候,其实感觉就像是一个工厂生产出了产品。而同一个工厂 (类别),可以生产出无限多个同样者产品 (实例)。

回到我们先前的例子,这里的 BaseballPlayerTennisPlayer 类别继承了 Athlete,并能够各自产出各自的实例,并且针对 hit 方法有不同的实作方式。

abstract class Athlete {
  constructor() {}

  abstract hit(): void;
}

class BaseballPlayer extends Athlete {
  hit() {
    console.log('Baseball player can hit baseball')
  }
}

class TennisPlayer extends Athlete {
  hit() {
    console.log('Tennis player can hit tennis')
  }
}

const jeter = new BaseballPlayer()
const federer = new TennisPlayer()

jeter.hit()      // Baseball player can hit baseball
federer.hit()    // Tennis player can hit tennis

但如果我们希望能够有个 AthleteFactory,像是中央生产工厂,不管我想要 baseball player 还是 tennis player,找它就可以生产了

简单工厂模式

於是,这里就出现了一个简单的解法:建立一个静态类别AthleteFactory,并且有一个静态方法 trainAthlete,可以根据使用者的输入,来判断要提供什麽样的产品

class AthleteFactory {
  static trainAthlete(category: string): Athlete {
    switch (category) {
      case 'baseball':
        return new BaseballPlayer()
      case 'tennis':
        return new TennisPlayer()
      default:
        return null
    }
  }
}

所以如果我想要一位 baseball player,就在呼叫方法的时候,输入 'baseball'。想要一位 tennis player 的时候,就输入 'tennis',就能得到期待中的结果。

const ohtani = AthleteFactory.trainAthlete('baseball')
const nadal = AthleteFactory.trainAthlete('tennis')

ohtani.hit()   // Baseball player can hit baseball
nadal.hit()    // Tennis player can hit tennis

另一方面,如果想要持续增加不同的产品,只要在 AthleteFactory 当中持续扩充 switch 区块就行!

简单工厂模式的优缺点

使用者能够快速从工厂取出需要的实例来使用,不需要自己去找目标类别来建立实例。另一方面,使用者也不需要管这些实例是怎麽被建立的,只要负责使用就行。

简单工厂的实作方式简单好懂,能够快速上手(平常可能不知不觉就采用了简单工厂模式)。

不过缺点就是,如果要新增一个新的商品,就必须要回去修改 AthleteFactory 当中的程序码,有可能会影响到旧有的程序码。另一方面,简单工厂集中了所有建立实例的逻辑与责任,一旦不能正常运作,那麽所有产品都会受到影响。

工厂模式

所以,与其让某个实际的工厂拥有所有生产产品的逻辑,不如就让每个产品有各自的工厂,但却有遵守相同的生产方式。

所以在工厂模式中,没有一个中央生产的工厂,只有一个指导其他工厂如何运作的抽象介面,像是下面的 AthleteFactory

interface AthleteFactory {
  trainAthlete(): Athlete;
}

接着,我们分别位 baseball player 和 tennis player 建立 BaseballPlayerFactoryTennisPlayerFactory 工厂,并且执行 AthleteFactory 介面

class BaseballPlayerFactory implements AthleteFactory {
  constructor() {}

  trainAthlete() {
    return new BaseballPlayer()
  }
}

class TennisPlayerFactory implements AthleteFactory {
  constructor() {}

  trainAthlete() {
    return new TennisPlayer()
  }
}

最後,如果我们希望生产 baseball player,就建立一个 baseballPlayerFactory;如果我们希望生产 tennis player,就建立一个 tennisPlayerFactory

const baseballPlayerFactory = new BaseballPlayerFactory()
const tennisPlayerFactory = new TennisPlayerFactory()

接着,两者可以用同样的方式,建立需要的实例,得到预期中的结果

const ohtani = baseballPlayerFactory.trainAthlete()
const nadal = tennisPlayerFactory.trainAthlete()

ohtani.hit()    // Baseball player can hit baseball
nadal.hit()     // Tennis player can hit tennis

解决什麽问题?

有些时候我们因应外在环境的变动,需要产出类似的、但是又截然不同的实例(譬如棒球选手和网球选手),但又要避免像简单工厂那样,因集中生产所造成的维护和扩充问题,最後这里我们透过「抽象」的方式来解决问题。

首先是建立一个介面(或是类别也可以)来定义各个工厂的实作要求 (譬如 trainAthlete 方法),但是不定义详细的实作方式。

接着,让不同的产品建立相对应不同的工厂,并各自执行 trainAthlete 方法。譬如 BaseballPlayerFactory 的做法就是呼叫 new BaseballPlayer()

最後,使用者根据需求,呼叫需要的工厂来生产实例。

优点与缺点

相对於简单工厂模式,工厂模式的好处是满足了「单一功能原则」,让 AthleteFactory 只管要有什麽方法,而不用管实作细节;也满足了「开放封闭原则」,未来有任何新的需求出现,只要按照介面(或类别)设计新的工程即可,大大提升了维护性和扩充性。

不过缺点就是,每当有新需求出现,我们就需要建立一个新的工厂 (e.g., xxxFactory),以及负责生产实例的类别,两者会成双成对出现。


<<:  Day 19 - WooCommerce: 初始化付款外挂

>>:  19 首页与开始游戏按钮

Day1 前言

报名这次的铁人赛事十分紧张,对於我自己资讯方面的技术并不是那麽熟练,想利用铁人赛来督促自己学习新的...

Android x Kotlin : 简易实作第一堂-滚动式选单NumberPicker

简介 滚动式选单依然是常用基本元件之一,使用起来也非常容易。直接上图。 选项里面可以放Int或Str...

可以录制Mac 内部/外部声音的 5 种荧幕录制方法

我想用录制我的Mac电脑的荧幕和声音,我该怎麽办? 其实,如果你使用合适的应用软件,你可以简单地将M...

[想试试看JavaScript ] 阵列

阵列 当我会想储存比较复杂的资料的时後就会使用物件 除了物件,当这些资料有顺序的话,我也可以使用阵列...

.Net Core Web Api_笔记08_HTTP资源操作模式PATCH

在HTTP请求中 PUT 跟 PATCH 都代表更新 然後他们之间比较主要的差异在於 PUT 用在更...