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

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

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

程序介紹

由於 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#

image-20220915230253913

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

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. 如果沒有可用對象,程序則創建一個新對象(並將其添加到對象池中)。

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

參考資料