iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0

功能標誌讓程式可以先整合與部署,再依產品安排開放功能。如果要替換既有功能的實作,改版期間仍須維持原本功能運作,也要讓其他開發者能繼續開發依賴它的功能。

抽象分支(Branch by Abstraction)是一種逐步替換既有程式的開發技巧,透過共同抽象,讓新舊實作可以並存。開發者先調整既有程式的結構,保留原本行為,再逐步加入新實作,完成驗證後切換,最後清理不再需要的程式。改版可以持續一段時間,每個完成的步驟仍能整合到主幹。

寄信程式需要調整

Nathan 團隊接著想做的事是寄信改版。

目前所有通知信都在網站處理請求時寄送,網站必須等寄信操作結束,才能回應使用者。團隊希望把寄信移到背景程序,縮短使用者等待網站回應的時間。研究完發現,可以使用佇列。它可以先保存寄信任務,再由背景程序取出執行,但排入佇列不表示信件已經寄出。

Mandy 負責修改寄信程式,Brent 準備佇列與執行背景任務的程序。由於團隊還沒有使用佇列寄信的經驗,兩人需要一起確認任務是否能正常處理,以及寄送失敗時怎麼辦。準備與驗證期間,正式功能仍須使用原本的寄信方式,其他成員也會繼續修改通知信的內容與觸發通知的功能。

為了控制切換後的影響範圍,團隊先選定一個允許延後收到通知的客戶,以其收信網域 example.net 作為這次改版的範圍。確認這批通知的背景寄送流程能正常運作後,再評估是否擴大範圍;其他網域先維持原本方式。

目前通知功能會先準備好收件地址與信件內容,再交給 ImmediateSender 寄送。寄信操作集中在這個類別裡,因此可以從這裡著手調整寄送方式。

以下範例使用 PHP 8.5 與 Laravel 13,原本的 app/Mail/ImmediateSender.php 如下:

namespace App\Mail;

use Illuminate\Mail\Mailable;
use Illuminate\Support\Facades\Mail;

final class ImmediateSender
{
    public function send(string $email, Mailable $message): void
    {
        Mail::to($email)->send($message);
    }
}

Mailable 是 Laravel 用來描述信件主旨、內容與附件的類別。本例每次寄信都建立新的信件物件,沒有實作會讓信件自動排入佇列的 ShouldQueue 介面,因此這裡的 send() 會在當下執行寄送操作。

這些呼叫 ImmediateSender::send() 的通知程式,以下稱為呼叫端。改版前,呼叫端直接建立並使用 ImmediateSender,關係如下:

https://ithelp.ithome.com.tw/upload/images/20261001/20102562A6AfCb8OiQ.png

保留原本的寄信方式

Mandy 可以在自己的分支裡修改 ImmediateSender,讓指定網域的信件改為排入佇列,等背景寄送流程準備好、驗證通過後,再合併回主幹。在改版合併回主幹前,主幹上的程式仍使用原本的寄信方式。

不過,其他人會繼續修改通知程式。如果其他人修改了通知信的內容,並合併到主幹,Mandy 就要把這些變更合併到自己的分支,確認新的寄信方式仍能正常寄送這些通知信。

這又回到開發者之間的距離問題。即使 Mandy 持續取得主幹更新,新寄信程式與通知功能的配合仍只在她的分支上驗證。等到整份改版完成才合併,團隊就無法提早在共同主幹上整合與驗證這些程式。

另一種做法是保留 ImmediateSender,另外建立一個類別實作新的寄信方式。新類別可以先供改版與測試使用,不必等背景寄送全部準備好才整合。新舊類別都在主幹後,其他人修改通知信的內容時,團隊就能在同一個版本裡驗證兩種寄信方式,正式功能則繼續使用原本的類別。

只是,如果兩個類別各自定義方法,後續有人調整其中一個方法的名稱或參數,另一個類別沒有跟著調整,呼叫端換用實作時就可能無法照原本方式呼叫。團隊因此需要先約定兩個類別共同提供的操作,再用介面約束方法宣告。

確認兩種實作共同遵守的行為

方法名稱與參數相同,還不足以代表兩種實作可以替換。原本要等寄送操作結束,呼叫端才會繼續執行;指定網域改走佇列後,只要成功排入任務,呼叫端就會繼續執行,不會等待背景程序完成寄送。

這些指定網域的通知雖然允許延後寄送,仍要確認呼叫端是否依賴原本的寄送時機。Mandy 和負責通知功能的成員確認,呼叫端不需要立即知道寄送結果。如果呼叫端需要在當次請求中得知寄送失敗並顯示錯誤,就不能直接改成背景寄送,必須先調整提示與錯誤處理方式。

立即寄送或排入佇列時發生的錯誤,會由目前的呼叫流程處理;成功排入之後才發生的寄送錯誤,則交由背景任務的失敗處理接手。

這些確認也呼應了 Liskov 與 Wing 對子型別的要求:

“…any property proved about supertype objects also holds for its subtype objects.”

—— Barbara H. Liskov、Jeannette M. Wing,1994(節錄)

也就是說,對父型別物件已證明成立的性質,對子型別物件也必須成立。套用到這個案例,兩種寄信實作都需要遵守共同介面的行為約定,讓呼叫端依照約定所做的判斷仍然成立。

抽出介面,保留原本行為

確認用途後,可以把共同的操作寫成 Sender 介面,放在 app/Mail/Sender.php:

namespace App\Mail;

use Illuminate\Mail\Mailable;

interface Sender
{
    public function send(string $email, Mailable $message): void;
}

介面要求實作提供 send(),接收收件地址與信件物件。PHP 會檢查實作的方法宣告是否相容,但不會替團隊確認信件內容、寄送時機與錯誤處理是否符合約定,這些仍要透過測試與實際流程驗證。

接著讓原本的 ImmediateSender 實作介面,只調整類別宣告:

final class ImmediateSender implements Sender

send() 裡的程式保持不變。原本直接建立 ImmediateSender 的通知功能仍可照常使用,此時還沒有開始背景寄送。

下圖呈現這一步的關係:呼叫端仍直接建立並使用 ImmediateSender,而 ImmediateSender 實作了 Sender 介面。

https://ithelp.ithome.com.tw/upload/images/20261001/20102562E5pf7Pm2Km.png

Mandy 和通知功能負責人先確認原本的流程仍使用相同收件人、主旨與內容寄信,失敗時也按照原本方式處理。必要驗證未通過時,就先修正這一步,不能等新實作完成後再一起處理。完成審查與驗證後,就可以把介面與原本的實作整合到主幹。

這樣就完成了改版的第一個可整合步驟:團隊已經約定共同的寄信操作,並用介面約束方法宣告,原本的通知功能也能繼續運作。後續調整呼叫端與加入佇列實作時,就有共同的介面可以依循。

小結

抽象分支讓改版能分步整合。這裡的「分支」是在程式中保留可替換的實作,每個步驟仍可透過短期分支提出、審查與合併,不必把整份改版留在長期分支。

目前呼叫端仍直接建立 ImmediateSender。接下來要把建立物件的決定移到共同位置,才能在切換實作時,不必逐一修改每個通知功能。

參考資料


上一篇
Day 16:功能標誌的實作與管理
系列文
重新認識主幹開發(Trunk-Based Development) 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言