作者poopoo888888 (阿川)
看板PHP
标题[心得] 用GUARD CLAUSES与EAFP写code。
时间Sun Mar 23 16:31:41 2014
常常觉得,软体总是越写越复杂,过一段时间我一定维护不了。
甚至怀疑自己到底够不够格当一个软体工程师。
直到有一天我学会了两招。
今天想跟各位分享我写程式以来,学到最重要的两招。
哪里有写错的话,希望各位不吝指教。
http://blog.turn.tw/?p=83
----------------------------------------------------------------
以前写project的时候,常常越写越不安。
心里头总是有两个声音:
为了更能应付不同状况,程式码要处理的情况越来越多、越来越复杂。if/else跟
switch/case越写越多层、一层又在一层又在一层里面,越写越像在挑战自己智商的极限。
这个project再继续维护(扩充功能、修bug)三个月没问题、半年还撑得住,
之後一定越写越慢。不到一年,程式码将庞杂到难以挽救。
总觉得哪里出了问题:如果之後写商用软体、有持续扩充的野心,
这样写code下去一定会遇到大麻烦。莫非要跟客户或是老板说:
这project我无法继续维护了,因为我能力不够、您可能要另请高明。
难道我真的智商不足,没能力写出规模比较大的软体?
终於,有一天,我学到了两招,从此不再有这种担忧。
今天,就来跟各位分享我的秘密。
(天阿,口气真夸张,好像要推销产品还是卖保险,哈)
这两招是Guard Clauses与EAFP原则(It’s Easier to Ask Forgiveness than Permission
)。下面跟各位分享,有写错的部份也期待各位前辈指教
Guard Clauses
与Guard Clauses相对的是Nested Structure。
下面这种code是典型的Nested Structure,相信也曾出现在你的project。灾难一场。
function the_method($some_variable){
if (some_expression){
// some code
// ...
if (another_expression){
// some more code
// ...
if (another_expression){
// 这里是理想情况
// 终於可以在这里做想做的事了
// 不过我相信这串code过了一个月连你自己都看不懂
}
}
// ... maybe some 'else' block to handle here
}
// ... and here
}
遇到这种情况,要怎麽应用guard clauses?请看下面的范例。
以电子商务网站为例,下面是某class建立一张购物订单的method。要输入单品的id阵列、
买家id、付款方式与折扣金额:
function create_one($ids, $user_id, $payment_method,
$deduction_amount)
{
// 抵扣金额不是整数
// 呼叫这个method的工程师一定哪边搞错了吧
if ( !is_integer( $deduction_amount) ){
throw new Exception("invalid 'deduction_amount': $deduction_amount.
it's not integer.");
}
// 之前决定资料库内用负数代表折扣金额
// 传正数进来的工程师你昨天很晚睡吼
if ($deduction_amount > 0){
throw new Exception("invalid deduction amount: $deduction_amount. it
can't be position");
}
// 资料库内找不到这个$user_id
// 呼叫这个method的工程师你要不要再检查一下程式码
$query = $this->db
->where('id',$user_id)
->get('users');
if ( $query->num_rows() == 0 ){
throw new Exception("invalid user id: $user_id. the user doesn't
exist");
}
// 公司只有接受三种付款方式
$acceptable_payment_method = array( 'ATM', 'at_home', 'store');
if ( !in_array( $payment_method, $acceptable_payment_method ) )
{
throw new Exception("invalid payment_method: $payment_method .");
}
// 继续一串要检查的项目
// 这边终於可以把资料整理一下然後存进资料库了
// 可能还是会在这里喷出不知道什麽鬼的exception
// 但整段code真的好读、好维护、又robust很多了
return true;
}// end function create_one
後者是不是清楚非常多?
EAFP原则(It’s Easier to Ask Forgiveness than Permission)
*注1
与EAFP相对的是LBYL原则(Look Before You Leap)
想像下面这个用LBYL写的除法函式:
function safe_divide_1($x, $y){
if ($y == 0){
echo "分母为零";
return null;
}
else{
return $x/$y;
}
}
接着是用EAFP写的除法函式:
function safe_divide_2($x, $y){
try{
return x/y
}
// 如果你有定义这样一个ZeroDivisionException类别的话才行
catch (ZeroDivisionException $e){
echo "分母为零";
return null; // 或是再throw $e给上面的stack处理也行
}
// 就算你上面没定义ZeroDivisionException
// 这边还是会抓得到错误然後显示讯息啦
catch (Exception $e){
echo ($e->getMessage());
return null;
}
}
後者有没有比较乾净俐落?
(好吧,这边sample code看起来好像没有,哈哈)
LBYL代表你小心处理各种情况;EAFP代表你直接执行最重要的程式码,然後碰到问题
(出现exception)再面对。
在MVC web开发领域,通常就是controller写try/catch去执行model的某些method(EAFP)
,而不要在controller内自己写一大堆判断式检查(LBYL)。
对应到刚刚电子商务订单的例子,某个成立订单的controller根据EAFP会写出
这样的code:
try{
$this->Order_model->create_one($ids, $user_id, $payment_method,
$deduction_amount);
} catch (Exception $e){
// 这样写其实很混,使用者会看到一个很丑的error页面
// 但是work就是了
exit($e->getMessage());
}
// some code ...
// 显示恭喜使用者的页面/请他拿钱包准备付款的页面之类的
若是根据LBYL原则去写controller,则会写出一大串if/else if/else去检查$ids,
$user_id, $payment_method, $deduction_amount这几个变数、
小心处理完各种情况才去呼叫create_one这个method。程式码会又长又丑,
看不懂整段code重点在哪。
以上是个人学习写code学到最重要的其中两招。其中有写错的部份也期待各位前辈指教。
Q&A
Q1: guard clauses看起来超直觉的啊,为什麽我到今天才知道可以这样写code?
这跟学校教育有关。正统教育有个名词叫做single entry/exit point,说是为了维持code
的高品质,叫你应该这麽做。
Q2: 有时候我还是觉得写一串if/else比guard clause直觉啊?
程式码的外观其实直接说明了某些事情。guard clauses的code看起来就像在说:如果发生
这件事,赶快处理完、然後滚吧。一串if/else if/else像是在说:你看,各个情况都一样
重要、所以大家都在一样深层的巢状结构里面,
真正重要的code跟某些狗屁情况一样重要,所以他们一样在这恐怖程式码的内部第N层!
到底该怎麽写还是看实际情况。不妨都试试,看当下哪个适合?
Q3: 我就老实告诉你吧!我根本不想学exception或是try/catch是什麽!因为我的code用
if/else或是switch/case/default就把全部情况都处理好了!
会这样想是因为你以前学C语言对吗?C语言没有exception机制,工程师必须写出完整处理
各种情况的程式码。毕竟LBYL(Look Before You Leap )也是一种撰写风格。那就写到你
觉得痛苦的那天再回来学这几招吧。
丢exception不但code较易读,还能在原本function之外去处理相关错误(把问题丢给「其
他层级」去处理,像是model丢给其他model、model再丢给controller之类的)、追踪
exception在各stack内的流动。
Q4: 好吧,我刚是骗你的。其实我对正在用的语言的exception机制不熟。我还能用guard
clauses吗?
可以!上面sample code的throw new Exception全部写成return false即可。
然後EAFP就不会写成try/catch了。
以同一个建立订单的例子来说,如果你觉得成功跟失败都是人之常情,请写成这样:
$success =$this->Order_model->create_one($ids, $user_id, $payment_method,
$deduction_amount);
if ($success){
// some code ...
// 显示恭喜使用者的页面/请他拿钱包准备付款的页面之类的
}
else{
echo "亲爱的使用者,因为我还没学exception所以我不能告诉你哪边出错,反正出错
了。"
}
如果你觉得在你系统内应该不太会失败才对,请写成这样:
$success =$this->Order_model->create_one($ids, $user_id, $payment_method,
$deduction_amount);
if ( !$success){
echo "亲爱的使用者,因为我还没学exception所以我不能告诉你哪边出错,反正出错
了。"
}
// some code ...
// 显示恭喜使用者的页面/请他拿钱包准备付款的页面之类的
Q5: 你Q2、Q3好像在说有时候的确需要写一串丑丑if/else if?可以再做一点说明?
if/else if 可以想成在处理「合理的各种情况」。它们都一样重要,所以code在同层级各
占了一整串。
throw exception则是在处理「不合理但确实可能面对的情况」。这种情况随便丢个
exception还是秀error之类的,叫它快滚就好了。
如果你很在意user experience,那就去客制化(宣告class去继承原生exception)各个
exception、帮助其他developer(通常就是你自己)在各种exception之下处理各种情况吧
。
不过,在Web开发领域,喷exception常是在controller呼叫model函式之时发生。我通常直
接在view内用JavaScript提示使用者。
他若是进行什麽诡异操作跳过了我的JavaScript协助,
那我就用浏览器大方的喷个超丑的error message页面给他。
Q6: 为什麽Q4你要示范两种写法?你让我好困扰、我到底要挑哪种?
把Q2再看一遍。
————————————————————————————–
注1:EAFP与LBYL这两个词我是在Python社群看到的。这篇用字遣词可能不甚精确。
注2:文章里建议的某些处理方式非常随便。以上仅是我目前的理解。请不断追寻自己最喜
欢的best practice。
--
※ 发信站: 批踢踢实业坊(ptt.cc)
◆ From: 118.160.253.56
1F:→ poopoo888888:希望能得到前辈们的意见,谢谢。 03/23 16:32
2F:推 fri13:之前看过用函式拆开流程的 比较不会一堆if或catch 03/23 17:48
3F:→ fri13:更正 单一函式看起来比较不会一堆if或catch 03/23 17:49
4F:推 wayway2004:good 03/23 21:53
5F:推 GoalBased:你第一个例子会比较清楚 是因为有注解吧= = 03/23 22:41
6F:推 tkdmaf:我能建议的就2楼说的,函式单一职责吧! 03/23 23:25
7F:推 Kenqr:推 03/24 12:45
8F:推 locklose:包装相关且相依,切割延伸与额外 04/02 15:50
9F:→ locklose:刚出道我老板讲的很简单,但我摔了一堆坑才明白… 04/02 15:52