標籤彙整: 軟體開發方法

quality

開發網站時,寫測試的3個簡單方法

許多想要學寫測試的朋友,常常不知道怎麼開始。

其實寫測試比大多數人以為的還要簡單。

就算是小型的網站、簡單的購物網站,也可以利用軟體測試加強軟體品質。

今天跟大家分享3個簡單的導入測試開發方法。

(本文使用 Laravel + PHPUnit 示範,但相關概念通用在任何語言/工具上。)

第一招、controller轉移到entity

開發網站時,大部份商業邏輯都在對entity(例:產品、訂單、使用者…etc)操作。

相關的code通常會散在controller裡面。

舉例來說,假設我們在寫一個購物網站,那麼把產品加進訂單、更新訂單金額的code會像這樣:

寫測試的目的是為了增加開發者的信心。

像上面這樣的code,有些人會覺得打開瀏覽器按個幾次,看看最後訂單金額是否正確就好。

反覆按個幾次,就算不寫測試,也對程式品質很有把握、相信它沒有bug。

但如果程式再複雜一點呢?例如增加不同運費的邏輯:

有些人依然覺得這不複雜,在瀏覽器上多操作幾次,然後看看金額、運費是否都正確即可。

但如果你像我一樣,採取比較嚴謹的立場,預設自己寫的所有code都有bug,那麼可以先把code從controller搬到entity:

然後可以這樣寫測試:

像這樣的測試寫好之後,就不用每次改動一點商業邏輯,就打開瀏覽器重複做一堆人工測試了。

如果運費的算法更複雜,根據訂單金額而有4、5種運費金額的話,

光是要測試運費邏輯,就要人工去操作下單流程好幾次。

這種時候,寫測試方便多了。

這種開發流程,我稱為「controller轉移到entity」。

一開始邏輯單純的時候,直接寫在controller即可。

複雜起來後,再把code轉移進entity,然後補寫測試即可。

第二招、轉移到service

接續上面的例子,來看看更複雜的例子。

現在產品賣掉的時候,產品本身的庫存數量要減一。

以原本的寫法來說,需要在entity加上這一行:

然後在測試內多加幾行:

看起來沒多大問題。邏輯放在Order內還算合理,該測的也都測到了。

但如果邏輯再複雜一點呢?

假設有人氣指數的功能,在賣出去的同時,商品的人氣指數要加上10分:

這行code還是放在Order類別內嗎?

為什麼一定是$order->addProduct($product),而不能是$product->addToOrder($order)呢?

隨著Product相關邏輯變多,商品加入訂單的功能不再完全像是訂單的事,也很像是Product的事。

像這種不知道要放在Order內還是Product內的時候,可以獨立出來成Service類別:

測試的時候,改寫成這樣即可:

跟第一招一樣,可以在程式變大之後、有必要的時候才獨立成service類別並補寫測試。

轉移的成本是很低的,幾乎只是把code從這邊剪下貼上到那邊而已。

第三招、轉移到專門的POPO

POPO(Plain Old PHP Object)是指單純的全手寫類別,不去繼承Eloquent之類的華麗類別。

雖然是最基本的OOP用法,卻常常被人們所忽略。

現在假設訂單進一步變複雜,運費的邏輯會根據消費者的所在地區而有不同:

再加上金額的變化,光是運費本身的計算就很複雜、需要寫很多測試才安心。

與其在entity或service內計算,不如弄一台運費計算機出來,會更分工明確:

接著就能替計算機寫許多測試,測到安心為止:

本來的service就可以改寫成,吃計算機當作參數:

POPO常常被人們忘記,因為太習慣把code全都寫進entity或是controller裡面。

其實,在程式複雜起來之後,最好把意義上獨立的部份各自獨立成類別。

然後分別替這些POPO寫各式各樣的測試,就能大幅增加軟體的品質與穩定性了。

結論

寫測試的目的純粹是為了增加安全感,讓工程師晚上安心入睡而已。

不需要撲天蓋地般地狂寫測試,適量的測試就非常足夠。

以中大型專案來說,寫測試可以幫助省下「非常大量的時間」。

實務上,在一開始商業邏輯不多的時候,寫在controller通常沒什麼問題,把幾個變數存進資料庫而已,不太需要寫測試。

等到邏輯慢慢變複雜,發現不寫測試會不敢上線的時候,再拉出來並且補寫測試即可。

希望你之後的專案開發,可以試試看寫測試帶來的好處與美妙!


歡迎訂閱轉個彎日誌的粉絲專頁,我很樂意和你分享各種心得。

(Photo via Elizabeth Hahn, CC licensed.)

test

PHP與撰寫測試入門

許多人對於撰寫測試躍躍欲試,卻因為PHPUnit等工具太複雜、

相關名詞艱澀難懂而不知該如何開始。

其實PHPUnit這類測試工具並非必須品,相關專有名詞也可以先擺一邊,

只要發揮創意想辦法測試就是第一步了。

我們從最簡單的形式開始談起吧!

假設我們有一個簡單的加法程式要測試

最簡單的測試其實只要這樣寫

要執行測試非常簡單,只要打開terminal然後輸入

就可以看到測試結果了!

突然發現if打成了$if,修正之後再次執行的結果是

發現少打一個分號,修正之後再次執行的結果是

發現多打一個單引號,修正之後再次執行的結果是

終於通過測試了!

注意到了嗎?光是替這樣簡單的程式撰寫簡單的測試,就可以省下不少開發時間:原本要打開瀏覽器重新整理三次才能修完bug(人工測試3次),變成只要在terminal跑三次就修完bug了!

Q1: 打開瀏覽器重新整理三次其實沒花多少時間…寫測試真麻煩,而且不划算。

有人說寫測試一定划算,也有人說寫測試一定不划算,其實都是不對的說法!
某些情況下確實不用寫測試,光靠瀏覽器就人工測試完成了。
但很多時候人工測試的時間會大於撰寫測試的時間。譬如說一個需要先登入、輸入一串假資料、東點西點按鈕才出現結果的頁面。那種時候,寫測試就會划算了。

Q2: 二加三等於五是什麼爛測試…,也太不嚴謹了吧。

這類稱之為happy path的測試確實非常隨便。但是以入門來說,光寫這類測試其實已經有很出色的測試效果:

1. syntax error全部會被找到
2. 至少確認happy path通過了。很多時候程式出錯就是連這種happy path都過不了

不過,為了謹慎起見,我們來寫更完整的測試吧!

接著在terminal輸入

會看到結果顯示

恭喜,通過更嚴酷的測試了!

Q3: 老實說你的測試還是很爛…只是拿1跟2跟0跟-1隨便測一下而已,實在不能保證什麼。

沒有錯,其實軟體測試從來就無法保證軟體不會出錯。
有人說軟體測試是為了讓程式正確執行,其實不太正確!
軟體測試只是為了讓你對程式有信心而已。
要測到什麼程度則取決於:

1. 你心臟有多大顆
2. 這個程式值得你花多少時間去測到何種程度

以你的高標準來說,可以再拿999,999,999跟-999,999,999,999,999,999等數字去測,甚至拿null、空字串、亂數字串丟進去測(如果你覺得有意義的話)。

很不幸的,大部份的程式不會像上面那樣單純顯示結果,而是會包含一堆html。
像是一個新增文章的程式,使用者會從表單填寫標題與內容,接著以HTTP POST送出。

像這樣結果複雜的程式,該怎麼測試呢?

嘿嘿,還記得一開始說的「只要發揮創意想辦法測試」嗎?

我們來檢查最後結果有沒有包含特定字串吧!

接著在terminal輸入

會看到結果顯示

奇妙吧!根本還沒打開瀏覽器,就幾乎可以確定這個新增文章的程式沒問題了!
不但syntax確認沒問題,也很有信心會正確新增文章!

Q4: 這個測試未免也太爛了。一看就知道bug一堆。要是測試標題輸入「Title」不就會抓到h1裡面那個「Title」?那就是根本沒測到!另一個測試「I am happy」更扯,如果頁面其他地方本來就有字串包含「I am happy」,不就毫無意義嗎?

說得真好!不過Q3已經說了,軟體測試只是為了對程式有信心而已。
雖然只寫了兩個破爛測試,但我已經對程式很有信心囉!我認為OK的!
而且不是說了要發揮創意嗎?不然我還能怎麼測呢?

好吧,其實會這麼鳥是因為呈現邏輯(presentation logic)和商業邏輯(business logic)混在一起的關係。
把business logic獨立出來測吧!

先把business logic獨立出來放進另一個檔案,包進function內

再改一下原本程式

測試則改寫成

接著在terminal輸入

會看到結果顯示

酷吧!你提到的「頁面其他地方本來就有字串包含『I am happy』」的bug解決了!
這下是不是對程式更有信心了?
除此之外,現在測試裡面不再硬塞value給$_POST變數,也不再需要output buffering的技巧了。
事情更單純了吧。你常聽到有人說測試要一小塊一小塊測,就是這麼一回事。

Q5: 不對喔…我發現事情不對勁。這下你的測試根本沒include到blog_submit_v2.php這支檔案!檔名根本不配blog_submit_v2_test.php。變成只測到business logic而沒測到presentation logic了。

我測完business logic其實已經很安心了,至於presentation logic的話我會直接打開瀏覽器,頁面正確顯示我就安心了。你真要那麼教條化嗎?好吧那你再寫一個測試去include blog_submit_v2.php這檔案好了。(寫那種測試的時間真的值得嗎?直接開瀏覽器檢查不好嗎?)
但是我同意blog_submit_v2_test.php這命名有問題。就改成add_blog_to_db_test.php吧!

Q6: 等一下!我發現你的範例跟測試都是個笑話!你把add_blog_to_db函式最重要的任務「資料庫存取」省略了!回傳那array根本搞笑,真的存取資料庫是要怎麼測試?你倒是說說看。

老話一句,發揮創意吧。把mysql函式寫得不成功便成仁就是一種方式

或是最後回傳true之類的,讓測試程式能夠檢查就好。發揮創意吧,發揮創意吧。

讀到這邊,相信你已經發現一件事了:最傳統的一個php檔案寫到底的寫法,非常難以測試。就算測了心裡也很不踏實。聽過有人說「寫測試不難,難的是寫出能被測試的程式」嗎?

以後不要再寫像那樣老舊的php code了,建議把重要的business logic全部包成獨立的function,然後再分別測試function比較好!

除此之外,前面只用到==運算子和strpos函式而已,如果想寫出更多種的測試,光是內建函式就有這些可以用喔
is_​array
is_​bool
is_​callable
is_​double
is_​float
is_​int
is_​integer
is_​long
is_​null
is_​numeric
is_​object
is_​real
is_​resource
is_​scalar
is_​string
很酷吧!記得,重點是發揮創意,寫出能被測試的code、從最基本的測試開始做起!

有測試總比沒有好!

Q7: 我發現盲點了。寫測試說是要節省開發時間,但是光跑測試本身我就要輸入

這東西四次!想想看,每次增加新功能或是修改舊功能,為了確保全部程式都正確,我要重新輸入這東西四次!光是打字我就飽了,哪有更省時間?

這個問題很好解決,寫一個測試主程式就好啦

以後只要在terminal輸入

會看到結果顯示

光是這麼無聊的範例,看到9個測試都通過就很有成就感了,正式上線的application那種幾百幾千個測試通過的感覺更充實、更對系統穩定性有把握。

每次增加新功能或是修改舊功能,為了確保全部程式都正確,記得都跑一次php test_suite.php喔!再也不需要打開瀏覽器點來點去半天了!

結語

這篇文章是對測試基本觀念的說明,實際開發時使用PHPUnit或是其他測試框架,在做的事情其實跟本文差不多。

如果認為PHPUnit太複雜的話,可以先從比較簡單的測試工具開始,像是SimpleTest就不錯。

至於所謂嚴謹的測試技巧,以及物件導向的開發測試手法,我們下次再談吧!


社群feedback(last updated: 2015-10-14)

本文在PHP台灣FB社群引起一些討論與檢討,

推薦大家一併延伸閱讀

討論串一

討論串二

這樣寫測試錯了嗎?


如果您喜歡我的文章,可以在這裡訂閱。我有新想法的時候,很樂意跟你分享。

本文的程式碼可以在這裡取得。

(Photo via Masahito Oku, CC Licensed.)

structure

ARCA架構:以 Active Record 為軸心的架構

越來越多的framework本身提供Active Record實作。

不管是Rails的Active Record,還是Laravel的Eloquent,

這些Active Record實作的功能越來越豐富而強大。

它讓開發速度更快了,但也因為太多功能塞在一起,容易寫出極度肥胖的萬能類別。

該怎麼使用這種萬能類別,又不至於讓它胖到難以維護呢?

我從工作中整理出了稱之為ARCA的架構。今天來跟各位分享。

註:本文程式碼多以Laravel框架舉例。但是相關概念通用在任何擁有Active Record的語言/框架。

簡介

ARCA全名「Active Record Centered Architecture」,

也就是「Active Record中心架構」,以Active Record為主軸的軟體架構。

ARCA試圖在享受Active Record帶來的開發速度的同時,拉出一定的架構。

這套架構適用於中型以上(連續開發3個月以上)專案,不適用於小型專案(像是餐廳官網)。

ARCA架構

ARCA架構由幾個元素組成,檔案結構看起來如下(以MVC舉例):

一個元素會是一個資料夾,或是一個檔案(類別)。

/View資料夾
/Controller資料夾
/Model資料夾
…./Entity Model資料夾
……../Entity類別
……../Repository類別
……../Presenter類別
……../Form類別
…./Service資料夾
……../Service類別
…./Operation Model資料夾

Entity Model資料夾

擺放與一種entity相關概念的程式碼。

將一種entity相關的程式碼放在同一個資料夾。

Entity類別

entity是公司在乎的商業領域(domain)中,代表現實世界的一種事物。

常見的entity包含訂單、商品、顧客、禮券、文章。

通常會代表現實生活中的一種事物、需要具備ID。

在ARCA中負責直接繼承Active Record的類別。

這是ARCA架構的重點所在。遵照少數的類別命名、資料表命名慣例(convention),

直接賦予這些entity類別多種能力:

  • 物件property存進資料庫的相關操作(CRUD)
  • entity間的relationship
  • 對資料庫取出的值做適當轉換

而開發者所要做的,只是寫好類別定義與幾個函式而已。

也正是Active Record的多功能,讓entity的建立如此迅速。

公司domain所需的商業行為,與entity相關的動作都是寫在此處。

Repository類別

封裝query logic,提供取得entity(一或多個)的不同方法。

不論是Rails的Active Record還是Laravel的Eloquent,都有提供查詢(query)的功能。

舉例來說,想要拿出新舊排序過的精選文章,可以這樣寫:

但是這幾行程式碼很可能常常用到,會到處重複。

這時就可以用repository封裝這段code:

以這段封裝來講,不但讓controller內的code可讀性提昇,同時還提供取出不同數量的參數功能。

ARCA內的repository都依賴Active Record提供的query實作,

所以建立一個abstract class再讓所有repository都繼承它是一個好主意。

參考這份EloquentRepositoryArticleRepository實作。

Presenter類別

封裝與business logic無關,只與呈現(presentation)相關的logic。

entity相關的名稱、金額、日期,常常在不同地方有不同呈現格式。

將這些寫成entity的function可不是好主意。

這類presentation logic可以獨立成presenter類別。

(或稱View Objects…。注意:軟體名詞在不同社群的用法極度混亂。)

概念上只是將entity塞進presenter,也就是decorator pattern

實作上可以依不同語言、框架的特色發揮創意。

像是這套善用了PHP特性,讓presentation 相關的code明確分離。

Presenter長這樣:

接著可以從entity直接使用presenter實體,呼叫presentation logic。

Form類別

處理使用者從HTTP表單填入的資料,封裝驗證(validation) logic。

web application少不了大量的運用html表單與使用者互動。

針對user input的驗證 logic(例如字串長度、日期、金額大小)可以抽出來成為Form類別。

(在Laravel 5稱為Form Request,在Rails社群稱為Form object

原理上來說,就是將表單傳來的參數傳給Form類別,在裡面驗證過後才決定是否執行下一步驟。

舉例來說,這件事若在controller內進行,看起來就像:

而Form類別的實作則依照語言/框架的不同各自發揮。

舉例:Rails可用VirtusLaravel可應用Validator

Service資料夾

擺放跨domain概念的程式碼。

多個service類別會放在此資料夾。

Service類別

概念上不屬於任何entity的business logic可以獨立成service。

通常是施加在多個entity上(來自不同domain)的複雜行為。

舉例來說,計算訂單的折扣與售價,通常會涉及訂單與禮券,並且運算邏輯較為複雜:

使用時,將entity傳進去即可:

Operation Model資料夾

圍繞某種行為概念的程式碼可以整理起來、放在一起。

當service開始依賴其他service時,代表這項行為開始變得複雜、開始造成理解的困難。

試著看清是哪個概念(這很不容易)、以它命名(也不容易),建立出Operation model。

舉例來說,電子商務公司提供報價會同時涉及「禮券折扣」、「出貨日期」、「總金額」的計算。

也就是QuotationService(報價)會用到DiscountService(計算折扣金額),

DueDateService(計算出貨日期), PriceService(計算總金額)。

這時可以將它們從Service資料夾移出、建立一個Quotation資料夾、再全部丟進去。

丟進去之後可以進行適當的refactoring,增加這幾個類別間的cohesion,降低之後理解的困難。

也可以進一步使用Facade Pattern封裝這些類別,建立一個統一對外的接口,

降低其他地方的程式碼與這個Operation Model的coupling。

以上面報價的例子來說:

結語

討論架構時,通常只討論概念,不將檔案結構列入討論。

我認為那樣的討論讓很多人有看沒有懂,所以將檔案結構一併納入ARCA說明了。

ARCA不是一種理論或是無敵的架構,只是一種用來搭配Active Record,

將常用元素(或說是Design Pattern)組合起來的一種方式而已。

可以將ARCA架構視為一種適合新創公司快速開發的、通用的中型基本架構。

隨著情況不同,做出調整、增減元素(例如好用的Factory類別)即可。

有任何疑問,或是有推薦的好用Design Pattern,覺得適合加進ARCA嗎?

歡迎在下方留言,或是在 Twitter @howtomakeaturn 和我討論。


Q&A

Q1: Service類別的例子怪怪的…明明可以寫進其中一種entity啊!像是這樣就可寫進訂單entity:

何時放在entity,何時獨立成service呢?

如你所說,一項行為究竟是否足夠「複雜」到需要獨立出來,的確屬於開發者的主觀判斷。

但是認定所有行為都不「複雜」的話,entity很容易變得極度肥胖喔。

Q2: 我覺得用Active Record會讓測試很難寫,有解法嗎?

這確實是Active Record的一大缺點:在測試時很難不去碰到database。

使用快速建立測資的套件,似乎是唯一能勉強接受的方法。

Ruby有factory_girl,PHP有Factory Muffin

參考看看吧。

(Photo via SuperCar-RoadTrip.fr, CC licensed)

codeigniter-logo

CodeIgniter不是OOP好老師

寫完框架不應該有「MODELS」資料夾之後,

收到林先生來信如下:

您好, 我剛拜讀完您的「框架不應該有「models」資料夾」
我對文章的內容有許多地方不理解…不…可以說是混亂
我本身較熟悉的框架只有CI, 平時的撰寫模式就是將使用者的輸入放在controller做驗證, 驗證通過之後才會將之丟進model class(或是您稱呼的 entities)做處理, 丟進model class處理時會有一些除了真正該執行的任務外以外的行為(像是註冊新會員時我會需要檢查帳號是否已被使用或是不是黑名單), 根據model class 的回應結果決定controller該輸出什麼結果或view
然後我看到了「抽出行為邏輯變成Service Objects、抽出表單驗證邏輯變成Form Objects、抽出資料庫查詢邏輯變成Query Objects、抽出呈現邏輯變成View Objects…聽起來真棒,也確實很有幫助,不是嗎?」這一段, 您想表達的意思是否是將各種行為(驗證輸入資料、查詢/寫入資料庫或其他的行為邏輯)細分出來放在model(或像您所的依照專案/公司名稱做分類)底下, 而controller則是扮演操作這些entities角色?

我可以理解你的困惑,我可以理解你的混亂。

我有整整一年使用CodeIgniter開發。

當我第一次看到Reddit上頭大家的討論、當我第一次看到Laravel論壇的原始碼

我也是完全一頭霧水:這些人在用的PHP跟我在用的PHP是同一個嗎?

轉進Laravel社群幾個月之後,我終於知道問題出在哪了。請聽我娓娓道來。

CI不鼓勵你打造Entity

…驗證通過之後才會將之丟進model class(或是您稱呼的 entities)…

這句話不正確。CI的Model不是Entity。

…而controller則是扮演操作這些entities角色?

這句話不正確。service object, view object, form object都不是entity。它們是與entity相關的類別。

CI的Model並沒有代表「現實生活中的一種事物」,

它只是負責「把一張資料表的內容直接丟回去」而已。

以Blog Model舉例,假設它在資料表有title跟content欄位,但是沒有summary欄位。

如果我們需要文章簡介summary(由title加上content前100字組合而成),

在Laravel它會是這樣:

在Model之外用到Entity的時候,會是這樣:

你得到了$blog這個entity,它代表著一篇部落格文章。

這篇文章很樂意告訴你它的summary。

但是在CI呢?

從CI的Model拿到的東西,只代表「跟文章相關的一坨資料」。可以是Array或是stdClass。

並不代表一篇部落格文章,也無法具有「行為」。

CI的Model不是Entity。

我提到的Service Objects、Query Objects、View Objects都是圍繞著Entity打轉,而CI本身只幫你拉資料出來,沒幫你做Entity。也難怪會一團混亂了。

怎麼會這樣?

這裡的Entity說穿了就是將資料庫的資料轉換成物件而已。

可以自己寫Class手動將資料庫的value一個一個轉成物件的property。這麼做又麻煩又緩慢。

常見的作法是直接用ORM,可以是Data Mapper Pattern或是更常見的Active Record Pattern。

許多框架都會提供Active Record Pattern的實作,Laravel, Rails都是。

有Entity可以幹嘛?

可以讓程式碼更優雅。

我就直接拿View Objects、Query Objects、Form Objects做舉例了。

View Objects

這個View Object實作為例,

你可以把User Entity中只與「呈現」相關的logic抽出來成這種類別:

使用的時候這樣即可:

Query Objects

這個Query Object實作為例,

你可以把取得文章的多種不同方式logic獨立出來,成為這樣的class:

在使用的時候,只要這樣即可:

Form Objects

Form Objects倒是跟entity比較無關,但我還是舉例給你看它長相。

這個Form Object實作為例,

你可以把建立Article的表單驗證logic抽出來成這種類別:

在使用的時候,只要這樣即可:

以上,你會發現各式各樣的logic都可以拆分成獨立class,讓思維上比較OOP。

controller裡面的code可讀性也高很多。

這些在CI中比較難做到。CodeIgniter不是OOP好老師。

當然了,有些人很討厭OOP,對它一點興趣也沒有。看著辦吧。


Q&A

Q1: 可是CI有Active Record類別啊?

CI的Active Record類別是整個CI社群最悲慘的誤會之一。

CI的Active Record完全是叫爽的。

那根本沒有實作Active Record Pattern,只是個陽春的Query Builder(幫你打造SQL query的助手)而已。

CodeIgniter’s “Active Record” isn’t what the real active record is all about.

You’re also being lead astray by CI’s rather confusing name ActiveRecord, which actually doesn’t implement the ActiveRecord pattern.

CodeIgniter has a query builder it calls “ActiveRecord”, but which does not implement the Active Record pattern.

CI的狗屁Active Record就是一個對使用者有害、混淆觀念、沒人要點破、

大家不求甚解、為了行銷而存在的垃圾名詞。

業界不像學界嚴謹,大家永遠為了行銷在瞎搞,這種狗屁到處都是。

「MVC」就是其中程度最誇張的例子。

Q2: 我整篇文章都看不太懂,我現在更緊張了。

別急,慢慢來。OOP要寫得優雅,本來就非常不容易。

有空去摸摸看Laravel吧,感受一下,也許會有靈感。

有問題可以到Laravel台灣發問唷。


社群看法(2015-4-6)

PHP台灣

model

框架不應該有「models」資料夾

各大back-end framework幾乎都採用了「MVC」架構。

它們至少會有「views」、「 controllers」、「 models」三個資料夾。

「views」跟「controllers」沒太大問題,但是「models」資料夾根本不該存在。

我要對這些框架提出嚴厲指控:

「models」資料夾的存在是一種錯誤的架構設計。它不但阻礙新手學習,還會傷害scalability。

任何back-end framework都不應該有「models」資料夾。

我會在這篇文章解釋理由,並且提出改善架構設計的幾個方向。

哪些框架有「models」資料夾?

光是我接觸過的Ruby、PHP框架,就至少有:

  • Rails(Ruby)
  • CodeIgniter(PHP)
  • Yii(PHP)

除此之外,像是Java, Python還是什麼語言,

一定也有框架做這種事:放一個「models」資料夾在那邊。

這真是大錯特錯。

你到底要放什麼東西到「models」資料夾裡面?你覺得Model是什麼?

Model是什麼?

撇開我之前提到的MVC正名爭議不談,光是MVC的M該如何解釋就已經是個大哉問。

看看這則出色的Stack Overflow問答:

How should a model be structured in MVC?

Model是layer、它包羅萬象、它涵蓋你全部的business logic。

眾說紛紜中,這是我們唯一能有的共識。

Model難以定義、沒有絕對正確的架構設計。聽起來真令人洩氣,對吧?

不!這樣很好!這樣才對!軟體架構本來就是大哉問,有無限種可能的方法,

這也是我們所有人應該要一起討論和嘗試的地方。

而「models」資料夾卻嚴重妨礙我們討論、阻止我們思考,

它不但阻礙新手學習,還會傷害scalability。

「models」資料夾如何阻礙新手學習?

說明這段之前,我先定義一個名詞:「entity」。

我將entity定義成「代表現實生活中的一種事物」。

以常見的Active Record pattern來說,

類似這樣的東西,你一定看過。

user.rb、user.php、post.php,這些就是我所謂的「entity」。

也就是這些「entity」,讓新手容易誤以為「entity」就是MVC裡面的M。

錯!M是layer,entity只是M裡面的組成元素之一而已。

「models」資料夾的存在本身,會讓新手以為「弄幾個entity類別丟進去就搞定架構了」。

然後entity的行為、對entity做出的行為、關乎兩個以上entity的行為,管他什麼logic,

管他什麼行為,全部想辦法塞進entity類別。

下場通常就是:那些entity類別最後變得超肥胖、難以理解、動輒達到上千行程式碼。

我稱之為「胖胖entity」。

「models」資料夾帶給新手「model == entity」錯覺!

「models」資料夾誘惑新手去做出一堆胖胖entity!

「models」資料夾如何傷害scalability?

看看Rails社群這篇出名的文章:

7 Patterns to Refactor Fat ActiveRecord Models

幹得好!它點出「胖胖entity」的問題,並給出7個patterns去協助你設計軟體架構。

抽出行為邏輯變成Service Objects、抽出表單驗證邏輯變成Form Objects、抽出資料庫查詢邏輯變成Query Objects、抽出呈現邏輯變成View Objects…聽起來真棒,也確實很有幫助,不是嗎?

問題來了:抽出來的這些類別,到底要放哪裡?

我們看看文章下面comments提到的範例:GItLab

它的檔案結構如下:

新的問題來了:那個「models」資料夾到底代表什麼?它是我們包羅萬象的偉大Model layer嗎?那finders、services、uploaders為什麼跟models在同一層,而不是在models裡面?lib/底下的gitlab/又是怎麼回事?難道GitLab的商業邏輯也出現在lib?

這就是我想說的:「models」資料夾的存在從一開始就污染了架構設計。

它引誘人們把entity全丟進去。結果除了entity以外的東西,像是前面的Service Objects、 Form Objects、Query Objects、View Objects,還有後面的finders、services、uploaders、gitlab全都不知道放哪了。只好隨便亂放。

GItLab原始碼中的models有代表MVC的M嗎?怎麼不改名叫entities?

就算改了又如何?Model layer到底在哪裡?四分五裂、結構鬆散。

一團混亂的設計、難以理解的命名、與MVC的M不相容的檔案結構。

所以我說,「models」資料夾傷害scalability!

那該怎麼辦?

要解決這個問題,首先得要了解MVC是三個極度不對稱的存在。

V: 負責呈現UI
C: 負責接受request、請M處理、回傳response
M: 負責全部的business logic

M幾乎是你的整個application。

你可以在框架底下,找地方建一個空資料夾,用公司名稱或是專案名稱替它命名,

然後開始煩惱軟體架構這件事。

好好煩惱entity要放在哪裡、Service Objects、 Form Objects、Query Objects、View Objects、finders、services、uploaders這些要放哪裡,彼此又要怎麼分門別類。

MVC不是萬靈丹,只是軟體架構的入門磚。

架構設計本來就是這麼難,OOP本來就是這麼難。

恭喜你,至少你跨出第一步了:

你不再把一堆胖胖entity丟進「models」資料夾,然後覺得設計完軟體架構了。


Q&A

Q1: 「models」資料夾毫無優點嗎?

「models」資料夾還是有少數優點。
它是一種quick and dirty作法,鼓勵你眼中只看見entity,然後把所有business logic全塞進裡面。
換句話說,它在小型的專案可以幫你節省時間。但它的優點也僅此而已。

Q2: 你的結論好空泛,什麼建一個公司名稱空資料夾啊。拜託給點方向?

沒問題,我給你兩個架構設計的參考方向。

第一個來自這篇文章:

Rails is Not Your Application

引用作者的話,核心精神如下:

Rails不是你的application。它可以是你的views還有資料來源,但不是你的application。把你的application放在Gem裡面或是lib/資料夾底下。

我不覺得這樣有很優雅,但至少點出一個可能方向,並且至少不再有models資料夾。

我第二個要給你的,是Laravel官方論壇原始碼

這個Laravel.io專案簡稱為Lio,結構如下:

光看檔案結構就很優雅。也正是我前面所說的:建一個空資料夾,用公司名稱或是專案名稱替它命名,然後開始煩惱架構設計這件事。

想想看Laravel官方論壇的原始碼為什麼長這樣吧。

Q3: 講得好像多有道理!我覺得你只是在鬼扯!框架的製作團隊都是業界大神,既然他們決定要有「models」資料夾,必定有它的正當性!

不,你錯了。那些業界大神只是背負了行銷框架的壓力。

他們為了滿足用戶的錯誤期待而委屈地放了個「models」資料夾在那。

但還是有高尚的人存在。PHP最被推崇的框架Laravel就沒有「models」資料夾。

向Laravel的Taylor Otwell致敬吧!

他不願成為殘害新手的幫凶,硬是把「models」拿掉了,

強迫你去思考:「軟體架構到底該長怎樣」。

你要自己在Laravel裡面做一個「models」資料夾,然後把那些entity class全丟進去嗎?

那你是自願把entity當成整個Model,真遺憾,

但別說是Laravel鼓勵你這麼做。Laravel盡力了。

Q4: 少自以為了解Taylor Otwell了!你憑什麼代替他發言!

Reddit上有一則 Why would anyone choose Laravel over Symfony or Silex?

Taylor Otwell本人親自做出回答。下文擷取自第四段:

我個人在開發Laravel 4的早期階段就想把「models」資料夾整個移除了。因為我不覺得它有用,也不覺得它能協助你設計軟體架構。而且它還會引誘人們掉入「model == database」的陷阱裡。所以,我希望你不要覺得我對架構設計很無知。我花了點時間才想清楚我到底想在PHP世界打造什麼。

Laravel實作Active Record Pattern,資料表映對到entity class。他指的「model == database」陷阱就是我說的「model == entity」錯覺。我並沒有代替他發言。

Q5: 我還是覺得,你沒有資格批評那麼多框架。「models」資料夾就是有某種正當性。除了Taylor Otwell,我看也沒有其他權威支持你的說法!

前面提到的出色Stack Overflow問答:

How should a model be structured in MVC?

作者是tereško。

Stack Overflow上關於MVC的幾個最高分討論,全都是由tereško解答

下文擷取自那篇出色問答的段落「What a model is NOT」:

model不是一個class,也不是任何一個單一物件。這是一個超級常見錯誤,因為大部分的框架都在助長這種誤解。

他選擇這樣帶過。我選擇正面指控。

然後我建議你討論事情的時候,不要太在乎權威還是前輩怎麼講。不如專注於討論事情本身。

Q6: 好啊!那來啊!照你的說法,「models」資料夾底下多放個「entities」資料夾不就搞定一切問題了?你果然是不切實際的理想主義者!最好是有框架幹這麼囉唆的事情!

有!它就是Cake(PHP)框架!

看看Cake在Model底下放了什麼:

看到了嗎?

Cake怕你把entity當成整個model,直接擺好幾個資料夾,逼你去思考entity跟model是什麼。

替這些用心良苦的框架歡呼吧!

Q7: 專注於討論事情本身是不是!那Cake的「models」資料夾就沒問題啊!你還說任何框架都不能有!

你看錯了,Cake沒有「models」資料夾,也沒有「Models」資料夾。它只有「Model」資料夾。

資料夾、package、資料庫table命名,都有一個關於單數/複數的原則可以參考:異質性與同質性。

你的「models」資料夾底下不再是同屬entity的class了,而是分為behavior、entity、以及其他你設計的分類,也就是異質,所以應該用單數命名。

參考這個連結:

Should package names be singular or plural?

簡單地說,既然model代表的是layer而非多個entity,資料夾命名上就應該用單數而非複數。

好吧,我這樣說有點太嚴苛了。

如果你知道自己在幹嘛的話,就繼續用你的「models」資料夾吧,我勉強可以接受。

Q8: 等等,不對勁…你整篇文章流露一股氣息…我覺得你不但反對「models」,你幾乎在否定MVC的價值?你怎麼可以覺得偉大的MVC沒有價值?

我前面提過,Taylor Otwell在Laravel 4移除了「models」資料夾,逼迫大家去思考「軟體架構」到底應該是什麼。

我告訴你第二件事。

你去逛Laravel官網,翻遍官網你都找不到「MVC」三個字。

MVC名氣多麼響亮!哪個framework不想打著MVC當作賣點?但Laravel拒絕這麼做。

我再告訴你第三件事。

2015年最新出爐的Laravel 5,它的views在resources/底下,controllers在app/Http/底下。一樣沒有models。

所以你神聖的MVC在Laravel 5底下長這樣:

你推崇的V跟C不再佔據檔案結構的核心位置了。你最愛的MVC現在看起來是如此渺小,

小到沒有討論價值,小到毫無意義可言。

「MVC是三個極度不對稱的存在」,這是個太過客氣的說法。

MVC這個觀念已經無法協助我們討論和思考了。放下它,往前走吧。

我來自Laravel社群。我們不聲稱自己擁戴MVC。

來把時間花在真正值得討論的概念上吧:你正在用的框架,架構合理嗎?框架有沒有擋住你的路?你在框架之下設計出的專案架構漂亮嗎?大中小型專案通用的架構存在嗎?如何分辨使用時機?怎麼做會最彈性?該怎麼描述某個框架才不會對新手揠苗助長?

放下你凡事都要套進MVC的執著,請直接思考「軟體架構」的本質。

啊,我看到MVC粉絲對Laravel 5的分析了:


MVC依然發揮重要的討論價值!我看到Controllers資料夾了!我看到views資料夾了!
剩下的十幾個資料夾全部統稱為Model!果然是豐富又厚重的layer!
我們來討論Model是什麼吧!MVC萬歲!

朋友,祝福你能得出有意義的結論。


小朱針對本文的延伸討論(2015-4-19補充)

[經驗談] 無招勝有招

(Photo via Alyssa L. Miller, CC licensed.)

double

一堆dependency怎麼測試?把替身叫出來。

我任職的翻譯公司需要線上報價功能。

其中,上傳文件到專案的功能,會同時計算字數、備份檔案到遠端。

程式碼大概是這樣:

由於這是個核心功能,我希望多寫幾個測試確保它的運作正常。

這樣的function該怎麼測試呢?

我是用Laravel + PHPUnit開發。首先,寫個基本測試,確保function沒有打錯變數名字、語法、不會有莫名exception噴出來:

問題來了,這個測試跑得非常慢。因為:

  • 它真的把檔案上傳到資料備份主機
  • 它真的用Reader數字數

我只想確認基本邏輯正常,並不在乎備份、計算字數功能是否正常。(那兩個應該另外測試。)

該怎麼辦呢?

是時候叫替身出來幫忙了!

Test Double

Test Double,我翻譯為「測試替身」,是Gerard Meszaros在2007年提出來的名詞。
它是軟體測試的常見手法之一,會做出類別的替身供測試使用。
又可細分為Fake, Mock, Stub, Dummy… 等等。
我們先不管差別何在,想辦法解決眼前的狗屁就好!

首先,我們得將原本的function改寫,把dependency改用參數傳入:

呼叫這個function的相關code,記得改寫成:

重點來了,測試怎麼寫?怎麼做出測試替身?

我想要一個不管收到什麼,一律回傳5555當作假id的$remote替身;
還要一個不管收到什麼,都說它計算出6666個字數的$reader替身。

PHPUnit本身就支援Test Double,只要這樣即可:

搞定!
成功避開遠端備份與計算字數的dependency!

測試完基本邏輯,接著可以寫各種不同的測試,去檢驗你想確認的部份,
全都不會真的上傳檔案到備份主機與計算字數!

利用test double的手法,你可以避開一大堆的dependency,測試到你原本以為不可能測試的地方。

很神奇吧!


Q&A

Q1: 把物件當dependency傳入真的好帥喔,這招叫什麼?

這招只是dependency injection的一種。

為了讓code能夠被測試,這招很常用到。

Q2: function傳入檔案$file又傳入$remote跟$reader。我怎麼覺得醜到爆啊?

這樣寫確實很醜。

通常物件dependency是先在constructor傳入,再從function內拿來用:

你常常看到的這種pattern就是在幹這件事:

但是我懶得改寫所有相關code,而且Laravel的Eloquent建構式又很難搞定物件dependency。

學到精神就好,寫法隨機應變。

Q3: 我還滿好奇,你提到的翻譯公司網站長怎樣?

http://wordcorp.net/

(Photo via AlmaArte Photography, CC licensed. )

confusion

MVC是一個巨大誤會

我是web工程師,從剛開始學MVC就深感困惑:

  • 怎麼每個地方說的MVC都不太一樣?
  • 有些文章講的MVC,跟我正在用的MVC,怎麼像完全不同的東西?

Model、Controller、View三者到底如何互動?真是一個定義不明、含糊不清的名詞。

這讓我研究了很久。最後,發覺它是一個嚴重的誤會。

這個誤會導致了學習和溝通上的代價,請聽我娓娓道來。

哪些是MVC?

web領域,不論前端(client-side)、後端(server-side)、不論什麼程式語言,幾乎所有framework都自稱、或被認為是「MVC」。

有哪些呢?

前端:Backbone.js、AngularJS、Ember.js…

後端:Ruby on Rails、CodeIgniter、Laravel、Django…

真的是這樣嗎?它們全都是MVC嗎?

MVC是什麼?

該怎麼定義MVC呢?

我們先來看看維基百科怎麼說:

MVC模式(Model-View-Controller)是軟體工程中的一種軟體架構模式,把軟體系統分為三個基本部分:模型(Model)、檢視(View)和控制器(Controller)。

嗯,跟大家說的一樣。我們繼續往下看:

模型(Model) 用於封裝與應用程式的業務邏輯相關的資料以及對資料的處理方法。「 Model 」有對資料直接存取的權力,例如對資料庫的存取。「Model」不依賴「View」和「Controller」,也就是說, Model 不關心它會被如何顯示或是如何被操作。但是 Model 中資料的變化一般會通過一種重新整理機制被公布。為了實作這種機制,那些用於監視此 Model 的 View 必須事先在此 Model 上註冊,從而,View 可以了解在資料 Model 上發生的改變。(比較:觀察者模式(軟體設計模式))

看起來有些陌生,整段描述跟你的web開發經驗完全不同,對嗎?

最大的疑問來自這句:

那些用於監視此 Model 的 View 必須事先在此 Model 上註冊,從而,View 可以了解在資料 Model 上發生的改變。(比較:觀察者模式(軟體設計模式))

後面直接叫你去看觀察者模式(observer pattern)。

問題來了:你有在view跟model之間實作observer pattern嗎?

也就是說,你的Model在資料改變之後,能主動通知View嗎?

沒有的話,就根本不符合MVC的定義。

全都不是MVC?

我們現在發現MVC有observer pattern這個必要條件了。

事情嚴重了起來。

client-side framework或許能夠符合這個條件。

Backbone.js官網範例來說,我們可以這樣在Model上註冊:

它的確實作了observer pattern。

但server-side framework呢?

你的Model如何能在發生改變之後去「主動通知」View?

你平常開發web哪有用到server push的技術?

所有server-side framework,從Ruby的Rails;PHP的CodeIgniter、Laravel;到Python的Django,他們全都不是MVC。

它們實作的,是昇陽電腦在1998年提出的「Model 2」。

什麼是Model 2?

Model 2名氣不大,在維基百科連中文條目都沒有。我們看看英文條目怎麼講

Model 2 is a complex design pattern used in the design of Java Web applications which separates the display of content from the logic used to obtain and manipulate the content.

In a Model 2 application, requests from the client browser are passed to the controller. The controller performs any logic necessary to obtain the correct content for display.

它針對web而設計,讓controller進行必要的程序之後,將資料塞進view去呈現。

正是我們server-side框架在做的事情。

也就是說,server-side目前只能實作Model 2;client-side可以實作Model 2,也可以實作MVC。

巨大的代價

web工程師最常碰的就是client-side跟server-side框架。結果整個業界把MVC跟Model 2混為一談,都說成MVC。

這帶來了什麼後果呢?

MVC變成一個從初學者到業界工程師,永遠說不清楚、下不了定義的名詞。

這件事對於學習和討論,造成了非常巨大的成本。(參考下方的Q1和Q2)

那該怎麼辦?

下次有初學者詢問「什麼是MVC」的時候,怎麼回答才不會害他回家之後「越查資料越混亂」?

Rails is not MVC的作者提出了三種解決方法:

第一個方法是聲稱MVC已經從原始意義改變了,Model 2也可以稱為MVC。如此一來,我們可以用「傳統MVC」或「真MVC」來描述原始的MVC。這是現在普遍的作法,但我不認為改變定義是一個好主意。這幾乎是越搞越亂。

第二個方法是到處推廣Rails其實是Model 2,MVC就留給…MVC吧。這很困難,但至少能保持定義不變。

第三個是直接忽略這些混亂。管它那麼多?

我個人覺得MVC這個詞已經沒救了,不管怎麼解釋都會帶給別人混亂。

當對方同時學習client-side跟server-side時,混亂更強烈。

我選擇這樣回答:


MVC有分很多種喔!網路上全部沒寫清楚,你一定看不懂。
沒關係,你只要知道View可以抽出來就好。
C跟M先別管,你先隨便瞎搞吧。


Q&A

Q1: 怎麼可能各大server-side framework都搞錯?

確實有人腦袋清醒得很,它就是Python的Django。

Django的官方文件內根本沒有「Controller」這個名詞。

看看Django官網的常見問題

Q: Django似乎是一個MVC框架,但你們把Controller命名為「View」,把View命名為「Template」。你們幹嘛不用標準命名啊?

A: (前略)…如果你真的很想要一個縮寫,你就說Django是一個MTV框架吧。Model、Template、View。這樣分比較有道理些。

Django不想變成搞亂MVC的幫凶,只好委屈地又發明了一個名詞「MTV」。

Q2: 那client-side框架有受影響嗎?

有。client-side框架也必須為MVC巨大誤會浪費一堆時間解釋。

看看Backbone.js官網的常見問答

Backbone跟「傳統MVC」的關聯何在?

(上略)
…我們來比較一下Backbone跟像是Rails這種server-side MVC框架的差別…
(下略)

Backbone實作了「傳統MVC」,卻被迫用「傳統MVC」來描述server-side的Model 2,然後花一堆篇幅解釋。

Q3: 知道Model 2的存在又如何?我現在依然一片混亂!

沒錯,Model 2跟MVC都用到Model、Controller、View三個名詞,所以看起來類似。

但是,我們不應該再把時間花在思考「MVC怎麼如此難懂」。

我們討論的重點,應該是「如何分辨MVC與Model 2」、「在server-side如何實作Model 2才漂亮」、「在client-side實作MVC跟Model 2的優劣分別何在」。

Q4: 好,那你現在回答我,「如何分辨MVC與Model 2」?

OK,就讓我拋磚引玉一下。
分別談談Model、View、Controller吧:

View

  • Model 2: 不具有行為,只是等別人塞資料進去的模板(template)。
  • MVC: 具有監視Model的行為,並以此去改變呈現(presentation)。

兩種View有沒有很像?跟張飛、岳飛一樣像。

看看Backbone.js官網的View範例。你server-side的View哪是長這樣?

Controller

  • Model 2: 接收請求與參數,轉交給Model處理,再把結果(最新的資料)塞進View。
  • MVC: 接收請求與參數,轉交給Model處理。沒其他事了。

兩種Controller有沒有很像?跟小狗、熱狗一樣像。

MVC的View跟Model 2的Controller可能還比較像一點。(隨便說說,千萬別這樣類比。)

Model

  • Model 2: 接收Controller傳來的參數,回傳結果。
  • MVC: 接收Controller傳來的參數,將結果通知View。

Model倒是有些類似。

總之,Model 2跟MVC除了三個部份的名字一樣之外,沒什麼關聯了。

Q5: 我決定徹底鑽研MVC跟Model 2的定義了。給我一些延伸閱讀?

MVC與Model 2的變異與結合

Rails is not MVC

Django appears to be a MVC framework, but you call the Controller the “view”, and the View the “template”. How come you don’t use the standard names?

How does Backbone relate to “traditional” MVC?

Model-View-Confusion part 1: The View gets its own data from the Model(2015-2-28新增。謝謝網友「王兲玐」的分享。)


一些社群的看法(2015-2-28)

附上幾個社群的連結,裡面有許多很棒的討論。

歡迎提供更多連結給我。

JavaScript.tw

PHP 台灣

Python Taiwan

劉依語 (Mosky)


良葛格針對本文的延伸討論(2015-4-19補充)

技術名詞紛爭多

(Photo via  Julia Wolf, CC License)