引子:人間活雷鋒#

小菜的同班同學薛磊風出車禍住院了。住院期間大家才知道:

  • 薛磊風從大一起就每週末去探望一位孤寡老人
  • 為老人洗衣、掃地、買米買油,三年多從未間斷
  • 老人並不知道是誰在幫忙,只當「學雷鋒做好事」
  • 出事後薛磊風委託同學接力照顧

這個故事正好呼應工廠方法模式的精神——做雷鋒的人可以更換,但「學雷鋒」這件事不會變

簡單工廠模式回顧#

以第 1 章的計算器為例,簡單工廠把實例化集中在工廠類:

class OperationFactory
{
    public static Operation createOperate(string operate)
    {
        Operation oper = null;
        switch (operate)
        {
            case "+": oper = new OperationAdd(); break;
            case "-": oper = new OperationSub(); break;
            case "*": oper = new OperationMul(); break;
            case "/": oper = new OperationDiv(); break;
        }
        return oper;
    }
}

簡單工廠的優點:客戶端不需依賴具體產品,只給「+」就能拿到對應實例。

缺點:每新增一種運算(例如「M 的 N 次方」),都要修改 createOperateswitch —— 既對擴展開放,也對修改開放,違反開放-封閉原則。

工廠方法模式#

工廠方法模式(Factory Method Pattern):定義一個用於創建物件的介面,讓子類別決定實例化哪一個類別。工廠方法使一個類別的實例化延遲到其子類別。[DP]

結構#

  • Product:定義工廠方法所創建物件的介面
  • ConcreteProduct:實作 Product 介面
  • Creator:宣告工廠方法,回傳 Product 型別物件
  • ConcreteCreator:覆寫工廠方法以回傳 ConcreteProduct 實例
classDiagram
    class IFactory {
        <<interface>>
        +CreateOperation() Operation
    }
    class Operation {
        +GetResult() double
    }
    class AddFactory
    class SubFactory
    class MulFactory
    class DivFactory
    class OperationAdd
    class OperationSub
    class OperationMul
    class OperationDiv
    IFactory <|.. AddFactory
    IFactory <|.. SubFactory
    IFactory <|.. MulFactory
    IFactory <|.. DivFactory
    Operation <|-- OperationAdd
    Operation <|-- OperationSub
    Operation <|-- OperationMul
    Operation <|-- OperationDiv
    AddFactory ..> OperationAdd : creates
    SubFactory ..> OperationSub : creates
    MulFactory ..> OperationMul : creates
    DivFactory ..> OperationDiv : creates

計算器的工廠方法版本#

不再有一個包含 switch 的工廠類,而是讓每個運算都有對應的工廠:

interface IFactory
{
    Operation CreateOperation();
}

class AddFactory : IFactory { public Operation CreateOperation() => new OperationAdd(); }
class SubFactory : IFactory { public Operation CreateOperation() => new OperationSub(); }
class MulFactory : IFactory { public Operation CreateOperation() => new OperationMul(); }
class DivFactory : IFactory { public Operation CreateOperation() => new OperationDiv(); }

客戶端:

IFactory operFactory = new AddFactory();
Operation oper = operFactory.CreateOperation();
oper.NumberA = 1;
oper.NumberB = 2;
double result = oper.GetResult();

新增「M 的 N 次方」時:

  • 增加一個運算類別(產品類)
  • 增加一個工廠類(創建者)
  • 不需要修改原有的工廠類

整個工廠與產品體系都「對擴展開放,對修改關閉」,完全符合開放-封閉原則。

簡單工廠 vs. 工廠方法#

  • 簡單工廠:把分支判斷集中在工廠類;新增產品要改工廠類,違反 OCP
  • 工廠方法:把分支判斷下放到客戶端「選擇要實例化哪個工廠」;新增產品只需新增類別

工廠方法的關鍵在於——選擇判斷的問題還是存在的,只是從工廠內部移到了客戶端。

雷鋒工廠範例#

把雷鋒做的事抽象出來:

class LeiFeng
{
    public void Sweep()   { Console.WriteLine("掃地"); }
    public void Wash()    { Console.WriteLine("洗衣"); }
    public void BuyRice() { Console.WriteLine("買米"); }
}

class Undergraduate : LeiFeng { } // 學雷鋒的大學生
class Volunteer     : LeiFeng { } // 社區志願者

簡單工廠版本#

class SimpleFactory
{
    public static LeiFeng CreateLeiFeng(string type)
    {
        switch (type)
        {
            case "學雷鋒的大學生": return new Undergraduate();
            case "社區志願者":     return new Volunteer();
        }
        return null;
    }
}

工廠方法版本#

interface IFactory { LeiFeng CreateLeiFeng(); }

class UndergraduateFactory : IFactory { public LeiFeng CreateLeiFeng() => new Undergraduate(); }
class VolunteerFactory     : IFactory { public LeiFeng CreateLeiFeng() => new Volunteer(); }

客戶端:

IFactory factory = new UndergraduateFactory();
LeiFeng student = factory.CreateLeiFeng();
student.BuyRice();
student.Sweep();
student.Wash();

要從「學雷鋒的大學生」換成「社區志願者」,只需修改一處——把 new UndergraduateFactory() 換成 new VolunteerFactory()

何時使用?#

  • 工廠方法克服了簡單工廠違背開放-封閉原則的缺點,保持了封裝物件創建過程的優點
  • 工廠方法是簡單工廠的進一步抽象和推廣
  • 利用多型性,使得更換物件時不需大幅改動,降低客戶端與產品物件的耦合

缺點:每增加一個產品就要新增一個產品工廠類別,增加額外的開發量

客戶端依然要選擇要實例化哪個工廠(雖然只有一處)。

這個問題的徹底解法是「反射」(Reflection),第 15 章「抽象工廠模式」會深入討論。