設計模式大冒險第三關:工廠模式,封裝和解耦你的程式碼

dreamapplehappy發表於2020-11-13

image

這篇文章是關於設計模式系列的第三篇文章,這一系列的每一篇文章都會通過生活中的小例子以及一些簡單的比喻讓大家明白每一個設計模式要解決的是什麼問題,然後通過什麼方式解決的。希望大家在看過每篇文章之後都能夠理解文章中講解的設計模式,然後有所收穫。話不多說,讓我們開始今天的冒險吧。

工廠模式的第一印象

對於初次聽說這個設計模式的同學來說,你們的第一印象是什麼呢?既然是工廠模式,那麼肯定跟工廠的一些功能或者行為有關係。那麼工廠都有哪些功能和行為呢?首先工廠收集原始材料,然後將原始的材料進行加工,處理,設計之後就變成了一個完整的產品或者部件。

這個過程對於產品的銷售店,或者使用者來說是不可見的。對於商家來說如果你想賣這個產品,你只需要去跟廠家溝通買一批這樣的產品就行了。那對於使用者來說,你想使用這個產品,只需要到賣這個產品的店裡把它買回來就好了。

所以根據上面的推論,類比到程式碼中我們可以得出一些初步的結論:工廠模式封裝了物件的建立過程,把建立和使用物件的過程進行了分離,解耦程式碼中對具體物件建立類的依賴。讓程式碼更好維護,更方便擴充套件

當然,如果想要知道這個設計模式是如何封裝了物件的建立過程,並且減少了對具體類的依賴的話,我們還是要實踐一下,通過一些例子或者開發中的場景學習如何使用好這個設計模式。那就讓我們開始吧。

簡單工廠

根據對程式碼封裝抽象的程度,工廠模式的實現方式有三種,它們分別是:簡單工廠工廠方法,以及抽象工廠

我們首先來學習和了解一下簡單工廠吧,假如你現在接手了一個生產蛋糕的程式,程式的部分程式碼如下:

// 泡芙蛋糕
const PUFF_CAKE = "PUFF_CAKE";
// 乳酪蛋糕
const CHEESE_CAKE = "CHEESE_CAKE";

class PuffCake {
  constructor() {
    this.name = "(泡芙蛋糕)";
  }
}
class CheeseCake {
  constructor() {
    this.name = "(乳酪蛋糕)";
  }
}

class CakeMaker {
  constructor(type) {
    if (type === PUFF_CAKE) {
      this.cake = new PuffCake();
    } else {
      this.cake = new CheeseCake();
    }
  }

  // 攪拌原料
  stirIngredients() {
    console.log(`開始攪拌${this.cake.name}`);
  }

  // 倒入模具中
  pourIntoMold() {
    console.log(`將${this.cake.name}倒入模具`);
  }

  // 烘烤蛋糕
  bakeCake() {
    console.log(`開始烘焙${this.cake.name}蛋糕`);
  }
}

// 製作蛋糕
const cakeMaker = new CakeMaker(PUFF_CAKE);
cakeMaker.stirIngredients();
cakeMaker.pourIntoMold();
cakeMaker.bakeCake();

現在這個製作蛋糕的程式需要新新增一種海綿蛋糕,你要怎麼去修改這個程式,讓它能夠支援生產海綿蛋糕呢?也許你的第一反應就是將CakeMaker建構函式進行修改,新增加一個型別的判斷,比如像下面這樣:

// ...
constructor(type) {
    if (type === PUFF_CAKE) {
      this.cake = new PuffCake();
    } else if (type === CHEESE_CAKE) {
      this.cake = new CheeseCake();
    } else {
      this.cake = new SpongeCake();
    }
}
// ...

這時,我們可以思考一下,雖然上面的方法的確可以幫助我們實現新增海綿蛋糕的功能,但是這樣做會有一些問題。會有哪些問題呢?

image

首先,如果按照這個方式的話,我們以後只要新增新種類的蛋糕或者移除不受歡迎的蛋糕就必須要修改CakeMaker建構函式

這樣做實在不是一個好的方案,而且每當我們在CakeMaker中新增加一個具體的蛋糕類的話,就相當於給這個類新增加了一個依賴。這樣我們CakeMaker的依賴會越來越多,任何一個依賴類發生改變都可能導致我們的CakeMaker類不能夠正常工作,出錯的機率大大增加

那麼我們應該如何修改呢?我們應該減少CakeMaker類中對具體類的依賴,然後將生成蛋糕種類的過程從CakeMaker中移除。我們可以這樣做:

// ...
// 封裝蛋糕的建立過程
function cakeCategoryMaker(type) {
  let cake;
  if (type === PUFF_CAKE) {
    cake = new PuffCake();
  } else if (type === CHEESE_CAKE) {
    cake = new CheeseCake();
  } else {
    cake = new SpongeCake();
  }
  return cake;
}
// ...
class CakeMaker {
  constructor(type) {
    this.cake = cakeCategoryMaker(type);
  }
  // ...
}
// ...

當你看完了上面的程式碼,你可能會說,這不只是把程式碼從一個地方移到了另一個地方,好像沒有發生什麼根本的變化呀。的確是這樣,但是我們來看一下,一旦我們把生成蛋糕種類的程式碼移到外面,我們的CakeMaker是不是減少了對具體蛋糕類的依賴。現在對於CakeMaker類來說,它的依賴只有cakeCategoryMakerCakeMaker不需要管你給我的蛋糕是什麼型別的,我只負責對其進行加工製作,並不關心蛋糕的原料和種類。

而且,我們的cakeCategoryMaker還可以被其它的蛋糕加工程式所共享;如果以後還需要增加或者移除蛋糕種類的話,我們只需要在這一個地方修改就可以了。而不需要在每個加工蛋糕的程式碼中分別進行修改。這就是一個很好的編碼習慣。

image

在實際的開發中,我們的程式中可能存在需要根據不同場景建立不同型別物件的功能,但是這些物件具有同樣的屬性和介面,或者需要根據不同的資料來源建立相同的物件。那麼這個時候,我們就可以把這一部分的邏輯抽離出來,然後在全域性中進行使用

這就是我們所說的簡單工廠了,當然嚴格意義上來說,簡單工廠不算是一個真正的設計模式。但是它很有用,它封裝了根據不同型別來建立不同物件的過程,將我們的程式進行了解耦,這樣便於程式的維護和擴充套件。是一個不錯的程式設計習慣和技巧,值得我們學習和使用

工廠方法

接下來我們來了解並學習工廠方法這種更高一級別的封裝和抽象。在實際的開發中我們有時會寫一些通用的元件,方便我們後續的業務開發使用。假如下面兩個元件是已經開發好的元件:

class Toast {
  constructor(text) {
    this.text = text;
  }
  show() {
    console.log(`toast show: ${this.text}`);
  }
  hide() {
    console.log("toast hide");
  }
}

class Modal {
  constructor(text) {
    this.text = text;
  }
  show() {
    console.log(`modal show: ${this.text}`);
  }
  hide() {
    console.log("modal hide");
  }
}

const toast = new Toast("hello");
toast.show();
toast.hide();
// modal
const modal = new Modal("world");
modal.show();
modal.hide();

上面關於元件的程式碼是沒有什麼太大問題的,但是我們再仔細思考一下也許會覺得好像這兩個元件都有showhide這兩個方法。那麼這就相當於是重複程式碼了,一般情況下如果出現了重複的程式碼那麼說明我們還是有優化的地方的。

image

並且如果在不改變現有的思路的情況下,我們要再開發一個新的提示型別的元件的話,還是會在程式碼中重複這兩個方法。那麼有沒有辦法解決這個問題呢?當然有辦法了,我們知道這種型別的元件都有showhide這兩個方法。那麼我們可以通過繼承的方式從父類那裡繼承這兩個方法,關於元件的具體建立過程我們可以在子類中進行實現

具體實現的程式碼如下:

class CustomComponent {
  createComponent() {
    // TODO 需要被子類實現
  }
  show() {
    this.concreteComponent = this.createComponent();
    console.log(
      `this ${this.concreteComponent.name} show: ${this.concreteComponent.text}`
    );
  }
  hide() {
    console.log(`this component hide`);
  }
}

class ToastComponent extends CustomComponent {
  createComponent() {
    return {
      name: "toast",
      text: "hello",
    };
  }
}

class ModalComponent extends CustomComponent {
  createComponent() {
    return {
      name: "modal",
      text: "world",
    };
  }
}

const toast = new ToastComponent();
toast.show();
toast.hide();
const modal = new ModalComponent();
modal.show();
modal.hide();

這個解決方案的思路就是:我們在父類中把子類的一些通用的操作進行實現。然後具體元件的建立細節交給子類去解決。那麼這樣做就相當於把元件建立的過程進行了封裝,父類不需要知道這個元件是如何建立的,被誰建立的。但是這個子類元件已經繼承了父類的那些方法,所以可以直接使用父類的方法進行展示和隱藏。

image

因為在JavaScript中暫時還沒有實現抽象類的功能,所以我們上面程式碼中的CustomComponent類從嚴格意義上說還不是一個抽象的父類。不過關係不是很大,思路和功能還是能夠實現的。

那我們來總結一下工廠方法的特性:

  • 父類通過一個抽象的方法封裝了物件的建立過程,物件的建立過程被延遲到子類中進行建立
  • 子類因為是從父類繼承而來,所以可以使用父類已經實現好的方法
  • 工廠方法將我們的程式碼進行了解耦,建立元件的時候不需要再給父類傳遞物件的型別,由子類決定建立的物件的型別

抽象工廠

接下來我們來講解一下抽象程度最高的抽象工廠,看過上一篇文章?設計模式大冒險第二關:裝飾者模式,煎餅果子的主場的同學應該知道,由於上次你給煎餅果子的老闆幫了個忙。所以他知道你的程式設計水平不錯,今天又來找你幫忙啦。

這次的問題是這樣的,老闆說他那邊有一個獲取煎餅果子原材料的程式,但是最近附近的一個菜市場提供的蔬菜不是很新鮮。所以想換一個菜市場去買原材料,但是更換菜市場的話,之前程式一些統計的資料就不準確了,所以需要讓你幫忙修改一下現有的程式。現在的程式部分程式碼如下:

class PanCakeMaterials {
  constructor(vegetableMarketName) {
    this.vegetableMarketName = vegetableMarketName;
  }

  getEgg() {
    if (this.vegetableMarketName === "VEGETABLE_MARKET_NAME_A") {
      return "a_market_egg";
    }
    if (this.vegetableMarketName === "VEGETABLE_MARKET_NAME_B") {
      return "b_market_egg";
    }
  }

  // ... 其它的原料
}

const panCakeMaterials = new PanCakeMaterials("VEGETABLE_MARKET_NAME_A");
console.log(panCakeMaterials.getEgg());  // a_market_egg

我們可以看到現在這個獲取原材料的程式雖然實現了獲取原材料的功能,但是現在的擴充套件性太差。如果新增了新的菜市場或者移除不使用的菜市場的話,就需要修改程式。所以又到了你大展身手的時候了,巧的是你剛剛學習完工廠模式的抽象工廠這個解決方案。所以你知道該如何重構現在的程式碼了。

首先原來的程式碼太依賴我們給的型別值了,如果輸入的型別值有問題的話,那麼整個獲取原材料的程式就沒有辦法執行起來。所以我們需要將每種食材獲取的過程封裝起來,由一個VegetableMarketProvider類來負責,然後對於PanCakeMaterials來說,我們只需要將VegetableMarketProvider子類的例項化物件當做傳給PanCakeMaterials類的引數進行初始化就可以了

實現的程式碼如下所示:

class VegetableMarketProvider {
  provideEgg() {}
}

class FirstVegetableMarketProvider extends VegetableMarketProvider {
  provideEgg() {
    return "a_market_egg";
  }
}

class SecondVegetableMarketProvider extends VegetableMarketProvider {
  provideEgg() {
    return "b_market_egg";
  }
}

class PanCakeMaterials {
  constructor(vegetableMarketProvider) {
    this.vegetableMarketProvider = vegetableMarketProvider;
  }

  getEgg() {
    return this.vegetableMarketProvider.provideEgg();
  }

  // ... 其它的原料
}

const firstVegetableMarketProvider = new FirstVegetableMarketProvider();
const secondVegetableMarketProvider = new SecondVegetableMarketProvider();
const panCakeMaterials = new PanCakeMaterials(firstVegetableMarketProvider);
console.log(panCakeMaterials.getEgg());  // a_market_egg
const secondPanCakeMaterials = new PanCakeMaterials(
  secondVegetableMarketProvider
);
console.log(secondPanCakeMaterials.getEgg());  // b_market_egg

我們來分析一下優化後的程式碼,首先我們寫了一個抽象的VegetableMarketProvider類,這個類裡面的所有方法也都是抽象的,需要由子類去實現具體的方法。每一個子類對應一個具體的菜市場,這樣的話每一個菜市場提供的原材料也就知道是什麼了

image

對於PanCakeMaterials類來說,我們不再傳遞一個表示菜市場型別的字串了;取而代之的是,傳遞一個菜市場的例項。這樣的話當我們初始化PanCakeMaterials的時候,對應的菜市場也就確定了,那對應的原材料也就確定了

這樣的好處有哪些呢?首先如果我們要更換菜市場,再也不需要改變PanCakeMaterials類的程式碼了,只需要更換傳給PanCakeMaterials類的引數就可以了。然後如果需要新增新的菜市場的話,只需要新加一個VegetableMarketProvider的子類,在子類裡面實現相應原材料的獲取。這就體現了我們程式設計中的一個原則,對修改關閉,對擴充套件開放

我們通過上面的優化,把獲取原材料的過程封裝到VegetableMarketProvider的子類中,然後通過物件組合的方式實現了對菜市場的更換,這樣的方式進一步解耦了我們的程式碼,每一個類都各司其職,保證了職責的單一

那我們再來簡單總結一下抽象工廠這個方式吧。

  • 通過使用一個抽象類,把相關的介面進行了定義,然後繼承這個抽象類的子類都具有相同的介面和屬性。這樣用到這些子類的類可以對這些類進行介面程式設計,而不是在針對具體的類
  • 物件的建立過程被封裝在子類中,這樣實現了程式碼的封裝以及類依賴的解耦
  • 使用不同的子類,通過物件的組合,我們可以實現我們想要的建立不同物件的功能

文章到這裡就結束了,如果大家有什麼問題和疑問歡迎大家在文章下面留言,或者在這裡提出來。也歡迎大家關注我的公眾號關山不難越,獲取更多關於設計模式講解的內容。

下面是這一系列的其它的文章,也歡迎大家閱讀,希望大家都能夠掌握好這些設計模式的使用場景和解決的方法。如果這篇文章對你有所幫助,那就點個贊,分享一下吧~

文章封面圖來源:unDraw

相關文章