分類彙整:programming

如何寫好code:用Guard Clauses與EAFP打造你的城堡。

以前寫project的時候,常常越寫越不安。

心裡頭總是有兩個聲音:

  1. 為了更能應付不同狀況,程式碼要處理的情況越來越多、越來越複雜。if/else跟switch/case越寫越多層、一層又在一層又在一層裡面,越寫越像在挑戰自己智商的極限。
  2. 這個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。

(Photo via Elescir, CC License)

MVC,令人搞不懂的Model。

自學web開發兩年,對MVC一直一知半解,常在想怎樣設計系統會最漂亮。整理了一下目前的理解,以Model出發跟各位分享。其中有寫錯的部份也期待各位前輩指教。

Q1: 一個model對應到資料庫一張table對嗎?

不對。雖然大部分framework實作都會將一張table對應到model內一個class,但「一個model對應一張table」比較像是在描述ORM之類幫助簡化資料庫SQL操作的class,而model是「處理跟某張table相關任務的class」。

差別何在?一個model可能會處理到多張table的資料,也可能一張table都不碰,純粹處理一串商業邏輯。舉例來說,可能會有個負責整理行銷資料的model叫Marketing,從多張table撈資料出來,整理之後丟給controller。而資料庫中可能有,也可能沒有「marketing」這張table。

Q2: 可是我寫專案幾乎一個model對應到一張table啊?

因為實務上一個服務常會對應到一個model,而提供這model所需的資料常常就只是一張table。所以許多情況下的確是一張model對應到一張table沒錯。

但也不能說是「一張」table。model內常會去join其他相關的table對吧?所以說「model對應到一張table」或是「model就是一種ORM」都是不精確而誤導的說法。雖然寫起來常會有這種錯覺。

Q3: 那你用一個不會誤導又精確的說法描述model給我看?

model是layer」。不要把MVC中的M跟VC想成平行的分工關係,把VC跟M想成上往下的幾層layer。VC最靠近使用者、下來是M、再下來是database。所以VC要取得資料庫的資料都要透過model,而不同任務有不同model。

Q4: 聽不太懂,談點別的好了。從Rails社群流出一句很流行的MVC原則「fat model, skinny controller」,你對此有什麼想法?

這句話對初學者有幫助,對進階者有害。「fat model, skinny controller」這句話可以提醒初學者盡量把code丟進model別丟進controller。你再看一次我Q3的回答,一個應用程式的最重要部份(商業邏輯、安全性、效能)本來就是跟資料庫互動的那層layer,也就是model,所以把code丟進model別丟進controller可以暗示初學者寫出reusable的code。

但是把太多code寫進model會形成god pattern,也就是某個model跟神一樣負責絕大部分的任務,導致維護起來有困難(想像你初學PHP把全部code寫在同一份檔案的恐怖情況)。

Q5: 又說code要盡量丟進model,又說model內code太多很不好,不然code到底要丟哪?

library以及helper。現在大部分的framework都預設有放library以及helper的地方。不使用framework的話你也可以自己開這兩個資料夾。

多個model常做的同幾件事可以包起來寫成library,而單純的資料格式轉換(甚至是產生相關html/css)之類的任務可以寫成helper。讓model主要專注在商業邏輯(這是最花功夫的地方),其餘的搬到library、helper、或是有些framework稱之為third_party或package的什麼地方都好。別丟進controller!更別丟進view。

Q6: 好吧。可是我開發時在controller跟model內真的會不小心就寫掉大部分任務,這樣寫真的比較快阿。你開發時,上面說的你自己有沒有都做到?

沒有。原因有兩個。一,實務上的確很難在一開始就把架構想清楚,所以一堆任務會丟給model。二,常常越往下寫才發現某些任務是共通的、某些任務一開始只是「簡單」處理資料轉換,寫到後來變成「複雜的」處理資料轉換,我認為這時再把它拉出來成為library或helper即可。重構本來就是不斷在做的事情,時機到了就做*(註1)。

Q7: 我知道library跟helper的地位何在了。但,library跟helper的差別又何在?

軟體開發時,有時會寫幾個function或method解決某些任務,有時候覺得這樣太鬆散,會包成class。library常常是class、helper常常是一組function。以我自己來說,常在controller或view使用helper,在model使用library。

Q8: 關於model,最後有沒有要提醒的?

不要在model裡面用session variable。用下去,那model根本不再reusable(其他controller用之前還要先設定好相關session)。連unit testing都毀了。解決之道是dependency injection。session variable某種程度來說根本就是global variable,濫用global variable是很不好的。

註1:這段話可能很有問題。我在startup工作,開發上極度注重「速度」,常為了拼上線時間而捨棄其他事情。

(Photo via  Alaskan Dude, CC License)

framework實作 – front controller pattern & template engine

以前在用Python的Django、Rails或是PHP其他framework的時候,總覺得很難想像,要怎麼寫一個framework出來。

連最入門的這兩件事我都覺得難以想像:

  • 網址輸入’user/create’ 要如何執行controller資料夾內user class的create method?
  • 要如何讓controller去讀取view資料夾底下的html 然後render出去?

也就是說,所謂MVC pattern要從何開始實作?

最近試著親自動手做,發現其實沒那麼難,也從中學到不少。上述兩件事,100行內就可以作到。

提供我嘗試實作的source code給各位參考。

那100行程式碼長這樣

https://github.com/howtomakeaturn/PigFramework/blob/super_light/index.php

資料夾結構長這樣
https://github.com/howtomakeaturn/PigFramework/tree/super_light

(Photo via   Glyn Lowe Photoworks, 2 Million Views, Thanks, CC License)

替Model寫單元測試可以幹嘛?

單元測試( Unit Test)應用在軟體開發各個領域,本文僅討論MVC架構中的Model。

在web的MVC架構中,我們開發時本來就不斷在test。

  • 你一直對網頁按重新整理就是在test
  • 你在剛寫好的頁面上輸入一些測資然後按確定,也是在test你的controller或model

也就是testing的工作你其實一直都在做。

而MVC架構中的Model特別難處理,因為Model的行為很難測。

在資料到達Model之前,會先經過View、Controller,所以你會來回狂測,想確定bug到底在哪。

是jQuery在抓網頁元素的value時抓錯?是JavaScript相關的code有bug?是html的某個form name打錯?是Controller拿到form POST的值時做了變數型態轉換?是Controller裡面變數名稱打錯?是丟變數給Model時順序丟錯?還是Model裡面有bug?

反覆跑完以上流程抓出bug就算了,偏偏並非每個功能都是直接在網頁上key完資料就丟給Model。

結果光是想丟測資給Model就很麻煩。尤其你修好一個method常會想確認其他method有沒有因此壞掉。

行為單純的Model就算了,以上過程還不會花掉太多時間。複雜一點的 Model真的是debug到我會在電腦前大罵髒話。針對Model寫單元測試等於在開發Controller跟View前,先把Model弄得很強壯。

為了搞清楚自己的Model有多健康,你會寫好幾個test、丟好幾筆測資。加上為了要讓model能被test,你會來回去修改、把model寫的更模組化、更testable 、讓使用Model的人用得更愉快。

然後使用Model的人就是你自己。

所以unit test寫完、測完,你再寫controller跟view的時候就快樂到爆。

因為你知道寫Model的那傢伙很猛、把它寫得很強悍(亂丟資料也不會怎樣,那個Model會丟Exception、會丟error、會吐訊息, 反正Model會強力守住最後一道關卡,資料庫絕對不會爛掉。)

所以你Controller跟View就可以輕鬆寫,寫超快、超隨便也沒差,頂多就是使用者key錯資料會看到exception或是一些怪怪的錯誤訊息,絕對不會有資料庫存到壞掉的資料或是資料一致性的問題出現。

開發上出現bug時,你也可以確定是controller或是view的資料處理出錯,千錯萬錯絕不是Model的錯。(那真的是Model有bug怎麼辦?回去再寫幾個test抓bug,然後修好就行囉)

然後我認為,單元測試也不需要把所有可能的測資、全部使用情境都測過。

每個method隨便寫幾個最笨的testing就很夠了,通常有bug就是return value完全跟預期不一樣。寫幾個笨testing就夠抓出一堆麻煩了。

像是:

  • 資料庫某table在新增完有沒有多一筆
  • 各欄位金額加總有沒有等於總金額欄位
  • 那個要抓10筆資料的method最後抓到的是不是10筆
  • 亂丟變數給某method有沒有吐Exception

只測試這些夠混吧?夠不嚴謹吧?但是你最常搞出來的bug就是這麼單純而已。

當這些testing都跑過了,你的Model已經非常頭好壯壯了,超健康。眼看自己動手去挑戰自己的Model,最後還全部過關,絕對會信心UP、UP。處理複雜的Model時,將省下很多時間。(就算不寫單元測試,你也是用瀏覽器東測西測、花時間抓bug不是嗎?)

好,做個整理。

時機

某Model光用想就覺得頭痛,開發起來你覺得很沒安全感

好處

  • 讓你跳出開發者的角度,從使用者的角度去看這些method好不好用、 直不直覺,因此能寫出更出色(reusability, maintainability, scalability)的Model
  • 每次改完某method,都能立刻檢查其他的method有沒有被搞壞
  • 更好抓bug(寫testing時會在Model狂抓bug。之後寫controller/view時,你會知道bug都在controller/view裡面,不用跳來跳去檢查一堆code)

所以快找幾個能用的工具,試著寫單元測試看看吧。

還是不知道從何寫起的朋友可以在下方留言,我拿最近寫的程式碼寫一篇實作的範本。(我是以PHP/Codeigniter以及Codeigniter內建的單元測試函式庫當作工具)

(Photo via  Robert E. Kennedy Library at Cal Poly, CC License)

Bootstrap + Font Awesome, 工程師的美工救星

工程師不管是自己架網站來玩,還是接案幫客戶做網站

都會遇到美工的問題,也就是網站太醜了

基本的工具Bootstrap我想不用多加介紹 在國內也有一定知名度

http://getbootstrap.com/

套了之後,各種基本元素都有一定的美工設計水準

 

另外有個在國內比較冷門的工具

它是設計師 Dave Gandy 搭配Bootstrap設計的icon包

http://fontawesome.io/

跟Bootstrap整合的很好

可以利用它的各種icon增添網站的設計感跟user experience

這邊附上github上一個完美使用兩者的範例

http://cabotapp.com/