[創意料理] 學會物件導向程式設計從「處理需求變化」開始

憶起第一次執行自己寫的程式的感動,到現在都還記得,當時用的語言是 VB,在 IDE 上開了一個 Form 拉了一個 Button,按下去之後跳出 MsgBox 顯示 "Hello World",內心不斷地給自己鼓掌「哇!我也會寫程式了。」,至今有沒有曾經後悔過走這條路已經忘記了,但是程式設計「賜我吃、賜我穿、賜我借錢可以還。」是個事實,講這些跟這篇文章的主題有什麼關係?

在當我學到 If / Then / ElseFor / Next 的時候就覺得「哇!程式可以做好多事情哦。」,長大了才知道這個叫「結構化程式設計」,運用結構化程式設計的三種結構:SequenceSelectionRepetition,用一門有支援結構化的程式語言,在確認需求的可行性後幾乎沒有寫不出來的,我也就這樣無論是 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,因為解決使用者的問題才是最主要的,可是如果我們已經厭倦了在腦中堆疊條件判斷的狀態,不妨嘗試換個不同的程式設計方式。

相關資源

C# 指南
ASP.NET 教學
ASP.NET MVC 指引
Azure SQL Database 教學
SQL Server 教學
Xamarin.Forms 教學