抽象(Abstraction)是保留與目前目的有關的重要特徵,省略不需要關注的細節。
抽象分支保留呼叫端需要的共同操作,將具體做法交給實作。新舊實作可以同時保留在程式中,再依需要切換。這就是「分支」,讓團隊能在維持原本功能的同時,逐步開發與驗證新實作。
在 Nathan 團隊透過依賴注入切換寄信實作的案例中,CustomerNotifier 把收件地址與準備好的信件交給 Sender,由寄信實作決定要立即寄送還是排入佇列。
換了寄信實作,CustomerNotifier 仍然呼叫同一個 send()。這裡的抽象,就是保留共同的寄信操作,讓通知功能不必知道每種寄送方式怎麼做。
PHP 的 interface 是表達這個抽象的方式,宣告各個實作都要提供的操作。設計抽象不只是定義方法宣告而已,還要定義操作的行為,包括需要哪些資料,以及呼叫後可以預期什麼結果。
Nathan 團隊又接到一個新需求。原本客戶追蹤的協作文件每次更新,網站就會寄出一封通知。有些客戶不需要即時收到每次更新,希望改成每天收到一封摘要,列出當天更新的文件與內容。這封摘要會在每天結束後寄出。
前面的改版只調整寄送方式,通知功能仍然照原本的方式準備信件,再交給 Sender 寄送。現在要把多筆文件更新整理成一封摘要,就需要先收集哪份文件在什麼時間更新,以及要通知哪位客戶,再準備信件。但 Sender 接收的是準備好的信件,只切換寄信實作,無法改變前面準備信件的方式。
原本呼叫 CustomerNotifier::notify() 時,只傳入收件地址與文字。這次可以建立通知流程介面,讓新的 notify() 接收客戶 ID、收件地址、文件名稱、更新時間與更新內容。
接著建立逐筆通知實作與每日摘要實作。逐筆通知實作每收到一筆文件更新,就準備信件寄送;每日摘要實作則先保存資料,每天結束後再依客戶彙整成一封信寄送。
下圖由上到下呈現呼叫端、通知流程介面、兩種通知實作與寄信介面 Sender 的關係:

兩種通知實作都透過 Sender 寄信,因此可以共用原本的寄信程式。程式會依客戶選擇的通知方式,使用逐筆通知實作或每日摘要實作。
兩種通知實作的整合方式,以及驗證與切換做法,可以參考前面的說明。
前面的需求只改變寄送方式,這次則連準備信件的方式也要改變。兩個需求要調整的程式範圍不同,因此選擇的抽象層級也不同。從呼叫端提供的資料與實作負責的事情,可以看出兩者的差別:
| 抽象 | 呼叫端提供的資料 | 實作負責的事 |
|---|---|---|
| 寄信操作 | 收件地址與準備好的信件。 | 立即寄送,或依網域排入佇列。 |
| 通知流程 | 客戶 ID、收件地址、文件名稱、更新時間與內容。 | 逐筆準備信件並寄送,或先保存資料、每天結束後彙整成摘要並寄送。 |
Sender 的抽象只處理寄信,通知流程的抽象則包含準備信件與寄信。這次需求連準備信件的方式都要改變,因此需要在通知流程這一層提供不同實作。
如果只改寄送方式,原本準備信件的流程可以繼續使用,就不必另外建立兩套通知流程,增加選擇流程的程式與相關測試。
要抽象哪些操作,取決於需求改變了哪些行為。先理解業務需求,才能決定呼叫端需要知道什麼,以及哪些做法可以交給不同實作。