由 howtomakeaturn 發表的所有文章

不如分享!

學生時代一起研究技術的幾個朋友,最近開始籌組co-writing blog。

他們都是極度有才華的工程師,blog上線之後一定會造福很多人。

寫blog分享的人永遠不嫌多,越多軟體工程師寫blog越好。

我從學生時代就一直這麼認為:不如分享!

讓我從三個角度解釋為什麼:

  • 排名與競爭的錯覺
  • 軟體工程師的本錢
  • 不必畏懼發表洞見

希望你看完之後,也會興起念頭:不如分享!

排名與競爭的錯覺

在職場上競爭,長期累積的個人實力,遠比短期內爭到的排名重要。

學生時代的十多年競爭排名經驗,卻是恰恰與此相反。

大學某年夏天,我見到同學拿著一份共同筆記要去影印。

我:「好厲害,你們幾個人一起做的嗎?可以借我看嗎?」

他一臉錯愕地拒絕:「共同筆記沒有在借的吧,這有公平性的問題。」

同年冬天,一個作業交不出來的朋友,臉色尷尬地問我:「作業該往哪個方向研究?」

我心想:「這傢伙是想跟我借作業吧!這好像有公平性的問題,對我不公平。」

然而,我又想了想:從這間學校離開之後,要面對的是全世界年輕人的競爭。

我今天幫助你學習,根本不害怕你反過來幹掉我,不然今天也不會是你向我請教。

但是你真的幹掉我又如何?那你一定掌握某種學習的訣竅,我到時再請教你就好了,

因為我是你的恩人,屆時你一定會無私報答我。

前者我根本沒吃虧;後者導致我變更強。穩賺不賠。

再說了,今天我排名比你前面畢業又如何?

面對全世界競爭的時候,我是用在校排名跟人競爭,還是用真正的實力跟人競爭?

最終我決定:不如分享!

從此一見到同學面有難色,作業直接遞過去:「拿去參考,要抄就抄。」

(但若被助教抓到害我零分,你就不用再跟我借了。)

更理想的期待是:如果大家都這麼做,那就是整個朋友圈一起變強、

整屆畢業生一起變強、整個國家一起變強。

軟體工程師的本錢

觀察了web programming產業一陣子,我怎麼看都是以「分享」為競爭的主舞台。

  • 若你在Stack Overflow有一萬分,業界怎麼評價你?
  • 若你在Github有一千顆星星,業界怎麼評價你?
  • 若你把洞見放在blog任人檢驗,業界怎麼評價你?

上面三點,不正是全世界軟體工程師奮力較勁的事嗎?

你在2015年,你在美好的產業:你不需要擔心技能被人學走而導致失去競爭力。

因為這行學無止盡、學習曲線沒有邊際效應遞減。

這就是軟體工程師的本錢:你只要擔心自己「分享的不夠用力」就可以了。

想找個與全世界競爭的舞台?不如分享!

不必畏懼發表洞見

不是業界大神,似乎就不應該寫blog?真是這樣嗎?

新手、中手就不應該發表洞見、暢所欲言他們的主張嗎?

國外工程師不都是互相撰文批評,然後讓圍觀的所有人一起學習嗎?

我在看一篇極具爭議的技術文章時,看到網友Zack打了很長一篇他的感想。

在我表達感激之意後,他回答:

Thanks. Writing helps me clarify my understanding.

對他來說,發表洞見不過是整理想法的一種手段。

同篇文章還有其他評論。網友Yannick Majoros留言:

Martin Fowler跟原po都只是毫無根據就愛發出噪音的幾個開發人員而已。

我看了大吃一驚,連忙追問:你哪來的資格批評Martin Fowler?你是認真的?

他回我:

我覺得他毫無學術根據。譬如說他對anemic models的抱怨就是一例。他不過就是有點經驗罷了。然後他試圖把個人意見說得像是事實一樣。如果原po把他的部落格文章寫成一本書,那就很像Martin Fowler那樣。

我根本不知道Yannick Majoros是誰、不知道他的實力、更不知道他的業界地位。

但是他這段話,對我就是極具啟發性。

讓我看完之後,更加覺得:不如分享!

(Photo via clappstar, CC licensed )

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它會是這樣:

class Blog extends Eloquent
{
    public function summary()
    {
        return $this->title . substr($this->content, 0, 100);
    }
}

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

$blog = Blog::find($id);

echo $blog->summary();

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

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

但是在CI呢?

class Blog_model extends CI_Model {
    //...
}

$this->load->model('Blog_model', 'blog');

$blog = $this->blog->get($id);

echo $blog->summary();// 噴error!我根本不是一篇文章!我只是一個陣列呀!

從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抽出來成這種類別:

use Laracasts\Presenter\Presenter;

class UserPresenter extends Presenter {

    public function fullName()
    {
        return $this->first . ' ' . $this->last;
    }

    public function accountAge()
    {
        return $this->created_at->diffForHumans();
    }

}

使用的時候這樣即可:

 echo "Hello, "  . $user->present()->fullName;

Query Objects

這個Query Object實作為例,

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

class ArticleRepository extends EloquentRepository
{
    public function getFeaturedArticles($count = 3)
    {
        return $this->model->with(['author', 'slug'])
                           ->where('status', '=', Article::STATUS_PUBLISHED)
                           ->orderBy('published_at', 'desc')
                           ->take($count)
                           ->get();
    }
    public function getAllPublishedByTagsPaginated($tags, $perPage = 10)
    {
        return $this->getAllPublishedByTagsQuery($tags)->paginate($perPage, ['articles.*']);
    }
    public function getAllPublishedByTagsQuery($tags)
    {
        $query = $this->model->with(['author'])
                             ->where('status', '=', Article::STATUS_PUBLISHED)
                             ->join('article_tag', 'articles.id', '=', 'article_tag.article_id')
                             ->orderBy('published_at', 'desc')
                             ->groupBy('articles.id');
        if ($tags->count() > 0) {
            $query->whereIn('article_tag.tag_id', $tags->lists('id'));
        }
        return $query;
    }
    public function getArticlesByAuthorPaginated(User $author, $perPage = 20)
    {
        return $this->getArticlesByAuthor($author)->paginate($perPage);
    }
    public function getArticlesByAuthor(User $author)
    {
        return $author->articles()->orderBy('articles.status', 'asc')->orderBy('published_at', 'desc')->orderBy('created_at', 'desc');
    }

}

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

    //這是controller中的一個function
    public function getIndex()
    {
        $repository = new ArticleRepository();
        // 取得已發布的特定標籤的特定分頁下的所有文章
        $articles = $repository->getAllPublishedByTagsPaginated($tags);
        // ...
    }

Form Objects

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

這個Form Object實作為例,

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

class ArticleForm extends FormModel
{
    protected $validationRules = [
        'title'           => 'required|min:10',
        'content'         => 'required',
        'laravel_version' => 'required',
        'status'          => 'required',
        'tags'            => 'required|max_tags:3',
    ];
    protected function beforeValidation()
    {
        \Validator::extend('max_tags', function($attribute, $tagIds, $params) {
            $maxCount = $params[0];
            $tagRepo = \App::make('Lio\Tags\TagRepository');
            $tags = $tagRepo->getTagsByIds($tagIds);
            if ($tags->count() > $maxCount) {
                return false;
            }
            return true;
        });
    }
}

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

   // 這是在controller內的一個function
   public function postCreate()
   {
        $form = new ArticleForm();
        if ( ! $form->isValid()) { //驗證表單內容是否滿足條件
             // 顯示錯誤訊息叫使用者重新輸入
        }
        // ...
   }

以上,你會發現各式各樣的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台灣

框架不應該有「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
// Rails
class User < ActiveRecord::Base
// user.php
// Laravel
class User extends Eloquent
// post.php
// Yii
class Post extends CActiveRecord

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

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

它的檔案結構如下:

/lib
    /gitlab
    /tasks
    /...
/app
    /assets	
    /controllers	
    /finders	
    /helpers	
    /mailers	
    /models	
    /services	
    /uploaders	
    /views	
    /workers

新的問題來了:那個「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,結構如下:

/app
    /controllers
    /views
    /Lio
        /Core
            /...
        /Accounts
            /User.php
            /UserPresenter.php
            /UserRepository.php
            /UserCreator.php
            /....
        /Articles
            /Article.php
            /ArticlePresenter.php
            /ArticleRepository.php
            /ArticleCreator.php
            /....
        /Comments
        /...

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

想想看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底下放了什麼:

/View
/Controller
/Model
    /Behavior
    /Entity
    /Table

看到了嗎?

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底下長這樣:

/resources
    /views
    /lang
    /assets
/app
    /Commands	
    /Console	
    /Events	
    /Exceptions	
    /Handlers	
    /Http	
    /Providers	
    /Services	
    /Https
        /Controllers
        /Middleware	
        /Requests

你推崇的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.)

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

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

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

程式碼大概是這樣:

upload($file);
        
        $reader = new Reader();
        
        $words = $reader->count($file);
        
        $document = new Document();
        
        $document->remote_file_id = $id;
        
        $document->words = $words;
        
        $document->save();
    }
}

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

這樣的function該怎麼測試呢?

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

addDocument($file);
                
        $this->assertGreaterThan(0, $project->words);
        
        $this->assertNotNull($project->remote_file_id);        
    }
    
}

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

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

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

該怎麼辦呢?

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

Test Double

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

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

class Project
{
    public function addDocument($file, $remote, $reader){        
        //....
        //....

        // $remote = new RemoteBackup();
        
        $id = $remote->upload($file);
        
        // $reader = new Reader();
        
        $words = $reader->count($file);
        
        $document = new Document();
        
        $document->remote_file_id = $id;
        
        $document->words = $words;
        
        $document->save();
    }
}

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

$project->addDocument($file, new RemoteBackup(), new Reader());

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

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

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

class ProjectTest extends TestCase {
    
    public function testAddDocument()
    {
        // 先設法做出測試用的$project與$file
        // ...

        $remote = $this->getMockBuilder('RemoteBackup')
                     ->getMock();

        $remote->method('upload')
             ->willReturn(5555);
             
        $reader = $this->getMockBuilder('Reader')
                     ->getMock();

        $reader->method('count')
             ->willReturn(6666);
             
        $project->addDocument($file, $remote, $reader);
                
        $this->assertGreaterThan(0, $project->words);
        
        $this->assertNotNull($project->remote_file_id);        
    }
    
}

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

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

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

很神奇吧!


Q&A

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

這招只是dependency injection的一種。

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

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

這樣寫確實很醜。

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

        // $id = $remote->upload($file);
        $id = $this->remote->upload($file);

        // $words = $reader->count($file);
        $words = $this->reader->count($file);

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

$stuff = new GoodClass(new ClassOne(), new ClassTwo());
    public function __constructor($dependencyOne, $dependencyTwo)
    {
        $this->dependencyOne = $dependencyOne;
        $this->dependencyTwo = $dependencyTwo;
    }

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

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

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

http://wordcorp.net/

(Photo via AlmaArte Photography, CC licensed. )

大學教授憑什麼出考古題

還在大學唸書的時候,每逢期中期末考,我就對這個問題充滿疑惑。

一直沒想通,卻也沒去請教過老師。

「為什麼考試題目照抄往年考題?」

出考古題導致的後果很明顯。

資質平庸的學生,被迫面臨抉擇:

要拿剩下的時間去死背考古題,讓自己什麼都學不到嗎?但是不這麼做就會輸給別人。

拿考古題來考試,毫無公平可言。

從國高中的段考,到基測、學測、指考、國考,

所有重要考試都嚴禁考題抄襲,否則一經披露,都會引起軒然大波。

看看幾個登上媒體的例子:

中山國中英語段考 試題爆抄襲(聯合報,2011)

教部:段考試卷 不可委外命題(立報,2010)

扯!普考抄襲特考考古題 一字不漏(TVBS,2013)

原民特考 試題抄襲成大5年前講義(自由時報,2014)

歷史爆抄襲 大考中心震怒(自由時報,2007)

如果我們無法接受國小老師、國中老師、高中老師,在任何一場重要考試照抄往年考題,

為什麼大學老師就可以?

大學教授憑什麼出考古題?

(Photo via Michiel, CC licensed)

你替controllers寫測試嗎?

前陣子看到替controllers寫測試的文章。摸了一陣子還是沒掌握。

跑去Reddit發問,得到好多回應。

因為獲益良多,所以在此翻譯討論串內容,和各位分享。

你替controllers寫測試嗎?

嗨!
我最近在學Domain Driven Design跟幾個測試技巧。
我替repositories、services、entities、factories寫各種測試來確保程式品質。
因為網站越來越依賴前端技術跟AJAX,我覺得也替controllers寫測試比較好。
但我又覺得測試controllers根本沒從controllers類別裡面initiate或是call任何東西。
只是寫一個測試去模擬對應controller action的內容而已。
這也能叫測試?這能帶來多少好處?
你們替controllers寫測試嗎?
誰能指點迷津?
感激不盡!

dohpaz42

軟體測試是一個超複雜大怪獸。每個人測試的方法都差很多。電腦理論都這樣的。
先簡單回答你的問題:測試所有東西。
完整回答比較複雜。我試著整理出來,請見諒。
測試有分很多種:unit tests,integration test,regression tests,feature tests,等等。根據測試的對象不同,你會使用不同的測試。
舉例來說,當你寫一個model,基本上你會用到unit test。你會替每個method可能走過的每條道路都寫測試。
如果你對這觀念不熟,就這樣想:若你的method會製造X種副作用,那你就得替這X種副作用寫測試。正確執行、失敗執行的情況都算。
unit test的定義就是測試application中的一個特定單位。如果測試碰到其他models或程式碼,你就mock那些物件(利用dependency injection)來忽略它們。若有個database model會寫入資料到database,我需要去管database layer是否產出正確的output嗎?大概不需要。你只需要注意正在測試的部份系統(System Under Test, SUT)會針對database的output正確反應。這代表你應該要對所有會影響系統運作的database output寫測試。只需要pass-through、proxy(或是任何不會造成副作用的方式)這樣的簡單方法,就能有效忽略那些code的可能走法。譬如說:

function fooProxy() {
    return $this->getDatabase()->query('SELECT * FROM foo');
}

這個method只是把application中另一層layer的結果丟回去,而那層layer你應該早就測過了。所以你不需要替fooProxy寫測試。

很好,感謝你保持耐心。正式回答前,我想先釐清這些。

controllers通常會與application中的多個部份一起合作。它們透過front controller、routing機制來運作,並用上models、views。很少會有專屬於controller的logic(這不代表controllers不需要測試)。要是有的話,那就測吧。測試在不同狀況下,他們與其他code的互動是否一如預期。這需要integration tests才行。

一個integration test不怎麼在乎系統運作的細節。它只在乎:如果XYZ放進去,那麼結果應該要是ZYX。拿REST API舉例:如果我丟一個GET request到/resource/a,我應該得到一個200 response,也許再加上body content。這樣算通過測試。如果我把幾個變數跟request一起丟出去,結果得到400 response,那就沒通過測試。不在乎系統是否碰到資料庫還是檔案什麼的,只在乎response是不是200。

正式回答你的問題:對,所有東西都應該測試。你會寫很多不同的test。這可能很瑣碎,甚至很無聊。但是當所有的tests都通過的時候,你終究會理解價值何在。

whtevn

我們會替controllers寫測試。但是integration tests,不是unit tests。我們會測所有api endpoints,然後記下他們在特定input下的各種response。我們也會測資料庫是否正確更新。這樣算是驗證了整個application。
這跟unit test要求徹底獨立的概念不同。從TDD的觀點,先寫unit tests是很合理。但是先寫integration tests更棒。它能協助你弄清楚功能,又不需要像unit test那樣有個逐漸遞增的feedback loop。

我們公司強調integration tests而非unit test。我們規定每個endpoint都必須要有integration test,而unit tests由工程師自行決定。確認特定的method運作正常當然重要。但我個人認為,確認所有API endpoint都符合規格,更重要。

我寫了一個integration test套件。我放在github上,但目前只有幾個範例有寫說明。

https://github.com/whtevn/routest/blob/master/sample/tests/succeed/routest-test.js

也放在npm上了。它不是外掛,只是一個免費工具。好像也算外掛。隨便啦。

https://www.npmjs.com/package/routest

mrargh

我們通常對models寫unit test,然後用web driver integration tests 測controllers – 這樣就都測到了。

philsturgeon

沒事幹才寫吧…但我總是有事幹。
我對libraries,models之類的寫unit test,然後用integration test測其餘部份。
對controllers寫unit test不是壞事,但不需要優先考慮。

WishCow

我不寫。我盡量在controllers內少放logic,把事情都交給services跟models。替controllers寫測試只是再模擬一次他們的互動而已。那樣是可以驗證參數與結果的關係,但對我來說太花功夫了,不划算。

chadcf

如果是API application我才替controllers寫測試。那些算functional tests。web介面的話,我會讓controllers內容少到不需要寫tests。大原則是讓controller action內容少到10行以內,只讓它建立物件、呼叫物件方法,然後回傳response。

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上註冊:

book.on({
  "change:title": titleView.update,
  "change:author": authorPane.update,
  "destroy": bookView.remove
});

它的確實作了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哪是長這樣?

var DocumentRow = Backbone.View.extend({

  tagName: "li",

  className: "document-row",

  events: {
    "click .icon":          "open",
    "click .button.edit":   "openEditDialog",
    "click .button.delete": "destroy"
  },

  initialize: function() {
    this.listenTo(this.model, "change", this.render);
  },

  render: function() {
    ...
  }

});

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)