DDD實戰:一段金流程序

最近開始研究DDD(Domain Driven Design),發現它確實是組織大型軟體架構出色又實際的方法。
可惜網路上只找得到一堆理論與說明,實際的範例code很少,我只好自己摸索了。
我會陸續將成果分享出來,希望對需要的人有所幫助。


需求

我公司串接由A公司提供之金流服務(第三方支付),A公司在確認收到顧客付款(信用卡、ATM轉帳)之後,會以HTTP POST通知我公司某設定好之URL。
由於可能有粗心大意之顧客,針對單筆訂單送出多筆交易、重複付款,因此需要在發現重複付款之時通知A公司放棄之前相關授權交易。

原解法

我公司有代表交易之trades資料表與代表訂單之orders資料表。
商業邏輯如下:
* 使用A公司SDK計算與驗證參數
* 從trades找到對應之交易並設為「已授權」
* 從orders找到對應之訂單並設為「已付款」

在MVC架構之下,我將其撰寫於某專責於金流交易之controller:

fetchParams();

        // 驗證失敗,參數並非A公司加密傳來
        if (!$params) {
            return '0|Fail';        
        }
         
        // 更新交易狀態
        $t = Trade::where('trade_no', $params['trade_no'])->first();
        $t->recieved_datetime = date('c');            
        $t->status = 1;
        $t->save();
        
        // 重複付款了,發送API通知A公司取消前幾筆交易
        if ($t->order->status == ORDER::PAID_STATUS){
            // blah blah
            // ...
            return '1|OK';            
        }
        
        // 更新訂單狀態
        $t->order->status = ORDER::PAID_STATUS;
        $t->order->save();
        
        return '1|OK';            
    }

}

問題:
* code可讀性不夠高
* 此controller action一次處理多件事情、分工不佳

反省

也許可將這串程序封裝成Trade類別底下的一個動作?


class SomeController extends BaseController {

    public function postPaymentReceive(){
        // A公司提供之SDK
        $sdk = new SDK();
        $params = $sdk->fetchParams();

        // 驗證失敗,參數並非A公司加密傳來
        if (!$params) {
            return '0|Fail';        
        }
        
        // 授權此筆交易
        // 內含交易狀態更新、訂單狀態更新、重複扣款通知
        $t = Trade::where('trade_no', $params['trade_no'])->first();
        $t->authenticate($params);
                
        return '1|OK';            
    }

}

問題:
* 只是將code由controller移進model,所有問題都依舊存在

在MVC架構之下,只追求’thin controller, fat model’會很容易將軟體架構只改善到這個階段。

DDD觀點

一個「交易」實體本身不會「授權」自己,應該由「某個程序」去進行「授權」一個「交易」實體的動作。
也就是說這行code不合理:

$t->authenticate();

它將原本Trade類別的意義由「一筆交易擁有的屬性、行為」擴大去包含了「會對一筆交易施加的行為」。
就算改寫為靜態函式依然擴大了原Trade類別的意義。
Trade類別的意義過廣會導致可讀性降低、不好理解、難維護。

DDD中關於一個service的判斷如下:
* 涉及到domain中的其他object
* stateless
* 涉及一個domain,通常不屬於一個entity或value object

我決定撰寫一個service類別專職處理這段接收通知的金流程序,並在controller中使用這個類別:


class SomeController extends BaseController {

    public function postPaymentReceive(){
        // A公司提供之SDK
        $sdk = new SDK();
        $params = $sdk->fetchParams();

        $service = new ReceivePaymentNotification();

        return $service->handle($params);        
    }

}
processParameters($params);

        return '1|OK';
    }
    
    public function processParameters($params)
    {        
        $t = Trade::where('trade_no', $params['trade_no'])->first();

        $this->processTransaction($t, $params);        

        $this->processOrder($t->order);        
    }

    public function processTransaction($t, $params)
    {
        $t->recieved_datetime = date('c');            
        $t->status = 1;
        $t->save();
    }
    
    public function processOrder($o)
    {

        // 重複付款了,發送API通知A公司取消前幾筆交易
        if ($o->status == ORDER::PAID_STATUS){
            // blah blah
            // ...
            return '1|OK';            
        }
        
        // 更新訂單狀態
        $o->status = ORDER::PAID_STATUS;
        $o->save();
    }
    
}

幾乎只是將原本的code剪下、貼上而已,卻花了我整個下午。

您覺得可讀性、可維護性有改善嗎?

期待您的寶貴意見,歡迎在下方給我feedback。