iT邦幫忙

2026 iThome 鐵人賽

DAY 2
0
IT Operation

AI 輔助開發下,測試如何保住品質防線系列 第 2

Day 02:12 個付款方式類別,怎麼用 14 個 Trait 組出來

  • 分享至 

  • xImage
  •  

前言:付款方式一多,欄位要怎麼組

「同一個 PurchaseRequest,要同時支援信用卡、ATM、超商代碼、無卡分期、電子發票,這麼多種欄位組合,是不是要寫一堆 if 判斷付款方式,然後每種都塞一大包參數?」

omnipay-ecpay 這個套件的做法不是這樣。它把「一種付款方式需要哪些欄位」拆成一個個 Trait,再讓 PurchaseRequest 這個類別 use 它需要的那幾個,最後用一個方法把它們依「使用者選的付款方式」動態組裝起來。今天就把這個設計攤開來看。

今日目標

  • 看懂這個套件怎麼用 Trait 組合欄位,而不是用繼承或大量 if
  • 知道 getSendExtend() 這個組裝方法怎麼依 ChoosePayment 決定要不要帶某組欄位
  • 理解這種設計的取捨:什麼時候值得拆 Trait,什麼時候只是徒增檔案數
  • 對照一組「不拆 Trait」的反例,感受兩者可讀性/可測試性的差異

拆出來的 14 個 Trait,各自負責什麼

src/Traits/ 底下有 14 個檔案,光看名字就能猜到分工:

HasATMFields.php                  ATM 專屬欄位
HasATMOrCVSOrBARCODEFields.php    ATM/超商代碼/條碼共用欄位
HasCreditFields.php               信用卡分期、定期定額、銀聯卡等欄位
HasCVSOrBARCODEFields.php         超商代碼/條碼共用欄位
HasInvoiceFields.php              電子發票欄位
HasECPay.php                      包裝官方 SDK 的共用邏輯(明天會細講)
...(其餘 8 個負責金額、預設值、商店代號等更基礎的欄位)

例如 HasATMFields 只負責一件事——ATM 繳費期限:

trait HasATMFields
{
    public function setExpireDate($value)
    {
        return $this->setParameter('ExpireDate', $value);
    }

    public function getExpireDate()
    {
        return $this->getParameter('ExpireDate') ?: 3;
    }
}

HasCreditFields 則負責信用卡相關的一大票欄位(分期期數、定期定額金額/週期、記憶卡號、銀聯卡選項……),每個 setter/getter 都配了中文註解說明綠界要求的參數規則。

PurchaseRequest 怎麼把這些 Trait 組回來

class PurchaseRequest extends AbstractRequest
{
    use HasAmount;
    use HasATMFields;
    use HasATMOrCVSOrBARCODEFields;
    use HasCreditFields;
    use HasCustomFields;
    use HasCVSOrBARCODEFields;
    use HasDefaults;
    use HasInvoiceFields;
    use HasMerchantTradeNo;
    use HasSendFields;
    use HasStoreID;
    // ...
}

一個類別 use 了 11 個 Trait,這代表 PurchaseRequest 的實例上同時擁有所有付款方式的 setter/getter(setCreditInstallment()setExpireDate()setCustomerIdentifier() 全部都在)。但真正要送給綠界的資料,並不是把所有欄位全部塞進去——而是靠 getSendExtend() 依使用者選的 ChoosePayment 動態決定:

private function getSendExtend($sendFields)
{
    return array_merge(
        $this->getCreditFields($sendFields['ChoosePayment']),
        $this->getATMFields($sendFields['ChoosePayment']),
        $this->getCvsFields($sendFields['ChoosePayment']),
        $this->getBNPLFields($sendFields['ChoosePayment']),
        $this->getInvoiceFields($sendFields['InvoiceMark'])
    );
}

private function getATMFields($choosePayment)
{
    return in_array($choosePayment, ['ALL', 'ATM'], true) ? [
        'ExpireDate' => $this->getExpireDate(),
        'PaymentInfoURL' => $this->getPaymentInfoURL(),
        'ClientRedirectURL' => $this->getClientRedirectURL(),
    ] : [];
}

每個 getXxxFields() 私有方法只做一件事:判斷這次付款方式用不用得到這組欄位,用得到就回傳陣列,用不到就回傳空陣列,最後全部 array_merge 起來。

❌ vs ✅:把欄位判斷寫死在同一個大方法裡

❌ 反例:所有欄位判斷擠在一個方法裡
private function getSendExtend($sendFields)
{
    $extend = [];
    $payment = $sendFields['ChoosePayment'];

    if ($payment === 'ALL' || $payment === 'Credit') {
        $extend['CreditInstallment'] = $this->getCreditInstallment();
        $extend['InstallmentAmount'] = $this->getInstallmentAmount();
        $extend['Redeem'] = $this->getRedeem();
        // ...還有將近 10 個信用卡欄位
    }
    if ($payment === 'ALL' || $payment === 'ATM') {
        $extend['ExpireDate'] = $this->getExpireDate();
        $extend['PaymentInfoURL'] = $this->getPaymentInfoURL();
        // ...
    }
    // ATM、CVS、BNPL、Invoice 全部混在同一個方法、同一層縮排
    return $extend;
}
✅ 正例:每種付款方式各自一個小方法,用組合的方式接起來
private function getSendExtend($sendFields)
{
    return array_merge(
        $this->getCreditFields($sendFields['ChoosePayment']),
        $this->getATMFields($sendFields['ChoosePayment']),
        $this->getCvsFields($sendFields['ChoosePayment']),
        $this->getBNPLFields($sendFields['ChoosePayment']),
        $this->getInvoiceFields($sendFields['InvoiceMark'])
    );
}

正例的好處不是程式碼變短,而是每個付款方式的欄位規則可以被獨立測試、獨立修改。這也解釋了為什麼 tests/Message/PurchaseRequestTest.php 可以針對 CreditATMBNPL、彈性分期各寫一個獨立測試——測試結構直接對應著程式碼的拆分方式。程式碼怎麼拆,測試就會怎麼長;拆得好,測試自然就好寫。

這種設計的取捨

Trait 組合不是沒有代價。一個 PurchaseRequest 實例身上掛了 11 個 Trait 的方法,你光看類別簽章看不出它實際支援哪些付款方式的欄位——要嘛去翻 use 的清單,要嘉去翻 getSendExtend()。這也呼應昨天提到的:README 沒說清楚的東西,最後只能靠翻程式碼或翻測試補回來。

如果只有 2、3 種付款方式,這種拆法可能反而是過度設計;但當付款方式一路長到 5 種以上、欄位規則彼此獨立又各自複雜(像信用卡那組近 10 個參數),拆開來讓每組欄位各自可測試,會比一個愈長愈難讀的大方法更容易維護。該不該拆,要看的是「欄位規則彼此獨立的程度」,不是「類別數量看起來多不多」。

今日思考題

回頭看看你手上維護的專案,有沒有一個方法因為「要處理好幾種模式/類型」而越長越難讀?如果把它拆成 Trait 或小方法組合起來,你覺得會變得更好測,還是只是把複雜度搬到別的地方?

今日重點回顧

  • omnipay-ecpay 用 14 個 Trait 分別封裝各付款方式的欄位,PurchaseRequest 全部 use 進來
  • getSendExtend()ChoosePayment 動態決定要合併哪些欄位,每種付款方式各自一個私有方法
  • 程式碼怎麼拆,測試結構就會跟著長成什麼樣子
  • Trait 組合不是免費的:類別身上掛的方法變多,可讀性要靠測試跟命名補回來

明日預告

明天會看 HasECPay 這個 Trait,它包的不是欄位,而是綠界官方 SDK 本身——一個要給多人共用的套件,要怎麼把廠商自己發布的 SDK 介面,接進 Omnipay 統一的 Gateway/Request/Response 介面裡。


上一篇
Day 01:AI 幫你寫更多程式碼,但品質防線要自己蓋
下一篇
Day 03:把廠商官方 SDK,包進一個多人共用的套件慣例裡
系列文
AI 輔助開發下,測試如何保住品質防線5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言