許多開發者知道「如何寫介面」,卻不清楚「為什麼要寫介面」。本文透過一個具體的付款情境,示範介面如何實現解耦、定義契約、支援多型替換,並說明介面在團隊協作與架構演進中的核心價值。
系統需支援「現金」、「信用卡」、「Line Pay」等付款方式,未來可能新增「Apple Pay」、「比特幣」。若直接在主程式寫死 if ($type === 'cash') ...,每新增一種就得修改核心邏輯,違反開放封閉原則(OCP)。
// PayInterface.php
interface PayInterface
{
// 契約:所有付款方式必須實作 pay() 方法
public function pay(float $amount): bool;
// 可選:退款介面(另一個介面,遵循介面隔離原則 ISP)
// public function refund(string $orderId): bool;
}
// CashPay.php
class CashPay implements PayInterface
{
public function pay(float $amount): bool
{
echo "以現金支付 $amount 元";
return true;
}
}
// CreditCardPay.php
class CreditCardPay implements PayInterface
{
private $cardNumber;
public function __construct(string $cardNumber)
{
$this->cardNumber = $cardNumber;
}
public function pay(float $amount): bool
{
echo "以信用卡 {$this->cardNumber} 扣款 $amount 元";
// 呼叫金流 API...
return true;
}
}
// LinePay.php
class LinePay implements PayInterface
{
public function pay(float $amount): bool
{
echo "導向 Line Pay 支付 $amount 元";
// 產生付款連結、導向...
return true;
}
}
// OrderService.php
class OrderService
{
// 建構子注入:依賴抽象(介面),而非具體類別
public function __construct(private PayInterface $payment) {}
public function checkout(float $amount): void
{
// 業務邏輯:驗證庫存、建立訂單...
echo "處理訂單中...\n";
// 委派付款給實作類別,完全不需知道具體是哪一種
if ($this->payment->pay($amount)) {
echo "付款成功,訂單成立!\n";
} else {
throw new Exception('付款失敗');
}
}
}
// index.php
require_once 'vendor/autoload.php';
// 情境 A:使用者選擇現金
$service = new OrderService(new CashPay());
$service->checkout(1500);
// 情境 B:使用者選擇信用卡
$service = new OrderService(new CreditCardPay('4242-4242-4242-4242'));
$service->checkout(3000);
// 情境 C:從設定檔/資料庫動態決定
$paymentClass = $_ENV['DEFAULT_PAYMENT'] ?? 'CashPay'; // 例如 'LinePay'
$payment = new $paymentClass();
$service = new OrderService($payment);
$service->checkout(500);
| 核心價值 | 說明 | 上例體現 |
|---|---|---|
| 解耦 | 高層模組不依賴低層實作細節,雙方僅透過介面溝通 | OrderService 完全不知 CashPay/CreditCardPay 存在 |
| 契約 | 強制所有實作類別遵守相同方法簽名,編譯/執行期保證相容性 | 所有付款類別必須有 pay(float): bool |
| 多型 | 執行期可動態替換實作,支援策略模式、外掛架構、A/B 測試 | 同一 OrderService 可搭配任一付款方式 |
interface 可定義 public function defaultMethod(): void { ... } 提供預設實作,減少樣板程式碼。若付款方式「不支援退款」,不應強迫實作 refund()。拆分介面:
interface PayableInterface { public function pay(float $amount): bool; }
interface RefundableInterface { public function refund(string $orderId): bool; }
class CashPay implements PayableInterface { ... } // 僅實作 Payable
class CreditCardPay implements PayableInterface, RefundableInterface { ... } // 兩者皆實作
這樣 OrderService 僅依賴 PayableInterface,退款服務依賴 RefundableInterface,職責更單一。