開發application時,常常會有大量的呈現(presentation)邏輯。
像是日期格式、金額格式(是否穿插逗號)、名稱(是否需要大寫)等等。
把這些logic寫在model的話,很容易就會導致model過胖。
舉例來說,如果文章model在不同地方需要呈現日期、日期時間、西方格式、台灣格式,
model就會長這樣(以Laravel Eloquent舉例):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Article extends Eloquent { public function getDate(){/*...*/} // 呈現給台灣地區的時間格式 public function getTaiwaneseDateTime(){/*...*/} // 呈現給歐美地區的時間格式 public function getWesternDateTime(){/*...*/} public function getTaiwaneseDate(){/*...*/} public function getWesternDate(){/*...*/} } |
要將這類presentation logic抽出來,許多框架採用了「helper function」的作法,
也就是定義好幾個functions用來處理呈現邏輯。
除了helpers的作法之外,還有一個非常簡單有效的方法。
也就是使用Decorator pattern來封裝出presenter,像這樣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class ArticlePresenter { protected $article; public function __construct(Article $article) { $this->article = $article; } // 呈現給台灣地區的時間格式 public function getTaiwaneseDateTime(){ return date('Y-m-d', $this->article->created_at); } // 呈現給歐美地區的時間格式 public function getWesternDateTime(){/*...*/} public function getTaiwaneseDate(){/*...*/} public function getWesternDate(){/*...*/} } |
概念上只是把原本的model從constructor傳進去,原本的function搬過去,
把$this改寫成$this->article即可。
然而,這樣會導致view裡面需要初始化物件:
1 2 3 4 |
@foreach($articles as $article) <?php $presenter = new ArticlePresenter($article); ?> 發文日期:{{ $presenter->getTaiwaneseDate() }} @endforeach |
在view裡面做這種事,怎麼看都是很醜的。
多寫一個function即可改善:
1 2 3 4 5 6 7 |
class Article extends Eloquent { public function present() { return new ArticlePresenter($this); } } |
view內會變成這樣:
1 2 3 |
@foreach($articles as $article) 發文日期:{{ $article->present()->getTaiwaneseDate() }} @endforeach |
新的問題來了:多次呼叫present會浪費記憶體。
可以這樣改善:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Article extends Eloquent { protected $presenterInstance; public function present() { if ( ! $this->presenterInstance) { $this->presenterInstance = new ArticlePresenter($this); } return $this->presenterInstance; } } |
多個model都使用presenter的話,這段code會重複。善用PHP trait可以解決這個問題:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
trait PresentableTrait { protected $presenterInstance; public function present() { // ... if ( ! $this->presenterInstance) { $this->presenterInstance = new $this->presenter($this); } return $this->presenterInstance; } } |
Laravel社群至少有3套成熟的presenter實作:
https://github.com/laracasts/Presenter
https://github.com/robclancy/presenter
https://github.com/laravel-auto-presenter/laravel-auto-presenter
有的功能豐富、有的比較陽春,但是概念上一樣,都是decorator pattern的應用。
下次你的application包含大量presentation logic時,不妨試試看這個作法。
(Photo via Susanne Nilsson, CC licensed)