PHP exception handling in business logic - using Inheritance
前言
開發人員開發時經常會遇到一個狀況是,某支程式隨著需求不斷變更與增加,條件判斷越來越多、越來越複雜,程式的流程越來越混亂,閱讀程式時必須跳來跳去,程式漸漸變得難以理解,令人頭痛不已。
這種時候例外處理就可以派上用場了!
簡介
所謂的例外處理(Exception Handling),不外乎就是專門處理例外情況的發生,異常、錯誤等例外,這是現代程式語言常見的功能,隨著PHP版本的更新,PHP對例外處理的支援度及彈性也是越來越高。對開發人員來說,我們可以在程式中藉由妥善地利用例外處理機制,來增加程式的可讀性以及可維護性。
這邊是例外處理的基本形式:
try {
throw new Exception('here is a exception!');
} catch(Exception $e) {
echo $e->getMessage();
}
Output:
here is a exception!
本篇接著會舉一個簡單的案例,來說明如何在商業邏輯上套用PHP的例外處理功能,以及如何應用繼承的技巧,達成更好的例外處理方式。
案例
情境說明
小明到了線上購物平台上購買了一樣商品,在確認完資料後,他點擊了「送出訂單」,等待畫面上的圓圈轉了幾圈後,原以為購買完成了,卻跳出「訂單成立失敗」的訊息⋯⋯
雖然看到這樣的訊息令人失望,但小明會看到這樣的錯誤訊息,正式因為程式有好好的檢查資料正確性的緣故。
即便使用者從畫面上按下了「送出訂單」,但總不可能就這樣讓他毫無節制的成立訂單吧!除了程式、資料會出錯以外,還經常會造成一些交易糾紛,所以稱職的後端工程師必須在訂單真正成立之前,經過一連串嚴密的檢查,來避免這些事情的發生。
處理 - 使用if-else
為了要處理那一系列的判斷,一開始工程師可能會採用if-else來針對每項檢查來做處理。就訂單成立前要做的檢查而言,包括比如說商品是否已經下架不能購買了、買家所購買的數量不足⋯⋯等等。
class Order
{
public function check()
{
// 檢查是否可以被購買
if (!$this->isGoodsCanBePurchased()) {
return '無法購買此商品';
}
// 檢查庫存數量是否足夠
if (!$this->isInventoryEnough()) {
return '庫存不足';
}
return '';
}
private function isGoodsCanBePurchased() { /* omitted */ }
private function isInventoryEnough() { /* omitted */ }
public function create() { /* omitted */ }
}
在成立訂單的Controller中會有這樣的邏輯:
$order = new Order($orderData);
$result = $order->check();
if ($result != '') {
echo $result;
} else {
$order->create();
}
成立訂單時,先判斷$order->check()
的回傳結果是否正確,如果沒有錯誤,才繼續往下執行成立訂單的動作。這樣的程式正確性沒有問題,但是閱讀程式途中總是有點不順暢的感覺,比較難直接清楚地知道下一步的行為是什麼,可讀性還有改善的空間。
處理 - 使用Exception
為了改善可讀性,我們可以將$order->check()
中的行為改寫成當訂單檢查沒有通過時,直接往外拋Exception,並且在Controller中,使用try-catch
來捕捉Exception後做對應處理,來讓主流程的程式行為更明顯。
class Order
{
public function check()
{
// 檢查是否可以被購買
if (!$this->isGoodsCanBePurchased()) {
throw new Exception('無法購買此商品');
}
// 檢查庫存數量是否足夠
if (!$this->isInventoryEnough()) {
throw new Exception('庫存不足');
}
}
private function isGoodsCanBePurchased() { return/* omitted */ }
private function isInventoryEnough() { /* omitted */ }
public function create() { /* omitted */ }
}
Controller:
try {
$order = new Order($orderData);
$order->check();
$order->create();
} catch (Exception $e) {
echo $e->getMessage();
}
從範例中,我們可以比較清晰地看出程式流程:做完檢查$order->check()
後,成立訂單$order->create()
。訂單檢查有發現錯誤時,會直接拋出Exception來中斷主流程的進行,不會往下執行成立訂單,而是直接跳到catch區塊做錯誤處理,這樣一來也可以確保程式的的正確性沒有問題,不會執行不該執行的步驟。
處理 - 使用Exception搭配繼承
但是需求總是會不斷變更的,某天PM突然提出新需求,要求工程師除了要阻擋錯誤的訂單成立以外,還要搭配不同的錯誤,進行不同的額外處理,比如說,轉址到不同頁面、紀錄錯誤log或在DB中寫下一些資訊,這種時候繼承就可以幫上忙了!
首先我們利用繼承定義一系列訂單檢查相關的Exception:
\\訂單檢查主要的Exception
class OrderCheckException extends Exception{}
\\商品無法購買的Exception
class GoodsCannotBePurchasedException extends OrderCheckException{}
\\庫存不足的Exception
class StockNotEnoughException extends OrderCheckException{}
接著在Class中改用新定義的Exception,當發生對應的狀況時,就丟出對應的例外類別。
class Order
{
public function check()
{
// 檢查是否可以被購買
if (!$this->isGoodsCanBePurchased()) {
throw new GoodsCannotBePurchasedException();
}
// 檢查庫存數量是否足夠
if (!$this->isInventoryEnough()) {
throw new StockNotEnoughException();
}
}
}
接著在Controller中改寫成針對不同例外做不同處理。
try {
$order = new Order($orderData);
$order->check();
$order->create();
} catch (GoodsCannotBePurchasedException $e) {
// 商品不能被購買 -> 重新導向首頁
redirectToHomePage();
} catch (StockNotEnoughException $e) {
// 庫存不足 -> 重新導向回商品頁
redirectToGoodsPage();
} catch (OrderCheckException $e) {
writeOrderErrorLog($e->getMessage());
}
一樣,主流程很清楚,不用在主流程中費力處理例外狀況。而例外狀況會在每個catch區塊中處理,針對商品不能購買時候錯誤,可以重新導向首頁,庫存不足時,則可以導向回商品頁,讓用戶重新選庫存。
而最後一個catch中所捕捉的例外OrderCheckException,並且也可以捕捉所有繼承OrderCheckException的類別,則是利用物件導向多型的技巧。
當$order->check()
中有其他判斷,但是並不需要特殊處理時,我們可以直接拋出OrderCheckException或是其他繼承OrderCheckException的例外類別,讓最後一個catch可以判別是order檢查時拋出的錯誤,來記錄訂單的錯誤log,以供備查。
結語
本篇使用的例子是非常簡化過的,實際上可能會有更複雜的狀況,大家可以試著在程式中套用例外處理來改善程式流程~
本篇程式碼執行環境
OS: Mac OS Mojave 10.14.2PHP: PHP 7.3.0