寫完框架不應該有「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.
CI的狗屁Active Record就是一個對使用者有害、混淆觀念、沒人要點破、
大家不求甚解、為了行銷而存在的垃圾名詞。
業界不像學界嚴謹,大家永遠為了行銷在瞎搞,這種狗屁到處都是。
「MVC」就是其中程度最誇張的例子。
Q2: 我整篇文章都看不太懂,我現在更緊張了。
別急,慢慢來。OOP要寫得優雅,本來就非常不容易。
有空去摸摸看Laravel吧,感受一下,也許會有靈感。
有問題可以到Laravel台灣發問唷。