引子:人間活雷鋒#
小菜的同班同學薛磊風出車禍住院了。住院期間大家才知道:
- 薛磊風從大一起就每週末去探望一位孤寡老人
- 為老人洗衣、掃地、買米買油,三年多從未間斷
- 老人並不知道是誰在幫忙,只當「學雷鋒做好事」
- 出事後薛磊風委託同學接力照顧
這個故事正好呼應工廠方法模式的精神——做雷鋒的人可以更換,但「學雷鋒」這件事不會變。
簡單工廠模式回顧#
以第 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 次方」),都要修改
createOperate的switch—— 既對擴展開放,也對修改開放,違反開放-封閉原則。
工廠方法模式#
工廠方法模式(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 章「抽象工廠模式」會深入討論。