大話設計模式——第1章 代碼無錯就是優?——簡單工廠模式
1.1 面試受挫
小菜今年計算機專業大四了,學了不少軟件開發方面的東西,也學着編了些小程序,躊躇滿志,一心要找一個好單位。當投遞了無數份簡歷後,終於收到了一個單位的面試通知,小菜欣喜若狂。
到了人家單位,前臺小姐給了他一份題目,上面寫着:“請用C++、Java、C#或VB.NET任意一種面嚮對象語言實現一個計算器控制檯程序,要求輸入兩個數和運算符號,得到結果。”
小菜一看,這個還不簡單,三下五除二,10分鐘不到,小菜寫完了,感覺也沒錯誤。交卷後,單位說一週內等通知吧。於是小菜只得耐心等待。可是半個月過去了,什麼消息也沒有,小菜很納悶,我的代碼實現了呀,爲什麼不給我機會呢。
時間:2月26日20點 地點:大鳥房間 人物:小菜、大鳥
小菜找到從事軟件開發工作七年的表哥大鳥,請教原因,大鳥問了題目和了解了小菜代碼的細節以後,哈哈大笑,說道:“小菜呀小菜,你上當了,人家單位出題的意思,你完全都沒明白,當然不會再聯繫你了。”
小菜說:“我的代碼有錯嗎?單位題目不就是要我實現一個計算器的代碼嗎,我這樣寫有什麼 問題。”
class Program
{
static void Main(string[] args)
{
Console.Write("請輸入數字A:");
string A = Console.ReadLine();
Console.Write("請選擇運算符號(+、-、*、/):");
string B = Console.ReadLine();
Console.Write("請輸入數字B:");
string C = Console.ReadLine();
string D = "";
if (B == "+")
D = Convert.ToString(Convert.ToDouble(A) + Convert.ToDouble(C));
if (B == "-")
D = Convert.ToString(Convert.ToDouble(A) - Convert.ToDouble(C));
if (B == "*")
D = Convert.ToString(Convert.ToDouble(A) * Convert.ToDouble(C));
if (O == "/")
D = Convert.ToString(Convert.ToDouble(A) / Convert.ToDouble(C));
Console.WriteLine("結果是:" + D);
}
}
1.2 初學者代碼毛病
大鳥說:“且先不說出題人的意思,單就你現在的代碼,就有很多不足的地方需要改進。”
1.3 代碼規範
“哦,說得沒錯,這個我以前聽老師說過,可是從來沒有在意過,我馬上改,改完再給你看看。”
class Program
{
static void Main(string[] args)
{
try
{
Console.Write("請輸入數字A:");
string strNumberA = Console.ReadLine();
Console.Write("請選擇運算符號(+、-、*、/):");
string strOperate = Console.ReadLine();
Console.Write("請輸入數字B:");
string strNumberB = Console.ReadLine();
string strResult = "";
switch (strOperate)
{
case "+":
strResult = Convert.ToString(Convert.ToDouble(strNumberA)
+ Convert.ToDouble(strNumberB));
break;
case "-":
strResult = Convert.ToString(Convert.ToDouble(strNumberA)
- Convert.ToDouble(strNumberB));
break;
case "*":
strResult = Convert.ToString(Convert.ToDouble(strNumberA)
* Convert.ToDouble(strNumberB));
break;
case "/":
if (strNumberB != "0")
strResult = Convert.ToString(Convert.ToDouble(strNumberA)
/ Convert.ToDouble(strNumberB));
else
strResult = "除數不能爲0";
break;
}
Console.WriteLine("結果是:" + strResult);
Console.ReadLine();
}
catch (Exception ex)
{
Console.WriteLine("您的輸入有錯:" + ex.Message);
}
}
}
大鳥:“吼吼,不錯,不錯,改得很快嘛?至少就目前代碼來說,實現計算器是沒有問題了,但這樣寫出的代碼是否合出題人的意思呢?”
小菜:“你的意思是面向對象?”
大鳥:“哈,小菜非小菜也!”
1.4 面向對象編程
小菜:“我明白了,他說用任意一種面嚮對象語言實現,那意思就是要用面向對象的編程方法去實現,對嗎?OK,這個我學過,只不過當時我沒想到而已。”
大鳥:“所有編程初學者都會有這樣的問題,就是碰到問題就直覺地用計算機能夠理解的邏輯來描述和表達待解決的問題及具體的求解過程。這其實是用計算機的方式去思考,比如計算器這個程序,先要求輸入兩個數和運算符號,然後根據運算符號判斷選擇如何運算,得到結果,這本身沒有錯,但這樣的思維卻使得我們的程序只爲滿足實現當前的需求,程序不容易維護,不容易擴展,更不容易複用。從而達不到高質量代碼的要求。”
小菜:“鳥哥呀,我有點糊塗了,如何才能容易維護,容易擴展,又容易複用呢,能不能具體點?”
1.5 活字印刷,面向對象
大鳥:“這樣吧,我給你講個故事。你就明白了。”
“話說三國時期,曹操帶領百萬大軍攻打東吳,大軍在長江赤壁駐紮,軍船連成一片,眼看就要滅掉東吳,統一天下,曹操大悅,於是大宴衆文武,在酒席間,曹操詩性大發,不覺吟道:‘喝酒唱歌,人生真爽。……’。衆文武齊呼:‘丞相好詩!’於是一臣子速命印刷工匠刻版印刷,以便流傳天下。”
“樣張出來給曹操一看,曹操感覺不妥,說道:‘喝與唱,此話過俗,應改爲‘對酒當歌’較好!’,於是此臣就命工匠重新來過。工匠眼看連夜刻版之工,徹底白費,心中叫苦不迭。只得照辦。”
“樣張再次出來請曹操過目,曹操細細一品,覺得還是不好,說:‘人生真爽太過直接,應改問語纔夠意境,因此應改爲‘對酒當歌,人生幾何?……’當臣轉告工匠之時,工匠暈倒……!”
“小菜你說,這裏面問題出在哪裏?”大鳥問道。
小菜說:“是不是因爲三國時期活字印刷還未發明,所以要改字的時候,就必須要整個刻板全部重新刻。”
大鳥:“說得好!如果是有了活字印刷,則只需更改四個字就可,其餘工作都未白做。豈不妙哉。”
“第一,要改,只需更改要改之字,此爲可維護;第二,這些字並非用完這次就無用,完全可以在後來的印刷中重複使用,此乃可複用;第三,此詩若要加字,只需另刻字加入即可,這是可擴展;第四,字的排列其實可能是豎排可能是橫排,此時只需將活字移動就可做到滿足排列需求,此是靈活性好。”
“而在活字印刷術出現之前,上面的四種特性都無法滿足,要修改,必須重刻,要加字,必須重刻,要重新排列,必須重刻,印完這本書後,此版已無任何可再利用價值。”
小菜:“是的,小時候,我一直奇怪,爲何火藥、指南針、造紙術都是從無到有,從未知到發現的偉大發明,而活字印刷僅僅是從刻版印刷到活字印刷的一次技術上的進步,爲何不是評印刷術爲四大發明之一呢?原來活字印刷的成功是這個原因。”
1.6 面向對象的好處
大鳥:“哈,這下你明白了?我以前也不懂,不過做了軟件開發幾年後,經歷了太多的類似曹操這樣的客戶要改變需求,更改最初想法的事件,才逐漸明白當中的道理。其實客觀地說,客戶的要求也並不過份,不就是改幾個字嗎,但面對已完成的程序代碼,卻是需要幾乎重頭來過的尷尬,這實在是痛苦不堪。說白了,原因就是因爲我們原先所寫的程序,不容易維護,靈活性差,不容易擴展,更談不上覆用,因此面對需求變化,加班加點,對程序動大手術的那種無奈也就成了非常正常的事了。之後當我學習了面向對象的分析設計編程思想,開始考慮通過封裝、繼承、多態把程序的耦合度降低,傳統印刷術的問題就在於所有的字都刻在同一版面上造成耦合度太高所致,開始用設計模式使得程序更加的靈活,容易修改,並且易於複用。體會到面向對象帶來的好處,那種感覺應該就如同是一中國酒鬼第一次喝到了茅臺,西洋酒鬼第一次喝到了XO一樣,怎個爽字可形容呀。”
“是呀是呀,你說得沒錯,中國古代的四大發明,另三種應該都是科技的進步,偉大的創造或發現。而唯有活字印刷,實在是思想的成功,面向對象的勝利。”小菜也興奮起來:“你的意思是,面試公司出題的目的是要我寫出容易維護,容易擴展,又容易複用的計算器程序?那該如何做呀?”
1.7 複製vs.複用
大鳥:“比如說,我現在要求你再寫一個Windows的計算器,你現在的代碼能不能複用呢?”
小菜:“那還不簡單,把代碼複製過去不就行了嗎?改動又不大,不算麻煩。”
大鳥:“小菜看來還是小菜呀,有人說初級程序員的工作就是Ctrl+C和Ctrl+V,這其實是非常不好的編碼習慣,因爲當你的代碼中重複的代碼多到一定程度,維護的時候,可能就是一場災難。越大的系統,這種方式帶來的問題越嚴重,編程有一原則,就是用盡可能的辦法去避免重複。想想看,你寫的這段代碼,有哪些是和控制檯無關的,而只是和計算器有關的?”
小菜:“你的意思是分一個類出來?哦,對的,讓計算和顯示分開。”
1.8 業務的封裝
大鳥:“準確地說,就是讓業務邏輯與界面邏輯分開,讓它們之間的耦合度下降。只有分離開,纔可以達到容易維護或擴展。”
小菜:“讓我來試試看。”
Operation運算類
public class Operation
{
public static double GetResult(double numberA, double numberB, string operate)
{
double result = 0d;
switch (operate)
{
case "+":
result = numberA + numberB;
break;
case "-":
result = numberA - numberB;
break;
case "*":
result = numberA * numberB;
break;
case "/":
result = numberA / numberB;
break;
}
return result;
}
}
客戶端代碼
static void Main(string[] args)
{
try
{
Console.Write("請輸入數字A:");
string strNumberA = Console.ReadLine();
Console.Write("請選擇運算符號(+、-、*、/):");
string strOperate = Console.ReadLine();
Console.Write("請輸入數字B:");
string strNumberB = Console.ReadLine();
string strResult = "";
strResult = Convert.ToString(Operation.GetResult(Convert.ToDouble(strNumberA),
Convert.ToDouble(strNumberB), strOperate));
Console.WriteLine("結果是:" + strResult);
Console.ReadLine();
}
catch (Exception ex)
{
Console.WriteLine("您的輸入有錯:" + ex.Message);
}
}
小菜:“鳥哥,我寫好了,你看看!”
大鳥:“孺鳥可教也,寫得不錯,這樣就完全把業務和界面分離了。”
小菜心中暗罵:“你纔是鳥呢。”口中說道:“如果你現在要我寫一個Windows應用程序的計算器,我就可以複用這個運算類(Operation)了。”
大鳥:“不單是Windows程序,Web版程序需要運算可以用它,PDA、手機等需要移動系統的軟件需要運算也可以用它呀。”
小菜:“哈,面向對象不過如此。下回寫類似代碼不怕了。”
大鳥:“別急,僅此而已,實在談不上完全面向對象,你只用了面向對象三大特性中的一個,還有兩個沒用呢?”
小菜:“面向對象三大特性不就是封裝、繼承和多態嗎,這裏我用到的應該是封裝。這還不夠嗎?我實在看不出,這麼小的程序如何用到繼承。至於多態,其實我一直也不太瞭解它到底有什麼好處,如何使用它。”
大鳥:“慢慢來,要學的東西多着呢,你好好想想該如何應用面向對象的繼承和多態。”
1.9 緊耦合vs.松耦合
第二天。
小菜問道:“你說計算器這樣的小程序還可以用到面向對象三大特性?繼承和多態怎麼可能用得上,我實在不能理解。”
大鳥:“小菜很有鑽研精神嘛,好,今天我讓你功力加深一級。你先要考慮一下,你昨天寫的這個代碼,能否做到很靈活的可修改和擴展呢?”
小菜:“我已經把業務和界面分離了呀,這不是很靈活了嗎?”
大鳥:“那我問你,現在如果我希望增加一個開根(sqrt)運算,你如何改?”
小菜:“那隻需要改Operation類就行了,在switch中加一個分支就行了。”
大鳥:“問題是你要加一個平方根運算,卻需要讓加減乘除的運算都得來參與編譯,如果你一不小心,把加法運算改成了減法,這豈不是大大的糟糕。打個比方,如果現在公司要求你爲公司的薪資管理系統做維護,原來只有技術人員(月薪),市場銷售人員(底薪+提成),經理(年薪+股份)三種運算算法,現在要增加兼職工作人員(時薪)的算法,但按照你昨天的程序寫法,公司就必須要把包含原三種算法的運算類給你,讓你修改,你如果心中小算盤一打,‘TMD,公司給我的工資這麼低,我真是鬱悶,這下有機會了’,於是你除了增加了兼職算法以外,在技術人員(月薪)算法中寫了一句
if (員工是小菜)
{
salary = salary* 1.1;
}
那就意味着,你的月薪每月都會增加10%(小心被抓去坐牢),本來是讓你加一個功能,卻使得原有的運行良好的功能代碼產生了變化,這個風險太大了。你明白了嗎?”
小菜:“哦,你的意思是,我應該把加減乘除等運算分離,修改其中一個不影響另外的幾個,增加運算算法也不影響其他代碼,是這樣嗎?”
大鳥:“自己想去吧,如何用繼承和多態,你應該有感覺了。”
小菜:“OK,我馬上去寫。”
Operation運算類
public class Operation
{
private double _numberA = 0;
private double _numberB = 0;
public double NumberA
{
get { return _numberA; }
set { _numberA = value; }
}
public double NumberB
{
get { return _numberB; }
set { _numberB = value; }
}
public virtual double GetResult()
{
double result = 0;
return result;
}
}
加減乘除類
class OperationAdd : Operation
{
public override double GetResult()
{
double result = 0;
result = NumberA + NumberB;
return result;
}
}
class OperationSub : Operation
{
public override double GetResult()
{
double result = 0;
result = NumberA - NumberB;
return result;
}
}
class OperationMul : Operation
{
public override double GetResult()
{
double result = 0;
result = NumberA * NumberB;
return result;
}
}
class OperationDiv : Operation
{
public override double GetResult()
{
double result = 0;
if (NumberB==0)
throw new Exception("除數不能爲0。");
result = NumberA / NumberB;
return result;
}
}
小菜:“大鳥哥,我按照你說的方法寫出來了一部分,首先是一個運算類,它有兩個Number屬性,主要用於計算器的前後數,然後有一個虛方法GetResult(),用於得到結果,然後我把加減乘除都寫成了運算類的子類,繼承它後,重寫了GetResult()方法,這樣如果要修改任何一個算法,就不需要提供其他算法的代碼了。但問題來了,我如何讓計算器知道我是希望用哪一個算法呢?”
1.10 簡單工廠模式
大鳥:“寫得很不錯嘛,大大超出我的想象了,你現在的問題其實就是如何去實例化對象的問題,哈,今天心情不錯,再教你一招‘簡單工廠模式’,也就是說,到底要實例化誰,將來會不會增加實例化的對象,比如增加開根運算,這是很容易變化的地方,應該考慮用一個單獨的類來做這個創造實例的過程,這就是工廠,來,我們看看這個類如何寫。”
簡單運算工廠類
public 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;
}
}
大鳥:“哈,看到了吧,這樣子,你只需要輸入運算符號,工廠就實例化出合適的對象,通過多態,返回父類的方式實現了計算器的結果。”
客戶端代碼
Operation oper;
oper = OperationFactory.createOperate(“+”);
oper.NumberA = 1;
oper.NumberB = 2;
double result = oper.GetResult();
大鳥:“哈,界面的實現就是這樣的代碼,不管你是控制檯程序,Windows程序,Web程序,PDA或手機程序,都可以用這段代碼來實現計算器的功能,如果有一天我們需要更改加法運算,我們只需要改哪裏?”
小菜:“改OperationAdd 就可以了。”
大鳥:“那麼我們需要增加各種複雜運算,比如平方根,立方根,自然對數,正弦餘弦等,如何做?”
小菜:“只要增加相應的運算子類就可以了呀。”
大鳥:“嗯?夠了嗎?”
小菜:“對了,還需要去修改運算類工廠,在switch中增加分支。”
大鳥:“哈,那纔對,那如果要修改界面呢?”
小菜:“那就去改界面呀,關運算什麼事呀。”
大鳥:“我們來看看這幾個類的結構圖。”
1.11 UML類圖
小菜:“對了,我時常在一些技術書中看到這些類圖表示,簡單的還看得懂,有些標記我很容易混淆。要不你給我講講吧。”
大鳥:“這個其實多看多用就熟悉了。我給你舉一個例子,來看這樣一幅圖,其中就包括了UML類圖中的基本圖示法。”
UML類圖圖示樣例
大鳥:“首先你看那個‘動物’矩形框,它就代表一個類(Class)。類圖分三層,第一層顯示類的名稱,如果是抽象類,則就用斜體顯示。第二層是類的特性,通常就是字段和屬性。第三層是類的操作,通常是方法或行爲。注意前面的符號,‘+’表示public,‘-’表示private,‘#’表示protected。”
大鳥:“然後注意左下角的‘飛翔’,它表示一個接口圖,與類圖的區別主要是頂端有<<interface>>顯示。第一行是接口名稱,第二行是接口方法。接口還有另一種表示方法,俗稱棒棒糖表示法,就是唐老鴨類實現了‘講人話’的接口。”
小菜:“爲什麼要是‘講人話’?”
大鳥:“鴨子本來也有語言,只不過只有唐老鴨是能講人話的鴨子。”
小菜:“有道理。”
大鳥:“接下來就可講類與類,類與接口之間的關係了。你可首先注意動物、鳥、鴨、唐老鴨之間關係符號。”
小菜:“明白了,它們都是繼承的關係,繼承關係用空心三角形+實線來表示。”
大鳥:“我舉的幾種鳥中,大雁是最能飛的,我讓它實現了飛翔接口。實現接口用空心三角形+虛線來表示。”
|
||||
|
||||
大鳥:“你看企鵝和氣候兩個類,企鵝是很特別的鳥,會遊不會飛。更重要的是,它與氣候有很大的關聯。我們不去討論爲什麼北極沒有企鵝,爲什麼它們要每年長途跋涉。總之,企鵝需要‘知道’氣候的變化,需要‘瞭解’氣候規律。當一個類‘知道’另一個類時,可以用關聯(association)。關聯關係用實線箭頭來表示。”
class Penguin : Bird
{
private Climate climate;
大鳥:“我們再來看大雁與雁羣這兩個類,大雁是羣居動物,每隻大雁都是屬於一個雁羣,一個雁羣可以有多隻大雁。所以它們之間就滿足聚合(Aggregation)關係。聚合表示一種弱的‘擁有’關係,體現的是A對象可以包含B對象,但B對象不是A對象的一部分[DPE](DPE表示此句摘自《設計模式》(第2版),詳細摘要說明見附錄二)。聚合關係用空心的菱形+實線箭頭來表示。”
class WideGooseAggregate
{
private WideGoose[] arrayWideGoose;
}
大鳥:“合成(Composition,也有翻譯成‘組合’的)是一種強的‘擁有’關係,體現了嚴格的部分和整體的關係,部分和整體的生命週期一樣[DPE]。在這裏鳥和其翅膀就是合成(組合)關係,因爲它們是部分和整體的關係,並且翅膀和鳥的生命週期是相同的。合成關係用實心的菱形+實線箭頭來表示。另外,你會注意到合成關係的連線兩端還有一個數字‘1’和數字‘2’,這被稱爲基數。表明這一端的類可以有幾個實例,很顯然,一個鳥應該有兩隻翅膀。如果一個類可能有無數個實例,則就用‘n’來表示。關聯關係、聚合關係也可以有基數的。”
class Bird
{
private Wing wing;
public Bird()
{
wing = new Wing();
}
}
大鳥:“動物幾大特徵,比如有新陳代謝,能繁殖。而動物要有生命力,需要氧氣、水以及食物等。也就是說,動物依賴於氧氣和水。他們之間是依賴關係(Dependency),用虛線箭頭來表示。”
abstract class Animal
{
public Metabolism (Oxygen oxygen,Water water)
{
}
}
小菜:“啊,看來UML類圖也不算難呀。回想那天我面試題寫的代碼,我終於明白我爲什麼寫得不成功了,原來一個小小的計算器也可以寫出這麼精彩的代碼,謝謝大鳥。”
大鳥:“吼吼,記住哦,編程是一門技術,更加是一門藝術,不能只滿足於寫完代碼運行結果正確就完事,時常考慮如何讓代碼更加簡練,更加容易維護,容易擴展和複用,只有這樣纔可以真正得到提高。寫出優雅的代碼真的是一種很爽的事情。UML類圖也不是一學就會的,需要有一個慢慢熟練的過程。所謂學無止境,其實這纔是理解面向對象的開始呢。”
轉載地址:http://www.kuqin.com/design-patterns/20080106/3490.html#source