PHP例外处理在商业逻辑上的应用(一)使用继承

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:

class OrderCheckException extends Exception{} \\訂單檢查主要的Exception
class GoodsCannotBePurchasedException extends OrderCheckException{} \\商品無法購買的Exception
class StockNotEnoughException extends OrderCheckException{} \\庫存不足的Exception

接着在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.2
PHP: PHP 7.3.0

参与评论