高質量的程式碼 - 函式(1)

xdaccount發表於2020-11-14

       從這一篇開始我們將主要討論函式。我們首先從一個問題開始,我們為什麼需要函式?或是函式能給我們帶來什麼好處?下面的列表是我基於前人的經驗所理解的函式的好處:

  • 降低複雜度
  • 引入易於理解的抽象層
  • 封裝變化
  • 避免重複程式碼
  • 簡化複雜的條件表示式
  • ....

降低複雜度

       函式為什麼能夠降低複雜度呢?首先我們來看看我們是如何解決複雜問題的。我們在解決複雜問題的時候,常用的也是最有效的方法是:將複雜的問題分解成相對簡單的多個子問題。有時我們甚至會不斷的重複這個過程(即將子問題繼續分解成更小更簡單的問題),直至最小的問題能夠被我們相對容易的理解和解決。最小子問題被解決之後,就可以被當作一個個的模組或黑盒子直接拿來用於解決相對複雜的父問題。

       這讓我想起了我們小時候學習數學的過程。老師首先會教我們一些最基本數學定律,這些定律很容易被理解和證明。隨著年級的升高,老師會教我們如何用這些最基本的數學定律去證明相對複雜一點的定律,這樣層層迭代就能證明很複雜的數學問題。同時很多時候在證明相對複雜定律的時候我們並不關心這些相對簡單定律的證明過程。

       那麼在軟體的世界裡對於一個很複雜的邏輯我們怎麼來降低複雜度呢?我想這個時候你一定會想到:子函式。將複雜的邏輯分解成多個相對簡單的邏輯,然後用子函式封裝這些相對簡單邏輯。

引入易於理解的抽象層

       子函式的作用其實是為我們引入了一層易於理解的抽象層。原來是一個很大的函式,下一層直接就是具體的複雜的程式碼。這就好像我們在學習複雜數學定律的時候,老師直接從證明簡單定律開始,一直證明至複雜的數學定律。這個證明過程一定會冗長及晦澀。

       子函式這時相當於是那些相對簡單的數學定律。一是這些子函式相對容易被實現(因為所對應的邏輯相對簡單);二是與基於簡單定律直接證明相對複雜的定律類似,基於子函式再實現父函式就會相對簡單。但這裡有一個非常重要的前提條件:為子函式取一個表達其作用或意圖的名字。不然我們只是引入了一個抽象層,但這個抽象層卻不好理解。

封裝變化

        函式還能夠為我們封裝變化。封裝什麼樣的變化呢?我們首先來看看函式到底有哪些部分組成。下圖1是一個典型的函式,它有兩個部分組成:函式簽名、函式體。函式簽名相當於是一個約定或是服務,而函式體是具體怎麼來履行這個約定。

        函式的呼叫者只能看到這個約定,他們無法看到函式是如何履行這個約定的。事實上函式的呼叫者也只關心這個約定。只要這個約定不被打破,那麼無論被呼叫函式如何改變其履行約定的方法,都不會影響到函式的呼叫者,這就將履行約定的變化封裝在約定的背後。

圖1

      下面的程式碼片段展示了封裝變化的作用。在版本1中ValueHolder直接將其成員變數公佈在外面,ValueCustomer直接可以給這個變數賦值。這時如果需求變了呢?如果Value有了取值範圍呢?這時我們就需要到ValueCustomer的ChangeState函式中修改程式碼。如果有100多個地方對Value成員變數賦值了呢?那意味著我們需要去100個地方修改程式碼。這是不是讓你想起了價值篇(TODO)中的圖1?當一個變化來的時候,我們需要對系統多個地方進行修改。

       我們再來看看版本2的實現。在版本2中Value成員變數變成私有變數,外部無法訪問這個成員變數,外部甚至都不知道有這個成員變數的存在。同時ValueHolder提供一個用於修改Value成員變數的UpdateValue公共方法。這個公共方法封裝瞭如何修改其內部成員變數的具體實現。對於ValueCustomer 類它可以通過呼叫這個方法,來讓ValueHolder修改其內部的狀態。當變化來的時候,我們只需要修改UpdateValue方法的實現。所有使用UpdateValue函式的地方都不會受到影響,因為函式簽名(或約定)沒有改變。

版本1

public class ValueHolder
{
    public int Value;
}

public class ValueCustomer
{
    private ValueHolder _valueHolder;

    public void ChangeState()
    {
        ....

        _valueHolder.Value = 10;

        ....
    }
}

版本2

public class ValueHolder
{
    private const int MinValue = 0;
    private const int MaxValue = 5;
    private int _value;

    public void UpdateValue(int newValue)
    {
        if (newValue < MinValue)
        {
            _value = MinValue;
        }
        else if (newValue > MaxValue)
        {
            _value = MaxValue;
        }
        else
        {
            _value = newValue;
        }
    }
}

public class ValueCustomer
{
    private ValueHolder _valueHolder;

    public void ChangeState()
    {

    ....

    _valueHolder.UpdateValue(10);

    ....

    }
}

避免重複程式碼

       重複程式碼是引起一個變化導致多處修改的重要原因,而函式可以用來封裝變化。這是為什麼我們需要將重複程式碼封裝在一個函式中。絕大部分的程式猿都知道DRY(Don't repeate yourself),都會有意識去消除重複程式碼。有時候你會發現過分的消除重複程式碼,會導致程式在結構上變得複雜或導致不合理的依賴。這時我們就要問自己:消除重複程式碼是我們的目標嗎?消除重複程式碼只是我們的手段,降低整個軟體生命週期的成本才是我們的目標。所以有時候還需要平衡重複程式碼和結構清晰。我想這也是為什麼有人說程式設計是門藝術。

簡化複雜的條件表示式

        函式還可以用來簡化複雜的條件表示式。簡化條件表達其實是函式降低複雜度的一個具體的例子。我們可以通過將複雜的條件表達封裝在函式簽名的背後,然後為函式取一個能夠表達這個複雜條件表示式的目的或意圖的名字,這個函式的名字通常來自於軟體所應用的領域。

這時候函式和複雜條件表示式之間的關係就好像記憶體和硬碟的關係。從硬碟中獲取資料代價相對較大,那麼我們就是把硬碟的資料先讀入記憶體,下次直接從記憶體中讀取資料,這可以有效的提高讀取效。封裝複雜條件表示式的那個函式就是被載入到記憶體中的資料。

       這一篇我們主要是嘗試著回答一個問題:我們為什麼需要函式?我們從幾個不同的角度去審視函式的價值。但這些內容相對有些抽象,在接下里的文章中我們將通過一些具體的例子來理解函式這些作用。

相關文章