三言兩語說透設計模式的藝術-簡單工廠模式
一、寫在前面
工廠模式是最常見的一種創(chuàng)建型設計模式,通常說的工廠模式指的是工廠方法模式,是使用頻率最高的工廠模式。簡單工廠模式又稱為靜態(tài)工廠方法模式,不屬于GoF 23種設計模式,它屬于類創(chuàng)建型模式,是其它工廠模式的入門。
二、游戲工廠的設計
ONEGAME游戲公司計劃開發(fā)一條游戲生產(chǎn)線,該生產(chǎn)線可以向玩家提供不同類型的游戲,例如:RGP游戲、MMORGP游戲、MOBA游戲以及FPS游戲等。為了提供這些游戲,游戲公司需要創(chuàng)建一個游戲工廠,來創(chuàng)建這些游戲的實例。
ONEGAME游戲公司提出了初始設計方案,就是將所有類型的游戲的實現(xiàn)代碼封裝到一個Game類中,然后通過Game工廠來創(chuàng)建實例。實現(xiàn)代碼如下:
class Game{
private type: string;//游戲類別
constructor(type: string, data: any) {
this.type = type;
if(type.toLocaleLowerCase() === 'fps'){
// 初始化FPS游戲
}else if(type.toLocaleLowerCase() === 'rpg'){
// 初始化RPG游戲
}else if(type.toLocaleLowerCase() === 'moba'){
// 初始化MOBA游戲
}
}
play(){
if(this.type.toLocaleLowerCase() === 'fps'){
// 玩FPS游戲
}else if(this.type.toLocaleLowerCase() === 'rpg'){
// 玩RPG游戲
}else if(this.type.toLocaleLowerCase() === 'moba'){
// 玩MOBA游戲
}
}
}
上面的代碼實現(xiàn)了游戲的創(chuàng)建和玩游戲的功能,但是這樣的設計存在以下問題:
- Game類的代碼會越來越臃腫,而且違反了單一職責原則,不利于代碼的維護和擴展。
- 在需要對Game類進行擴展新游戲的時候,需要對源碼進行修改,這就違反了開閉原則。
- 用戶只能通過new關鍵字來直接創(chuàng)建實例,而不是通過Game工廠來創(chuàng)建實例,耦合性高,對象創(chuàng)建和使用無法分離。
為了解決上面的問題,我們可以對Game類進行重構,將其拆分成多個游戲類,每個游戲類只負責自己的初始化和玩游戲的功能,這樣就可以避免代碼臃腫和違反單一職責原則的問題。但是這樣做還是無法解決對象創(chuàng)建和使用無法分離的問題,我們可以通過簡單工廠模式來解決這個問題。
三、簡單工廠模式
簡單工廠的設計思想就是,將創(chuàng)建不同對象的相關的代碼封裝到不同的類中,即具體產(chǎn)品類,這樣就可以避免代碼的臃腫和違反單一職責原則的問題。將它們的公共代碼抽象到和封裝到一個抽象產(chǎn)品類中,每個具體類都是抽象產(chǎn)品類的子類。然后通過一個工廠類來創(chuàng)建這些具體產(chǎn)品類的實例,通過傳入的參數(shù)不同創(chuàng)建對應的具體產(chǎn)品對象。
什么是簡單工廠模式
簡單工廠模式:定義一個工廠類,通過傳入?yún)?shù)來創(chuàng)建不同的具體產(chǎn)品類的實例,被創(chuàng)建的實例都具有共同的父類。
簡單工廠模式結構包括三個角色:
- 工廠類:創(chuàng)建具體產(chǎn)品類的實例的工廠類,負責實現(xiàn)創(chuàng)建具體產(chǎn)品實例的內(nèi)部邏輯。工廠類可以被外界直接調(diào)用,創(chuàng)建所需的產(chǎn)品對象。
- 抽象產(chǎn)品類:創(chuàng)建具體產(chǎn)品類的實例的抽象產(chǎn)品類,它是需要工廠類所創(chuàng)建的所有具體產(chǎn)品類的公共父類,封裝了各種產(chǎn)品對象的公有方法。
- 具體產(chǎn)品類:具體產(chǎn)品類的實例,是簡單工廠模式的創(chuàng)建目標,它繼承抽象產(chǎn)品類的公共父類,所有被創(chuàng)建的對象都是這個產(chǎn)品對應的具體產(chǎn)品類的實例。
使用簡單工廠模式優(yōu)化上面的代碼,以實現(xiàn)一個游戲工廠為為例,實現(xiàn)可以生產(chǎn)不同類型的游戲為目的。首先定義一個抽象產(chǎn)品類Game,然后定義具體產(chǎn)品類FPSGame、RPGGame、MOBAGame,最后定義一個工廠類GameFactory,通過傳入不同的參數(shù)來創(chuàng)建不同的游戲?qū)嵗?/p>
// 游戲接口:抽象產(chǎn)品類
interface Game {
play(): void;
}
// 各種游戲的具體實現(xiàn)類:具體產(chǎn)品類
// FPS游戲
class FPSGame implements Game{
play() {
console.log('FPS游戲');
}
}
// RPG游戲
class RPGGame implements Game {
play() {
console.log('RPG游戲');
}
}
// MOBA游戲
class MOBAGame implements Game {
play() {
console.log('MOBA游戲');
}
}
// 游戲工廠:創(chuàng)建具體產(chǎn)品類的實例的工廠類
class GameFactory {
static createGame(type: string): Game {
this.type = type;
switch (this.type) {
case 'RPG':
return new RPGGame();
case 'MOBA':
return new MOBAGame();
case 'FPS':
return new FPSGame();
default:
throw new Error('Unknown game type');
}
}
}
用戶實際使用創(chuàng)建對應的游戲:
// 獲取RGP游戲
const rgpGame = GameFactory.createGame('RPG');
rgpGame.play();
// 獲取MOBA游戲
const mobaGame = GameFactory.createGame('MOBA');
mobaGame.play();
在實際使用中,客戶端代碼只需要傳入類型參數(shù),就可以獲取得到對應的游戲?qū)ο?,而不需要關系對象的具體實現(xiàn)。這就符合簡單工廠模式的設計思想。
簡單工廠模式的優(yōu)點
- 封裝實例化過程:簡單工廠模式在一個工廠類中封裝了實例化對象的過程,創(chuàng)建對象的細節(jié)被隱藏在工廠類中,外界無需關心對象是如何被創(chuàng)建出來的。
- 定義統(tǒng)一接口:工廠類所創(chuàng)建的對象都實現(xiàn)了一個共同的接口,對外提供一致的使用方式。
- 通過參數(shù)創(chuàng)建不同實例:客戶端只需要傳入不同的參數(shù)給工廠類,就可以獲取不同的對象實例,而不需要知道具體類名。
- 實現(xiàn)解耦:簡單工廠模式實現(xiàn)了客戶端與產(chǎn)品類的解耦,客戶端不需要知道所創(chuàng)建對象的具體實現(xiàn),只需要知道參數(shù)即可。
- 優(yōu)化開閉原則:當需要新增一個產(chǎn)品類時,只需要實現(xiàn)統(tǒng)一接口,然后在工廠類中添加對應類型即可,無需修改客戶端代碼。
- 高內(nèi)聚,低耦合:每個產(chǎn)品類只關心自己的功能實現(xiàn),不關心實例化過程;客戶端不依賴具體類。使代碼內(nèi)聚性高、耦合度低。
四、簡單工廠模式的優(yōu)化
使用泛型優(yōu)化工廠類
在上面的實現(xiàn)中,工廠類的創(chuàng)建方法返回的是Game接口類型,缺點是客戶端得到的對象類型信息不全,對此可以使用泛型來改進:
// 游戲接口:抽象產(chǎn)品類
interface Game {
play(): void;
}
class FPSGame implements Game {
//...
}
class RPGGame implements Game {
//...
}
class MOBAGame implements Game {
//...
}
class GameFactory{
static createGame<T extends Game>(type: string): T{
//...
}
}
這樣在客戶端代碼得到的對象類型信息更加準確。
const rgpGame = GameFactory.createGame<RPGGame>('RPG');
// rgpGame的類型是RPGGame,而不是Game
使用泛型優(yōu)化工廠類的優(yōu)化
上面的代碼中,所有的產(chǎn)品類都需要實現(xiàn) Game 接口,這樣會存在代碼重復的問題。我們可以引入一個泛型接口 IGame來改進:
interface IGame<T> {
play(): void;
info(): T;
}
class RPGGame implements IGame<string> {
play() {
// ...
}
info() {
return 'RPG';
}
}
class MOBAGame implements IGame<string> {
play() {
// ...
}
info() {
return 'MOBA';
}
}
class FPSGame implements IGame<string> {
// ...
}
這樣每個產(chǎn)品類就可以定制自己的 info 方法返回值類型了。
使用抽象類改進產(chǎn)品類
上面的代碼還存在問題:所有產(chǎn)品類都需要實現(xiàn) play 方法,這會導致重復代碼。我們可以使用抽象類來解決這個問題:
abstract class GameBase {
play() {
// 默認游戲邏輯
}
}
class RPGGame extends GameBase implements IGame<string> {
info() {
return 'RPG';
}
}
class MOBAGame extends GameBase implements IGame<string> {
// ...
}
class FPSGame extends GameBase implements IGame<string> {
// ...
}
這樣產(chǎn)品類就不需要重復實現(xiàn) play 方法了,只需要繼承 GameBase 并實現(xiàn) info 方法即可。
使用配置文件創(chuàng)建工廠類
上面的代碼中,工廠類的創(chuàng)建方法需要傳入一個類型參數(shù),這樣會導致客戶端代碼需要知道具體的類型參數(shù),這樣就會破壞簡單工廠模式的封裝性。我們可以使用配置文件來解決這個問題:
class GameConfig {
static gameTypes = {
'RPG': RPG,
'MOBA': MOBA,
'FPS': FPS
}
}
工廠類讀取配置創(chuàng)建對象:
class GameFactory {
static createGame(type: string) {
const Constructor = GameConfig.gameTypes[type];
if (!Constructor) {
throw new Error('Unknown type');
}
return new Constructor();
}
}
這樣當需要新增游戲類型時,只需要在配置類中添加新的類型和類即可,工廠類的代碼無需修改。
利用依賴注入實現(xiàn)解耦
我們還可以通過依賴注入進一步解耦:
@injectable()
class GameFactory {
constructor(
@inject(GameConfig.gameTypes.RPG) private rpgGame: Game,
@inject(GameConfig.gameTypes.MOBA) private mobaGame: Game,
@inject(GameConfig.gameTypes.FPS) private fpsGame: Game
) {}
createGame(type: string) {
switch(type) {
// ...
}
}
}
這樣工廠類不再負責創(chuàng)建對象,而是通過注入的方式獲取對象實例,大大提升了靈活性。
五、完整代碼示例
下面是使用 TypeScript 深入解析簡單工廠模式的示例,通過工廠類和產(chǎn)品類的抽象與解耦,可以實現(xiàn)創(chuàng)建對象邏輯的集中和優(yōu)化,提高代碼的靈活性和擴展性。TypeScript 通過接口、泛型和抽象類等特性增強了簡單工廠模式的實現(xiàn)。掌握設計模式對編寫優(yōu)雅可擴展的 TypeScript 代碼很有幫助。
// 游戲接口
interface Game {
play(): void;
}
// 泛型游戲接口
interface IGame<T> {
play(): void;
info(): T;
}
// 抽象游戲類
abstract class GameBase {
play() {
console.log('Playing game...');
}
}
// RPG游戲類
class RPG extends GameBase implements IGame<string> {
info() {
return 'RPG';
}
}
// MMORPG游戲類
class MMORPG extends GameBase implements IGame<string> {
info() {
return 'MMORPG';
}
}
// FPS游戲類
class FPS extends GameBase implements IGame<string> {
info() {
return 'FPS';
}
}
// 配置類
class GameConfig {
static gameTypes = {
'RPG': RPG,
'MMORPG': MMORPG,
'FPS': FPS
}
}
// 工廠類
class GameFactory {
static createGame(type: string) {
const Constructor = GameConfig.gameTypes[type];
if (!Constructor) {
throw new Error('Unknown type');
}
return new Constructor();
}
}
// 客戶端
const rpgGame = GameFactory.createGame<RPG>('RPG');
rpgGame.play();
console.log(rpgGame.info());
const fpsGame = GameFactory.createGame<FPS>('FPS');
fpsGame.play();
console.log(fpsGame.info());
六、簡單工廠模式和單例模式的區(qū)別
1. 用途不同
簡單工廠模式是一種創(chuàng)建對象的設計模式,它通過工廠類來創(chuàng)建產(chǎn)品對象,主要目的是將對象創(chuàng)建的過程封裝起來,便于管理和維護。
而單例模式是一種確保某個類只有一個實例的設計模式,它的目的是在整個軟件系統(tǒng)中,對某個類只創(chuàng)建一個對象實例,避免浪費資源。
2. 實現(xiàn)方式不同
簡單工廠模式是通過工廠類的靜態(tài)方法創(chuàng)建對象實例,可以創(chuàng)建多個實例。
單例模式是在類中定義一個靜態(tài)變量保存單例實例,并通過一個靜態(tài)方法來獲取這個實例,確保只創(chuàng)建一個實例。
3. 使用場景不同
簡單工廠模式用于創(chuàng)建同一類產(chǎn)品的不同對象實例,客戶端無需知道具體產(chǎn)品類的類名。
單例模式用于創(chuàng)建對唯一實例有需求的對象,如線程池、緩存、日志對象等。
小結一下,簡單工廠模式關注創(chuàng)建不同實例,單例模式關注如何只創(chuàng)建一個實例。二者解決的問題和應用場景不同,但可以結合使用,工廠類可以返回單例對象。
七、總結
通過上面的示例,我們使用 TypeScript 從多個方面對簡單工廠模式進行了深入解析,包括:
- 使用泛型優(yōu)化工廠方法的返回類型
- 使用泛型接口減少產(chǎn)品類代碼重復
- 使用抽象類提取產(chǎn)品類公共代碼
- 使用配置文件動態(tài)創(chuàng)建產(chǎn)品類實例
簡單工廠模式的優(yōu)點:
- 將對象創(chuàng)建的過程封裝在工廠類中,實現(xiàn)了解耦
- 客戶端無須知道所創(chuàng)建具體產(chǎn)品類的類名
- 可以方便地擴展新的具體產(chǎn)品類
簡單工廠模式的缺點:
- 工廠類職責較重,所有產(chǎn)品創(chuàng)建邏輯都集中在工廠類
- 如果產(chǎn)品類型較多,工廠類會變得很復雜
- 擴展新的產(chǎn)品困難,需要修改工廠類代碼
簡單工廠模式通過工廠類和產(chǎn)品類的解耦,可以實現(xiàn)創(chuàng)建對象邏輯的集中化和優(yōu)化,是非常常用和靈活的一種設計模式。TypeScript 通過接口、泛型和抽象類等特性,可以更優(yōu)雅地實現(xiàn)簡單工廠模式,提高代碼的復用性和擴展性。