故事引入
小菜今年計算機專業大四畢業,在找工作面試的時候,遇到一道題目👇
- “請用C++、JAV、C# 或 VB.NET 任意一種面向對象語言實作一個計算機控制台程式,要求輸入兩個數和運算符,得到結果”
小菜做完題目交卷後,石沉大海,小菜實作電腦的代碼:
代碼問題
- 代碼規範問題:明明不規範、三次無效判斷、沒有考慮特殊情況
- 隻是簡單實作電腦功能,沒有面向對象設計,維護和二次開發都不友善,要實作容易維護,容易擴充,又容易複用的代碼!
栗子說明問題
- 三國時期,曹操詩性大發“喝酒唱歌,人生真爽”,衆文臣齊呼“丞相好詩!”于是一個臣子立馬命印刷工匠刻闆印刷這首詩!
- 樣張出來後給曹操一看,覺得不妥,說把“喝”與“唱”太俗氣,應改成“對酒當歌”較好!于是這個臣子又工匠重新印刷,工匠眼看連夜刻闆之工,徹底白費,心中叫苦,但隻能照辦出第二版: -樣張再次出來後請曹操過目,曹操細細一品覺得還是不夠好,說“人生真爽過于直接,應改成問語才夠意境,因為這裡應該改成 “對酒當歌,人生幾何?” …”當臣子轉告工匠時,工匠暈倒!!
- 其中原因:三國時期,活字印刷術還未發明,改字的時候要整個刻闆全部重新雕刻!
- 解決辦法:有活字印刷後,隻需要改四個字即可,不會白費其他工作,豈不妙哉!!
- 要修改時隻需要修改更改的字,此為可維護
- 這些字做出來之後并不是沒用了,完全可以在後來的印刷中重複使用,此乃可複用
- 若要加字,隻需要另刻新字加入即可,這是可擴充
- 字的排序其實可能是豎排或者橫排,此時隻需要将活字移動就可以滿足需求,這叫靈活性好
- 在活字印刷術出來之前,這四種特性都無法滿足,修改、加字、排序、必須重刻,印完一本書後,整個版子就沒有價值
面向對象的好處
客戶要求改需求時,因為我們的程式不宜維護,靈活性差,不容易擴充,更談不上複用,是以面對需求分析,隻能加班處理!一般都要對程式大作修改!但如果用面向對象的分析設計程式設計思想,考慮通過封裝、繼承、多态把程式的耦合度降低,傳統印刷術的問題就在于所有字都可在同一闆面上造成耦合度太高導緻,用設計模式使程式更加靈活、容易修改、并且易于複用!
設計原則
1. 複用代碼封裝業務
用盡可能的辦法去避免重複,業務邏輯與界面邏輯分開,降低耦合度,分離才能更容易維護擴充
這樣的代碼如果要添加一個新的運算,需要修改Operation類,在switch新+一個分支!這樣需要讓加減乘除都參與編譯,可能會修改錯别的代碼!
舉個栗子
公司财務要求做運維軟體,目前有技術人員(月薪),銷售人員(底薪+提成),經理(年薪+股份)三種算法,現在要求加入兼職人員(時薪)算法,按照上面的算法,要修改全部計算工資算法的代碼,如果除了增加一個類算法還在别處做了修改,每一處都會讓公司财務發生不必要的麻煩!!整個風險太大!
正确操作要把各個運算分離,修改新增删除一個不影響其他類型!
3. 松耦合
Operation
運算類
運算類有兩個Number變量,用于計算輸入,GetResult()在各個具體類重寫的方法,這樣修改任何一個算法,都會傳回的是其運算的值,也不會影響到其他算法代碼!但是需要告訴電腦使用的是哪一個算法!
簡單工廠模式
執行個體化對象: 用一個單獨的類來做創造執行個體的過程
簡單運算工廠類
OperationFactory.cs
:
class OperationFactory
{
public static Operation CreateOperate(string operate)
{
Operation operation = null;
switch (operate)
{
case "+":
operation = new OperationAdd();
break;
case "-":
operation = new OperationSub();
break;
case "*":
operation = new OperationMul();
break;
case "/":
operation = new OperationDiv();
break;
}
return operation;
}
}
運算基類
Operation.cs
:
class Operation
{
private double num1 = 0;
private double num2 = 0;
public double Num1 { get => num1; set => num1 = value; }
public double Num2 { get => num2; set => num2 = value; }
public virtual double GetResult()
{
double result = 0;
return result;
}
}
加法類
OperationAdd.cs
:
class OperationAdd : Operation
{
public override double GetResult()
{
double result = 0;
result = Num1 + Num2;
Console.Write("OperationAdd [簡單工廠模式加法運算]:" + Num1 + " + " + Num2 + " = " + result + "\n");
return result;
}
}
減法類
OperationSub .cs
:
class OperationSub : Operation
{
public override double GetResult()
{
double result = 0;
result = Num1 + Num2;
Console.Write("OperationSub [簡單工廠模式減法運算]:" + Num1 + " + " + Num2 + " = " + result + "\n");
return result;
}
}
除法類
OperationDiv .cs
class OperationDiv : Operation
{
public override double GetResult()
{
if (Num2 == 0)
{
throw new ArgumentException();
}
double result = Num1 / Num2;
Console.Write("OperationDiv [簡單工廠模式除法運算]:" + Num1 + " / " + Num2 + " = " + result + "\n");
return result;
}
}
乘法類
OperationMul .cs
class OperationMul : Operation
{
public override double GetResult()
{
double result;
result = Num1 * Num2;
Console.Write("OperationMul [簡單工廠模式乘法運算]:" + Num1 + " × " + Num2 + " = " + result + "\n");
return base.GetResult();
}
}
用戶端主程式
Program.cs
class Program
{
static void Main(string[] args)
{
int num1 = 10;
int num2 = 20;
Operation addOperation = OperationFactory.CreateOperate("+");
addOperation.Num1 = num1;
addOperation.Num2 = num2;
double addResult = addOperation.GetResult();
Console.Write("OperationFactory 簡單工廠加法 \n\n");
// 簡單工廠 -
Operation subOperation = OperationFactory.CreateOperate("-");
subOperation.Num1 = num1;
subOperation.Num2 = num2;
double subResult = subOperation.GetResult();
Console.Write("OperationFactory 簡單工廠減法 \n\n");
// 簡單工廠 *
Operation mulOperation = OperationFactory.CreateOperate("*");
mulOperation.Num1 = num1;
mulOperation.Num2 = num2;
double mulResult = mulOperation.GetResult();
Console.Write("OperationFactory 簡單工廠乘法 \n\n");
// 簡單工廠 *
Operation divOperation = OperationFactory.CreateOperate("/");
divOperation.Num1 = num1;
divOperation.Num2 = num2;
double divResult = divOperation.GetResult();
Console.Write("OperationFactory 簡單工廠除法 \n\n");
Console.ReadKey();
}
}
運作結果:
OperationAdd [簡單工廠模式加法運算]:10 + 20 = 30
OperationFactory 簡單工廠加法
OperationSub [簡單工廠模式減法運算]:10 + 20 = 30
OperationFactory 簡單工廠減法
OperationMul [簡單工廠模式乘法運算]:10 × 20 = 200
OperationFactory 簡單工廠乘法
OperationDiv [簡單工廠模式除法運算]:10 / 20 = 0.5
OperationFactory 簡單工廠除法
不規範的類圖:
- 要增加新運算比如開方,直接添加一個開方類,并且修改工廠類即可,若要修改加法運算,直接修改加法類中代碼,不會影響其他運算!
工廠方法模式
- 簡單工廠添加新類時需要修改工廠裡面的
分支條件!修改原有的類說明不旦開放擴充,對修改也開放了,這樣違背了開放-封閉原則Case
工廠方法模式(Factory Method),定義一個用于建立對象的接口,讓子類決定執行個體化哪一個類。工廠方法使一個類的執行個體化延遲到其子類。
把工廠類抽象出一個接口,這個接口隻有一個方法,就是建立抽象産品的工廠方法,然後所有的要生産具體類的工廠就去實作這個接口,這樣一個簡單工廠模式的工廠類變成一個工廠抽象接口和多個具體生成對象的工廠!需要增加新的運算功能時,不需要改變原有的工廠類,隻需要增加此功能的運算類和相應的工廠類就行!
- 這樣一來整個工廠和産品體系其實沒有發生什麼修改,隻是擴充的變化,這就完全符合開放-封閉的原則!!
- 工廠方法模式實作時,用戶端需要決定執行個體化哪一個工廠來實作運算類,選擇判斷的問題還是存在的,也就是說,工廠方法把簡單工廠的内部邏輯判斷移到用戶端代碼來進行,如果說想要增加功能,本來是要改工廠類,現在變成在修改用戶端!
工廠方法模式應用
- 栗子:薛磊風是一個大學生,以學習雷鋒的名義去幫助老人做事,這裡可以設計成一個了雷鋒類,擁有掃地、洗米、買米等方法。
LeiFeng.cs
// 雷鋒類
class LeiFeng
{
public void Sweep()
{
Console.WriteLine("掃地");
}
public void Wash()
{
Console.WriteLine("洗衣");
}
public void BuyRice()
{
Console.WriteLine("買米");
}
}
‘學習雷鋒的大學生’類要繼承‘雷鋒’
Undergraduate.cs
public Undergraduate()
{
Name = "學習雷鋒的大學生";
}
用戶端實作代碼,大學生做雷鋒:
LeiFeng student = new Undergraduate();
student.BuyRice();
student.Wash();
student.Sweep();
如果說有三個大學生要去做事,就要執行個體化三個大學生,于是👇
由此可得建立一個社群志願者類
Volunteer .cs
class Volunteer : LeiFeng
{
public Volunteer()
{
Name = "學習雷鋒的社群志願者";
}
}
簡單學雷鋒工廠
SimpleFactory.cs
:
class SimpleFactory
{
public static LeiFeng CreateLeiFeng(string type)
{
LeiFeng result = null;
switch (type)
{
case "大學生":
result = new Undergraduate();
break;
case "志願者":
result = new Volunteer();
break;
}
return result;
}
}
用戶端代碼
Program.cs
:
LeiFeng studentA = SimpleFactory.CreateLeiFeng("大學生");
studentA.BuyRice();
LeiFeng studentB = SimpleFactory.CreateLeiFeng("大學生");
studentB.Sweep();
LeiFeng studentC = SimpleFactory.CreateLeiFeng("大學生");
studentC.Wash();
在執行個體化時都需要寫出這個工廠的代碼,這是重複的地方!!
// 雷鋒工廠
interface IFactory
{
LeiFeng CreateLeiFeng();
}
// 學習雷鋒的大學生工廠
class UndergraduateFactory : IFactory
{
public LeiFeng CreateLeiFeng()
{
//throw new NotImplementedException();
return new Undergraduate();
}
}
// 志願者工廠
class VolunteerFactory : IFactory
{
LeiFeng IFactory.CreateLeiFeng()
{
//throw new NotImplementedException();
return new Volunteer();
}
}
用戶端寫法
// 工廠方法
IFactory studentFactory = new UndergraduateFactory();
LeiFeng student = studentFactory.CreateLeiFeng();
student.BuyRice();
student.Sweep();
student.Wash();
IFactory volunteerFactory = new VolunteerFactory();
LeiFeng volunteer = volunteerFactory.CreateLeiFeng();
volunteer.BuyRice();
volunteer.Sweep();
volunteer.Wash();
輸出:
學習雷鋒的大學生 買米
學習雷鋒的大學生 掃地
學習雷鋒的大學生 洗衣
學習雷鋒的社群志願者 買米
學習雷鋒的社群志願者 掃地
學習雷鋒的社群志願者 洗衣
工廠方法和簡單工廠對比
- 工廠方法客服了簡單工廠違背 “ 開放-封閉原則 ” 的缺點,又保持了封裝對象建立過程的優點。
- 工廠方法模式是簡單工廠模式的進一步抽象和推廣,由于使用了多态性,工廠方法模式保持了簡單工廠模式的優點并克服它的缺點。
- 缺點:由于每增加一個産品都需要增加一個産品工廠類,增加了額外的開發量
- 同:工廠模式都是集中封裝了對象的建立,使要更換對象時,不需要做大的改動就可以實作,降低了客戶程式與産品對象的耦合!
工廠模式
意圖
主要解決接口選擇的問題。
使用場景
我們明确知道不同條件下要建立不同執行個體時
如何實作
讓工廠子類實作工廠接口,建立具體類型産品傳回,建立過程在子類執行完成
應用執行個體
- 吃一個面包,不需要關心怎麼做出來的,隻要知道價格味道
- 買一輛車直接從店裡提貨,不用管車怎麼制造的
- 使用API不需要關心底層建立邏輯,可以直接用這個類的方法
優點
- 一個調用者想要建立一個對象,隻需要知道其名字就可以
- 擴充性高,如果想增加一個産品,擴充一個工廠類+一個産品類即可
- 屏蔽産品的具體實作,調用者隻關心産品的接口
缺點
每次增加一個産品就需要增加一個具體類和對象具體實作類,導緻系統中類的數量成倍增長,增加系統代碼複雜度,同時也增加了系統具體類的依賴。這并不是什麼好事。
應用場景
- 日志記錄器:記錄可能記錄到本地硬碟、系統事件、遠端伺服器等,使用者可以選擇記錄日志到什麼地方。
- 資料庫通路,當使用者不知道最後系統采用哪一類資料庫,以及資料庫可能有變化時。
- 設計一個連接配接伺服器的架構,需要三個協定,“POP3”、“IMAP”、“HTTP”,可以把這三個作為産品類,共同實作一個接口。
- 在任何需要生成複雜對象的地方都可以使用工廠方法模式,但是簡單對象隻需要new 出來的就不需要用工廠,如果用了就會引入工廠類,隻會增加系統複雜度。
參考資料
《大話資料結構》
菜鳥教程 | 工廠模式