[心得] 用GUARD CLAUSES與EAFP寫code。
常常覺得,軟體總是越寫越複雜,過一段時間我一定維護不了。
甚至懷疑自己到底夠不夠格當一個軟體工程師。
直到有一天我學會了兩招。
今天想跟各位分享我寫程式以來,學到最重要的兩招。
哪裡有寫錯的話,希望各位不吝指教。
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
→
03/23 16:32, , 1F
03/23 16:32, 1F
推
03/23 17:48, , 2F
03/23 17:48, 2F
→
03/23 17:49, , 3F
03/23 17:49, 3F
推
03/23 21:53, , 4F
03/23 21:53, 4F
推
03/23 22:41, , 5F
03/23 22:41, 5F
推
03/23 23:25, , 6F
03/23 23:25, 6F
推
03/24 12:45, , 7F
03/24 12:45, 7F
推
04/02 15:50, , 8F
04/02 15:50, 8F
→
04/02 15:52, , 9F
04/02 15:52, 9F
討論串 (同標題文章)
完整討論串 (本文為第 1 之 2 篇):
PHP 近期熱門文章
PTT數位生活區 即時熱門文章