胖胖 Model 的減重方法:Presenter

開發application時,常常會有大量的呈現(presentation)邏輯。

像是日期格式、金額格式(是否穿插逗號)、名稱(是否需要大寫)等等。

把這些logic寫在model的話,很容易就會導致model過胖。

舉例來說,如果文章model在不同地方需要呈現日期、日期時間、西方格式、台灣格式,

model就會長這樣(以Laravel Eloquent舉例):

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,像這樣:

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裡面需要初始化物件:

@foreach($articles as $article)
    
    發文日期:{{ $presenter->getTaiwaneseDate() }}
@endforeach

在view裡面做這種事,怎麼看都是很醜的。

多寫一個function即可改善:

class Article extends Eloquent
{
    public function present()
    {
        return new ArticlePresenter($this);
    }
}

view內會變成這樣:

@foreach($articles as $article)
    發文日期:{{ $article->present()->getTaiwaneseDate() }}
@endforeach

新的問題來了:多次呼叫present會浪費記憶體。

可以這樣改善:

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可以解決這個問題:

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)