2020年11月28日

設計模式|利用策略模式 Strategy Pattern 簡化複雜的電商結帳流程

想像一下:

隨著公司的電商系統生意越做越大,支援的付款方式越來越多,負責處理訂單的OrderManager行數也越來越多,程式碼充滿了層層疊疊的if-else,老闆上周開會時又提出要新增其他付款方式,眼看程式碼就要爆炸,有沒有什麼可以簡化程式的方法?

Photo by Adi Goldstein on Unsplash

準備重構

(NOTE: 本篇範例程式碼皆為PHP)

我們先來看看原本的程式:

class OrderManager
{
    public function pay($payWay)
    {
        // ... 處理其他事情
    
        if ($payWay == 'CreditCard') {
            $payResult = $this->payByCreditCard();
            
        } else if ($payWay == 'atm') {
            $payResult = $this->payByAtm();
            
        } else if ($payWay == 'linePay') {
            $payResult = $this->payByLinePay();
        }
       
       // ... 處理其他事情
    }
    
    public function payByCreditCard()
    {
        // 處理信用卡付款....
    }

    public function payByAtm()
    {
        // 處理ATM付款....
    }

    public function payByLinePay()
    {
        // 處理LinePay付款....
    }
}

隨著公司業務的發展,付款方式越來越多,payByCreditCardpayByAtm等處理付款的程式,一隻一隻的增加,原本看起來結構還不錯的OrderManager也越來越長,越來越複雜。

於是,再新增新的付款方式之前,是重構的時候了。

仔細觀察後,我們發現,付款的流程其實都差不多。不論是信用卡、ATM或是LinePay付款,都可以歸納成兩步驟,付款取得付款結果

根據這個結論,我們可以建立一個Interface

interface PayMethodInterface
{
  	// 付款
    public function pay($orderInfo);
    
    // 取得付款結果
    public function getResult();
}

接著我們將原本放在OrderManager中的付款程式碼,移到另一個實作PayMethodInterFace的類別中,以信用卡(payByCreditCard)為例:

class CreditCard implements PayMethodInterface
{
     // 付款
    public function pay($orderInfo)
    {
        // 呼叫api執行付款...
    }
    
    // 取得付款結果
    public function getResult()
    {
        // 回傳付款結果
    }
}

重構

將付款程式都搬移之後,我們就可以開始改寫原本的OrderManager了。

要做的事情很簡單,就是將原本的payByXXX都移除掉,並修改pay method,這個pay需要傳入一個實作PayMethodInterface介面的物件:

class OrderManager
{
    public function pay(PayMethodInterface $payMethod)
    {
        $payMethod->pay($orderInfo);
        return $payMethod->getResult();
    }
}

這樣的意思是,以後的付款行為,都會藉由實作PayMethodInterface介面的物件來處理,如果今天傳入pay的是CreditCard物件,那麼這段程式就會自動去使用CreditCard物件來做信用卡付款。

新增付款方式

由於之前的努力,當老闆要我們再新增一個付款方式,我們不必再擴充原本的OrderManager,只要新增另一個實作PayMethodInterFace的類別即可。

例如,要新增Apple Pay,只要新增一個繼承PayMethodInterFaceApplePay物件,就可以直接丟給OrderManager來做付款了:

$orderManager = new OrderManager();
$orderManager->pay(new ApplePay);

結論

前面所舉的例子,就是Strategy Pattern的應用。

讓我們回到比較制式化的說法,根據GoF的物件導向設計模式(Design Patterns)中關於的Strategy Pattern的說法:

定義一整族演算法,將每一個演算法封裝起來,可互換使用,更可在不影響外界的情況下,個別抽換所引用的演算法。

前面示範的CreditCardApplePay等實作PayMethodInterFace的類別們,就是所謂的一整族演算法,它們將各種付款行為封裝起來,可以在不影響OrderManager的情況下,根據不同情境使用不同的付款方式。

其實這也做到了SOLID原則中的O,開放封閉原則The Open-Closed Principle,不需要修改重構後的OrderManager,就可以透過注入不同的PayMethod物件,來改變OrderManager的行為。

後話

之前在重構公司的結帳流程時,確實利用了這樣的方法,讓程式碼化簡,也更好維護了。雖然要將原本混亂的程式碼整理出一套邏輯很費神,而且重構也有一定的風險,但為了自己日後上班的心情(減少bug跟加班XD),還有公司業務能不能順利推廣,我認為是值得的投資。

參考