用 PHP 解釋 SOLID 原則裡的「O」

SOLID 原則的 S,在上篇我們已經用 TypeScript 來介紹

用 TypeScript 解釋 SOLID 原則裡的「S」

本篇文章要來介紹開放封閉原則 (Open–closed principle,OCP)
也是 SOLID 原則中的 O

開放?封閉?這個原則從名字上來看好像無法猜到其含義
來看看原文怎麼說

Entities should be open for extension, but closed for modification
實體對於擴展應該是開放的,但對修改是封閉的

這裡的實體 (Entities) 可以指的是類別 (Class)、函式 (Function)、方法 (Method)
這句話是什麼意思?小弟在 Laracast 中聽到講師是這麼解釋的

Change behavior of entities without modifying source code
改變實體的行為,但不修改其程式碼

What!?

這要怎麼做到?別急,讓我們看看例子吧

假設我們是某間新創電商的後端工程師
某天我們接到一個需求,需要開發電商平台的支付功能,這個支付功能只需要支援信用卡就好
於是我們著手開發,測試無誤之後,上到正式台

<?php

class Payment
{
    public function pay(Receipt $receipt)
    {
        $this->payByCreditCard();
    }

    // 用信用卡付錢
    public function payByCreditCard()
    {
        // 實作業務邏輯
        ...
    }
}

過了一陣子,電商生意漸漸起色,只支援信用卡支付,顯然無法滿足日漸增多的消費者
在聽到消費者的反應之後,公司希望我們可以幫電商加入超商付款的功能
於是我們修改原本的程式碼

<?php

class Payment
{
    public function pay(Receipt $receipt)
    {
        // $receipt 多加一個 type 屬性作判斷 (這個方法其實不太好)
        if ($receipt->type === 'cash') {
            $this->payByCash($receipt);
        } else {
            $this->payByCreditCard($receipt);
        }
    }

    // 信用卡支付
    public function payByCreditCard($receipt)
    {
        // ...
    }

    // 超商付款
    public function payByCash($receipt)
    {
        // ...
    }
}

又過了一陣子,電商越做越大,消費者希望支付方式可以更加多元化
於是公司決定再加入PayPal 與 ATM 付款等新的支付方式
所以辛苦的我們再一次修改程式碼,但是改到一半,我們終於注意到一件事情

「萬一哪天又要加入虛擬貨幣支付或是其他支付方式,我是不是還要再改一次?」

我們在加入功能的同時,不停的去修改原來的程式碼
這顯然違反了開放封閉原則
那我們可以怎麼做才能在不修改程式碼前提下,幫程式新增新的支付方式?

Clean Code 的作者羅伯特·C·馬丁 (Uncle Bob) 提出一種方式

Separate extensible behavior behind an interface and flip the dependencies
將可擴展的行為用介面包裝,並翻轉依賴關係

順著這個思路,我們可以將程式碼修改成

<?php

// 新增一個支付的介面,想要實現這個介面,就必須實作付錢的方式 acceptPayment()
interface PaymentMethodInterface
{
    public function acceptPayment($receipt);
}


// 信用卡支付
class payByCreditCard implements PaymentMethodInterface
{
    public function acceptPayment($receipt)
    {
        // ...
    }
}

// 現金支付
class payByCash implements PaymentMethodInterface
{
    public function acceptPayment($receipt)
    {
        // ...
    }
}

class Payment
{
    // 只要支付方式符合 PaymentMethodInterface 介面,方法 pay 不需要知道支付是使用哪種方式
    public function pay(Receipt $receipt, PaymentMethodInterface $payment)
    {
        $payment->acceptPayment($receipt);
    }
}

這樣的方式,假如日後真的要新增虛擬貨幣的支付功能,我們只需要再加上一段程式碼

// 虛擬貨幣支付
class payByBitCoin implements PaymentMethodInterface
{
    public function acceptPayment($receipt)
    {
        //
    }
}

不需要動到舊有的程式碼,就能幫程式碼加上新的功能
這就是開放封閉原則的概念

sharkHead 後端工程師,稍微擅長 Laravel、Python 與 Google
對於前端有興趣,無奈沒什麼慧根