憶起第一次執行自己寫的程式的感動,到現在都還記得,當時用的語言是 VB,在 IDE 上開了一個 Form 拉了一個 Button,按下去之後跳出 MsgBox 顯示 "Hello World",內心不斷地給自己鼓掌「哇!我也會寫程式了。」,至今有沒有曾經後悔過走這條路已經忘記了,但是程式設計「賜我吃、賜我穿、賜我借錢可以還。」是個事實,講這些跟這篇文章的主題有什麼關係?
在當我學到 If / Then / Else
跟 For / Next
的時候就覺得「哇!程式可以做好多事情哦。」,長大了才知道這個叫「結構化程式設計」,運用結構化程式設計的三種結構:Sequence
、Selection
、Repetition
,用一門有支援結構化的程式語言,在確認需求的可行性後幾乎沒有寫不出來的,我也就這樣無論是 VB、C#、JavaScript、PHP 總是抄著這三招一路過關斬將,將使用者的需求一一實現。
「物件導向程式設計」則是從我開始學習程式設計以來一直都會聽到的一個詞,但是就只是一個詞而已,因為即使我抄著支援物件導向的程式語言,我也只是用結構化程式設計的那三招在寫程式,就這麼 Coding 過了幾年後,無意中獲得了這本書,看了之後很有感,開始調整自己的程式設計思惟,而第一個改變的思惟就是「處理需求變化
」的思惟。
我用簡單計算營業稅的例子來說明,有一天老闆跟我說了個需求「Johnny,我們銷售產品的金額都是含稅的,麻煩把稅金給計算出來。」,哪有什麼問題,程式馬上就寫好了,我連四捨五入的問題都幫你考慮到了。
private static int GetTax(int price)
{
var tax = price - (price / 1.05d);
return Convert.ToInt32(Math.Round(tax, MidpointRounding.AwayFromZero));
}
交付之後老闆說了「Johnny 啊,公司的產品不是只有內銷,還有外銷日本及新加坡,日本稅金是 8%、新加坡是 7%。」,因此程式就又改了一個版本。
private static int GetTax(int price, string country)
{
var rate = 0.05d;
switch (country)
{
case "日本":
rate = 0.08;
break;
case "新加坡":
rate = 0.07;
break;
}
var tax = price - (price / (1d + rate));
return Convert.ToInt32(Math.Round(tax, MidpointRounding.AwayFromZero));
}
這樣子的需求如果改換物件導向程式設計會怎樣? 物件導向程式設計有三大特性:繼承、封裝、多型,既然是特性,我個人認為用物件導向設計出來的程式就應該要具有這三個特性,如果沒有就不是物件導向了,因此如果朝向物件導向設計出來的解決方案大致上會是這個樣子。
private static readonly Dictionary<string, Tax> Taxes =
new Dictionary<string, Tax>
{
{ "台灣", new TaiwanTax() },
{ "日本", new JapanTax() },
{ "新加坡", new SingaporeTax() },
};
private static int GetTax(int price, string country)
{
return Taxes[country].Calculate(price);
}
internal abstract class Tax
{
private readonly double rate;
protected Tax(double rate)
{
this.rate = rate;
}
public int Calculate(int price)
{
var tax = price - price / (1d + this.rate);
return Convert.ToInt32(Math.Round(tax, MidpointRounding.AwayFromZero));
}
}
internal class TaiwanTax : Tax
{
public TaiwanTax()
: base(0.05)
{
}
}
internal class JapanTax : Tax
{
public JapanTax()
: base(0.08)
{
}
}
internal class SingaporeTax : Tax
{
public SingaporeTax()
: base(0.07)
{
}
}
看到這裡我們可能會覺得多寫了好多程式碼,還多了一堆類別,然後感覺在脫褲子放屁,用物件導向程式設計好像也沒比較厲害啊! 沒錯,從表面上的程式碼行數來看,原本不到十幾行就能解決的事搞到五十幾行,而且真實世界要處理的需求比這個複雜多了,那愈複雜不就要增加愈多個類別、寫愈多行程式碼!?任誰來看都是一件沒什麼效益的事情,所以往往我們會選擇十幾行的那種設計方式。
偏偏我們又不擅長記憶這種邏輯狀態所堆疊起來的路徑,當我們在腦袋中一行一行執行我們寫的程式碼的時候,我們下的邏輯判斷愈多,所要記憶的邏輯狀態路徑也就愈深、愈廣,直到準確地找到了要增加(或修改)程式碼的地方,在此之前,只要稍微地一個中斷或是隔一段時日,我們就必須要在腦袋中重建原來的路徑,只要有會被影響的路徑沒有在我們的腦袋中被考慮到的話,Bug 就產生了,這也是很多系統的發展速度愈來愈慢的其中一個原因,因為改動 Code 成本愈來愈高,我們的腦袋就像挖礦機一樣,一直重複做著無效的運算,浪費時間也浪費我們的專注力。
我們來看一個科學一點的數據,雖然本篇文章範例的需求並不複雜,但是十幾行那個方法的循環複雜度就已經有 3 了(一般來講不要超過 10),而且複雜度還會隨著增加不同稅率而上升,後者雖然表面上的程式碼行數是五十幾行,但是沒有一個方法的循環複雜度超過 1,而且增加不同稅率複雜度仍能維持在 1。
這也使我對結構化程式設計中的條件判斷越來越戒慎恐懼,因為濫用的話,會讓原本簡單的東西變得複雜,沒有人(包括我)想要處理原本簡單,然後被搞得複雜的東西,要避免這種情況就是不要用條件判斷去寫程式,也就是說不要用 if / else 寫程式,很多人會有疑問「不用 if / else 的話那怎麼寫程式?」,不要誤會,這裡不是完全否定,而是在有其他選擇下思考其必要性,下語法之前想一下「這裡一定要用嗎?」、「有沒有更不複雜的寫法?」。
以上這只是個開始,如果這個開始對我們來說改變實在太大了,我完全用結構化程式設計來開發程式沒什麼問題、挺好的,而且樂在其中,那倒也 OK,因為解決使用者的問題才是最主要的,可是如果我們已經厭倦了在腦中堆疊條件判斷的狀態,不妨嘗試換個不同的程式設計方式。