我的設計模式之旅、09 工廠方法、簡單工廠

2022-09-16 06:00:43

一個菜鳥的設計模式之旅,文章可能會有不對的地方,懇請大佬指出錯誤。

程式設計旅途是漫長遙遠的,在不同時刻有不同的感悟,本文會一直更新下去。

程式介紹

由於 Go 中缺少類和繼承等 OOP 特性, 所以無法使用 Go 來實現經典的工廠方法模式。 不過, 我們仍然能實現模式的基礎版本, 即簡單工廠。案例中使用工廠結構體來構建多種型別的武器。因此工廠方法模式程式碼使用 C# 表示。

Gun: AK47 gun
Power: 4
Gun: Musket gun
Power: 1

程式程式碼、簡單工廠模式 Golang

首先, 建立一個名為 i­Gun的介面, 其中將定義一支槍所需具備的所有方法。 然後是實現了 iGun 介面的 gun槍支結構體型別。 兩種具體的槍支——ak47mus­ket火槍 ——兩者都嵌入了槍支結構體, 且間接實現了所有的 i­Gun方法。

gun­Fac­to­ry槍支工廠結構體將發揮工廠的作用, 即通過傳入引數構建所需型別的槍支。 main.go 則扮演著使用者端的角色。 其不會直接與 ak47mus­ket進行互動, 而是依靠 gun­Fac­to­ry來建立多種槍支的範例, 僅使用字元引數來控制生產。

iGun.go: 產品介面

package main

type IGun interface {
    setName(name string)
    setPower(power int)
    getName() string
    getPower() int
}

gun.go: 具體產品

package main

type Gun struct {
    name  string
    power int
}

func (g *Gun) setName(name string) {
    g.name = name
}

func (g *Gun) getName() string {
    return g.name
}

func (g *Gun) setPower(power int) {
    g.power = power
}

func (g *Gun) getPower() int {
    return g.power
}

ak47.go: 具體產品

package main

type Ak47 struct {
    Gun
}

func newAk47() IGun {
    return &Ak47{
        Gun: Gun{
            name:  "AK47 gun",
            power: 4,
        },
    }
}

musket.go: 具體產品

package main

type musket struct {
    Gun
}

func newMusket() IGun {
    return &musket{
        Gun: Gun{
            name:  "Musket gun",
            power: 1,
        },
    }
}

gunFactory.go: 工廠

package main

import "fmt"

func getGun(gunType string) (IGun, error) {
    if gunType == "ak47" {
        return newAk47(), nil
    }
    if gunType == "musket" {
        return newMusket(), nil
    }
    return nil, fmt.Errorf("Wrong gun type passed")
}

main.go: 使用者端程式碼

package main

import "fmt"

func main() {
    ak47, _ := getGun("ak47")
    musket, _ := getGun("musket")

    printDetails(ak47)
    printDetails(musket)
}

func printDetails(g IGun) {
    fmt.Printf("Gun: %s", g.getName())
    fmt.Println()
    fmt.Printf("Power: %d", g.getPower())
    fmt.Println()
}

Console: 輸出

Gun: AK47 gun
Power: 4
Gun: Musket gun
Power: 1

程式程式碼、工廠方法模式 C#

程式功能和簡單工廠相同,用工廠方法模式實現。

Gun.cs: 武器類

namespace 工廠方法;

public abstract class Gun
{
    protected Gun(string name, int power)
    {
        Name = name;
        Power = power;
    }

    public string Name { get; private set; }

    public int Power { get; private set; }

    public void setName(string name)
    {
        Name = name;
    }

    public void setPower(int power)
    {
        Power = power;
    }
}

public class AK47 : Gun
{
    public AK47() : base("ak47", 10)
    {
    }
}

public class Musket : Gun
{
    public Musket() : base("musket", 5)
    {
    }
}

Factory.cs: 工廠類

namespace 工廠方法;

public interface GunFactory
{
    Gun createGun();
}

public class AK47Factory : GunFactory
{
    public Gun createGun()
    {
        Console.WriteLine("正在生產AK47");
        return new AK47();
    }
}

public class MusketFactory : GunFactory
{
    public Gun createGun()
    {
        Console.WriteLine("正在生產Musket");
        return new Musket();
    }
}

Program.cs: 使用者端程式碼

using 工廠方法;

GunFactory gunFactory = new AK47Factory();

Gun ak47_1 = gunFactory.createGun();
Gun ak47_2 = gunFactory.createGun();

gunFactory = new MusketFactory();

Gun Musket_1 = gunFactory.createGun();

List<Gun> guns = new List<Gun>() { ak47_1, ak47_2, Musket_1 };

foreach (Gun gun in guns)
{
    Console.WriteLine($"武器名字:{gun.Name}");
    Console.WriteLine($"武器傷害:{gun.Power}");
}

Console: 輸出

正在生產AK47
正在生產AK47
正在生產Musket
武器名字:ak47
武器傷害:10
武器名字:ak47
武器傷害:10
武器名字:musket
武器傷害:5

思考總結

什麼是簡單工廠模式

簡單工廠模式也是工廠模式的一種,但不屬於23種設計模式。

目的使使用者端與產品解耦。將產品建立範例過程從使用者端程式碼中獨立出去。生成具體物件的邏輯判斷也從使用者端分離至簡單工廠類中。

簡單工廠模式的最大優點在於工廠類中包含了必要的邏輯判斷,根據使用者端的選擇條件動態範例化相關的類,對於使用者端來說,去除了與具體產品的依賴。

但簡單工廠違背了開放-封閉原則,導致每次新增新類別的時候,都需要去修改工廠類的分支case判斷,可以用反射動態生成範例解決。

簡單工廠反射案例:我的設計模式之旅、01 策略模式、簡單工廠、反射 - 小能日記

什麼是工廠方法模式

工廠方法是一種建立型設計模式, 其在父類別中提供一個建立物件的方法,允許子類決定範例化物件的型別。

工廠方法模式:定義一個用於建立物件的介面,讓子類決定範例化哪一個類。工廠方法使一個類的範例化延遲到子類。

image-20220915214645465

工廠方法類 (建立者類):主要職責並非是建立產品。其中通常會包含一些核心業務邏輯,這些邏輯依賴於由工廠方法返回的產品物件。子類可通過重寫工廠方法並使其返回不同型別的產品來間接修改業務邏輯。

主要解決:主要解決介面選擇的問題。使用者端將所有產品視為抽象的介面,使用者端知道所有產品都提供該介面要求的方法,並不關心其具體實現方式。

何時使用:

  • 我們明確地計劃不同條件建立不同範例時。
  • 當你在編寫程式碼的過程中,如果無法預知物件確切類別及其依賴關係時,可使用工廠方法。
  • 希望使用者能擴充套件你軟體庫或框架的內部元件,可使用工廠方法。
  • 希望複用現有物件來節省系統資源,而不是每次都重新建立物件,可使用工廠方法。在處理大型資源密集型物件(資料庫連線、檔案系統和網路資源)時,你會經常碰到這種資源需求。
  • 在許多設計工作的初期都會使用工廠方法(較為簡單,而且可以更方便地通過子類進行客製化),隨後演化為使用抽象工 廠、原型或生成器(更靈活但更加複雜)。

如何解決:讓其子類實現工廠介面,返回的也是一個抽象的產品。使用特殊的工廠方法代替對於物件建構函式new的直接呼叫。

實現步驟:

  1. 讓所有產品遵循同一介面。改介面必須宣告對所有產品都有意義的方法。
  2. 在工廠方法類(建立類中)新增一個空的工廠方法。該方法的返回型別必須遵循通用的產品介面。
  3. 找到對於產品建構函式的所有參照。依次替換為對工廠方法的呼叫,並將建立產品的程式碼移入工廠方法。(類似簡單工廠,此時switch分支是龐大的)
  4. 為每種產品編寫建立者子類,在子類中重寫工廠方法,將基本方法的相關建立程式碼移動到工廠方法中。子類過多時,可以給子類建立者傳入引數,判斷生成。
  5. 如果程式碼經過上述移動後,基礎工廠方法中已經沒有任何程式碼,你可以將其轉變為抽象類。如果基礎工廠方法中還有其他語句,你可以將其設定為該方法的預設行為。一般總是把建立者基礎類別作為抽象類。

關鍵程式碼:建立過程在其子類執行。

應用範例:

  • 您需要一輛汽車,可以直接從工廠裡面提貨,而不用去管這輛汽車是怎麼做出來的,以及這個汽車裡面的具體實現。
  • Hibernate 換資料庫只需換方言和驅動就可以。

優點:

  • 一個呼叫者想建立一個物件,只要知道其名稱就可以了。
  • 開閉原則。擴充套件性高,如果想增加一個產品,只要擴充套件一個工廠類就可以。
  • 遮蔽產品的具體實現,呼叫者只關心產品的介面。避免建立者和具體產品之間的緊密耦合。
  • 單一職責原則。你可以將產品建立程式碼放在程式的單一位置, 從而使得程式碼更容易維護。

缺點:每次增加一個產品時,都需要增加一個具體類和物件實現工廠,使得系統中類的個數成倍增加,在一定程度上增加了系統的複雜度,同時也增加了系統具體類的依賴。這並不是什麼好事。

使用場景:

  • 紀錄檔記錄器:記錄可能記錄到本地硬碟、系統事件、遠端伺服器等,使用者可以選擇記錄紀錄檔到什麼地方。
  • 資料庫存取,當用戶不知道最後系統採用哪一類資料庫,以及資料庫可能有變化時。
  • 設計一個連線伺服器的框架,需要三個協定,"POP3"、"IMAP"、"HTTP",可以把這三個作為產品類,共同實現一個介面。

注意事項:

  • 作為一種建立類模式,在任何需要生成複雜物件的地方,都可以使用工廠方法模式。有一點需要注意的地方就是複雜物件適合使用工廠模式,而簡單物件,特別是只需要通過 new 就可以完成建立的物件,無需使用工廠模式。如果使用工廠模式,就需要引入一個工廠類,會增加系統的複雜度。

  • 建立物件的邏輯判斷依舊在使用者端中實現。

與其他模式的關係:

  • 抽象工廠模式通常基於一組工廠方法,但你也可以使用原型模式來生成這些類的方法。
  • 你可以同時使用工廠方法和迭代器來讓子類集合返回不同型別的迭代器,並使得迭代器與集合相匹配。
  • 工廠方法是模板方法的一種特殊形式。同時,工廠方法可以作為一個大型模板方法中的一個步驟。

思考物件複用的方法

  1. 首先,你需要建立儲存空間來存放所有已經建立的物件。

  2. 當他人請求一個物件時,程式將在物件池中搜尋可用物件。

  3. 然後將其返回給使用者端程式碼。

  4. 如果沒有可用物件,程式則建立一個新物件(並將其新增到物件池中)。

四個步驟的程式碼必須位於同一處,確保重複程式碼不會汙染程式!

參考資料