2019年12月3日

開發 | Don't Repeat Yourself DRY 原則不是萬靈丹 | 不要為了重複而重複

這個東西可以共用,我們抽出來吧!

當我們發現程式中,有很多長得很像的部分,我們通常會開始思考,如何把這些東西整理出來。

畢竟著名的DRY原則,告訴我們:不要重複你自己。

於是我們學習使用function或class來解決相同程式重複撰寫的問題。

透過物件導向開發的方式,來減少重複的程式碼,後來更是衍生出各種設計模式(design pattern)。

聽起來很棒,但若是無腦的將所有看起來很像的東西,通通抽出來,那其實會造成一些潛在的問題。

比如說,有些時候看起來重複的程式,其實不適合整合成同一個function或class中的method。為什麼?因為他們可能只是現在看起來一樣,其實在處理的是不同問題,或是在本質上有所差異。

現實世界

在一個購物平台網站,買家跟賣家都可以註冊帳號。因為一開始系統很簡單,所以兩者的行為可能是差不多的,所以工程師決定簡單地新增一個共用的註冊程式。

/**
 * 註冊帳號 
 *
 * @param $identity 身份,buyer或seller
 * @param $account  帳號 
 * @param $password 密嗎
 */
function register($identity, $account, $password)
{
    // 寫入db
    insert_into_member_table($identity, $account, $password);
}

過了不久,PM說因為法規要求,賣家要提供營業地址。於是為了繼續共用程式,工程師將register()調整成:

/**
 * 註冊帳號 
 *
 * @param $identity 身份,buyer或seller
 * @param $account  帳號 
 * @param $password 密嗎
 * @param $address  營業地址
 */
function register($identity, $account, $password, $address)
{
    // 買家
    if ($identity == 'buyer') {
        $address = null;
    }
    
    // 寫入db
    insert_into_member_table($identity, $account, $password, $address);
}

又過了不久,PM說我們要將賣家分級,根據賣家等級,建立不同的賣場(store)。為了程式共用...

/**
 * 註冊帳號 
 *
 * @param $identity 身份,buyer或seller
 * @param $account  帳號 
 * @param $password 密嗎
 * @param $address  營業地址
 * @param $level    等級
 */
function register($identity, $account, $password, $address, $level)
{
    // 買家
    if ($identity == 'buyer') {
        $address = null;
    
    // 賣家
    } else {

        // 超級賣家
        if ($level == 'super') {
            create_super_store($account);
        
        // 普通賣家
        } else {
            create_normal_store($account);
        }
    }
    

    // 寫入db
    insert_into_member_table($identity, $account, $password, $address);

}

好,又過了不久...PM說商家也要分等級了!為了給高等級的商家有更好的服務,高等商家可以填寫地址,公司會不定期寄送贈品到指定地址。那原本賣家用到的$level跟$address看起來可以重複使用,為了不要新增參數,就沿用吧...

/**
 * 註冊帳號 
 *
 * @param $identity 身份,buyer或seller
 * @param $account  帳號 
 * @param $password 密嗎
 * @param $address  營業地址或居住地址
 * @param $level    賣家等級或商家等級
 */
function register($identity, $account, $password, $address, $level)
{

    // 買家
    if ($identity == 'buyer') {

        // 不是VIP會員不用填地址
        if ($level != 'vip') {
            $address = null;
        }
    
    // 賣家
    } else {

        // 超級賣家
        if ($level == 'super') {
            create_super_store($account);
        
        // 普通賣家
        } else {
            create_normal_store($account);
        }
    }
    

    // 寫入db
    insert_into_member_table($identity, $account, $password, $address);
}

ok...又隔了不久,PM說....工程師立刻答應,「好的沒問題,只要在register加上...」

等一下。

你確定繼續擴充這支程式沒問題嗎?

如果我們先檢視一下目前的程式,可以發現已經有一些很明顯的問題存在了:

  1. 各種買家、賣家獨有的判斷,導致if/else不斷增生,第一時間難以讀懂這支程式,$address的value到底是什麼?(可讀性不佳、維護性不佳)
  2. function的參數越來越不精準,有多重涵義(interface不明),無法透過程式介面大概猜測出程式的行為。(可讀性不佳)
  3. 賣場(store)只有賣家有,卻出現在一支名為register的function裡面(不符合單一職責原則)
  4. 只加上買家或賣家的行為,都要動到這支程式,必須要很小心或經過測試才知道有沒有影響到另一方的行為(擴充性不佳、維護性不佳、不符合單一職責原則)
  5. ...

繼續在這樣的架構上擴充程式,絕對會長成一個危樓...。

那...難道不共用/複用程式嗎?

當然不是,而是說我們應該要適時地調整程式架構(refactor),當發現程式出現分歧,或是開始難以維護或理解的時候,就不應該再用錯誤的方式重複使用程式(reuse)。

比如說上面的例子,如果對於商業邏輯的認知足夠的話,一開始就可以把註冊程式區分為registerSeller()registerBuyer(),並且將賣家的賣場store部分切離出去:

// 賣家註冊
function registerSeller($account, $password, $address)
{
    // 寫入db
    insert_into_member_table('seller', $account, $password, $address);
}

// 買家註冊
function registerBuyer($account, $password, $level, $address = null)
{

    // 不是VIP會員不用填地址
    if ($level != 'vip') {
        $address = null;
    }

    // 寫入db
    insert_into_member_table('buyer', $account, $password, $address);
}

雖然看起來重複程式了,但其實仔細想想,買家和賣家其實是兩種不同的身份,隨著平台的發展,行為可能會有很大的不同,未來程式有機會擴充成很不同的樣子,一開始如果沒有想清楚,為了共用而共用,並且按照這樣的邏輯發展下去,其實就只是把複雜的東西塞到一支萬能function或class裡面而已,雖然達到了reuse,但卻降低了程式的可讀性、維護性還有擴充性,這並不是真正的reuse。

結論

共用某些東西之前,多思考三秒鐘,確認它們:

  1. 這個類別、函數、參數名稱真的有相同意義嗎?
  2. 這段程式的行為在現實中所對應的概念是一致的嗎?
  3. 未來要擴充的話,可能的方向是?

那說了這麼多,結論大概是:

不要只想著要共用某隻 function 就把所有的東西往裡面塞 XD