使用 Presenter 輔助 View

若將顯示邏輯都寫在 view,會造成 view 肥大而難以維護,基於 SOLID 原則,我們應該使用 Presenter 模式輔助 view,將相關的顯示邏輯封裝在不同的 presenter,方便中大型專案的維護。

Version


Laravel 5.1.22

顯示邏輯


顯示邏輯中,常見的如 :

  1. 將資料顯示不同資料 : 如性別欄位為M,就顯示Mr.,若性別欄位為F,就顯示Mrs.
  2. 是否顯示某些資料 : 如根據欄位值是否為Y,要不要顯示該欄位
  3. 依需求顯示不同格式 : 如依照不同的語系,顯示不同的日期格式

Presenter


將資料顯示不同資料

性別欄位為M,就顯示Mr.,若性別欄位為F,就顯示Mrs.,初學者常會直接用 blade 寫在 view。

在中大型專案,會有幾個問題 :

  1. 由於 blade 與 HTML 夾雜,不太適合寫太複雜的程式,只適合做一些簡單的 binding,否則很容易流於傳統 PHP 的義大利麵程式。
  2. 無法對顯示邏輯做重構物件導向

比較好的方式是使用 presenter :

  1. 將相依物件注入到 presenter。
  2. 在 presenter 內寫格式轉換。
  3. 將 presenter 注入到 view。

UserPresenter.php

app/Presenters/UserPresenter.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
namespace App\Presenters;

class UserPresenter
{

/**
* 性別欄位為M,就顯示Mr.,若性別欄位為F,就顯示Mrs.
* @param string $gender
* @param string $name
* @return string
*/

public function getFullName($gender, $name)
{

if ($gender == 'M')
$fullName = 'Mr. ' . $name;
else
$fullName = 'Mrs. ' . $name;

return $fullName;
}
}

將原本在 blade 用 @if...@else...@endif 寫的邏輯,改寫在 presenter。

使用 @inject() 注入 UserPresenter,讓 view 也可以如 controller 一樣使用注入的物件。

將來無論顯示邏輯怎麼修改,都不用改到 blade,直接在 presenter 內修改。

改用這種寫法,有幾個優點 :
  1. 將資料顯示不同格式的顯示邏輯改寫在 presenter,解決寫在 blade 不容易維護的問題。
  2. 可對顯示邏輯做重構物件導向

是否顯示某些資料

根據欄位值是否為Y,要不要顯示該欄位,初學者常會直接用 blade 寫在 view。

在中大型專案,會有幾個問題 :

  1. 由於 blade 與 HTML 夾雜,不太適合寫太複雜的程式,只適合做一些簡單的 binding,否則很容易流於傳統 PHP 的義大利麵程式。
  2. 無法對顯示邏輯做重構物件導向

比較好的方式是使用 presenter :

  1. 將相依物件注入到 presenter。
  2. 在 presenter 內寫格式轉換。
  3. 將 presenter 注入到 view。

UserPresenter.php

app/Presenters/UserPresenter.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
namespace App\Presenters;

use App\User;

class UserPresenter
{

/**
* 是否顯示email
* @param User $user
* @return string
*/

public function showEmail(User $user)
{

if ($user->show_email == 'Y')
return '<h2>' . $user->email . '</h2>';
else
return '';
}
}

@if() 的 boolean 判斷,封裝在 presenter 內。

改由 presenter 負責送出 HTML。

使用 @inject() 注入 UserPresenter,讓 view 也可以如 controller 一樣使用注入的物件。

{!! !!!} 會保有原來 HTML 格式。

將來無論顯示邏輯怎麼修改,都不用改到 blade,直接在 presenter 內修改。

改用這種寫法,有幾個優點 :
  1. 是否顯示某些資料的顯示邏輯改寫在 presenter,解決寫在 blade 不容易維護的問題。
  2. 可對顯示邏輯做重構物件導向

依需求顯示不同格式

依照不同的語系,顯示不同的日期格式,初學者常會直接用 blade 寫在 view。1 1blade、mutator 與 presenter 的比較,詳細請參考如何依各種語言顯示不同日期格式?

blade很難維護

在中大型專案,會有幾個問題 :

  1. 由於 blade 與 HTML 夾雜,不太適合寫太複雜的程式,只適合做一些簡單的 binding,否則很容易流於傳統 PHP 的義大利麵程式。
  2. 無法對顯示邏輯做重構物件導向
  3. 違反SOLID開放封閉原則 : 若將來要支援新的語系,只能不斷地在 blade 新增 if...else2 2開放封閉原則 : 軟體中的類別、函式對於擴展是開放的,對於修改是封閉的。

比較好的方式是使用 presenter :

  1. 將相依物件注入到 presenter。
  2. 在 presenter 內寫不同的日期格式轉換邏輯。
  3. 將 presenter 注入到 view。

DateFormatPresenterInterface.php

app/Presenters/DateFormatPresenterInterface.php
1
2
3
4
5
6
7
8
9
10
11
12
13
namespace App\Presenters;

use Carbon\Carbon;

interface DateFormatPresenterInterface
{

/**
* 顯示日期格式
* @param Carbon $date
* @return string
*/

public function showDateFormat(Carbon $date) : string;
}

定義了 showDateFormat(),各語言必須在 showDateFormat() 使用 Carbon 的 format()去轉換日期格式。

DateFormatPresenter_uk.php

app/Presenters/DateFormatPresenter_uk.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace App\Presenters;

use Carbon\Carbon;

class DateFormatPresenter_uk implements DateFormatPresenterInterface
{

/**
* 顯示日期格式
* @param Carbon $date
* @return string
*/

public function showDateFormat(Carbon $date) : string
{

return $date->format('d M, Y');
}
}

DateFormatPresenter_uk 實現了 DateFormatPresenterInterface,並將轉換成英國日期格式的 Carbon 的 format() 寫在 showDateFormat() 內。

DateFormatPresenter_tw.php

app/Presenters/DateFormatPresenter_tw.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace App\Presenters;

use Carbon\Carbon;

class DateFormatPresenter_tw implements DateFormatPresenterInterface
{

/**
* 顯示日期格式
* @param Carbon $date
* @return string
*/

public function showDateFormat(Carbon $date) : string
{

return $date->format('Y/m/d');
}
}

DateFormatPresenter_tw 實現了 DateFormatPresenterInterface,並將轉換成台灣日期格式的 Carbon 的 format() 寫在 showDateFormat() 內。

DateFormatPresenter_us.php

app/Presenters/DateFormatPresenter_us.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace App\Presenters;

use Carbon\Carbon;

class DateFormatPresenter_us implements DateFormatPresenterInterface
{

/**
* 顯示日期格式
* @param Carbon $date
* @return string
*/

public function showDateFormat(Carbon $date) : string
{

return $date->format('M d, Y');
}
}

DateFormatPresenter_us 實現了 DateFormatPresenterInterface,並將轉換成美國日期格式的 Carbon 的 format() 寫在 showDateFormat() 內。

Presenter 工廠

由於每個語言的日期格式都是一個 presenter 物件,那勢必遇到一個最基本的問題 : 我們必須根據不同的語言去 new 不同的 presenter 物件,直覺我們可能會在 controller 去 new presenter。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function index(Request $request)
{

$users = $this->userRepository->getAgeLargerThan(10);

$locale = $request['lang'];

if ($locale === 'uk') {
$presenter = new DateFormatPresenter_uk();
} elseif ($locale === 'tw') {
$presenter = new DateFormatPresenter_tw();
} else {
$presenter = new DateFormatPresenter_us();
}

return view('users.index', compact('users'));
}

這種寫法雖然可行,但有幾個問題 :

  1. 違反 SOLID開放封閉原則 : 若將來有新的語言需求,只能不斷去修改 index(),然後不斷的新增 elseif,就算改用 switch 也是一樣。
  2. 違反 SOLID依賴反轉原則 : controller 直接根據語言去 new 相對應的 class,高層直接相依於低層,直接將實作寫死在程式中。3 3依賴反轉原則 : 高層不應該依賴於低層,兩者都應該要依賴抽象;抽象不要依賴細節,細節要依賴抽象。
  3. 無法單元測試 : 由於 presenter 直接 new 在 controller,因此要測試時,無法對 presenter 做 mock。

比較好的方式是使用 Factory Pattern

DataFormatPresenterFactory.php

app/Presenters/DateFormatPresenterFactory.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace App\Presenters;

use Illuminate\Support\Facades\App;

class DateFormatPresenterFactory
{

/**
* @param string $locale
*/

public static function bind(string $locale)
{

App::bind(DateFormatPresenterInterface::class,
'MyBlog\Presenters\DateFormatPresenter_' . $locale);
}
}

使用 Presenter Factorycreate() 去取代 new 建立物件。

這裡當然可以在 create() 去寫 if...elseif 去建立 presenter 物件,不過這樣會違反 SOLID開放封閉原則,比較好的方式是改用 App::bind(),直接根據 $locale去 binding 相對應的 class,這樣無論在怎麼新增語言與日期格式,controller 與 Presenter Factory 都不用做任何修改,完全符合開放封閉原則。

Controller

UserController.php

app/Http/Controllers/UserController.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
namespace App\Http\Controllers;

use App\Http\Requests;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use MyBlog\Presenters\DateFormatPresenterFactory;
use MyBlog\Repositories\UserRepository;

class UserController extends Controller
{

/** @var UserRepository 注入的UserRepository */
protected $userRepository;

/**
* UserController constructor.
* @param UserRepository $userRepository
*/

public function __construct(UserRepository $userRepository)
{

$this->userRepository = $userRepository;
}

/**
* Display a listing of the resource.
* @param Request $request
* @param DateFormatPresenterFactory $dateFormatPresenterFactory
* @return \Illuminate\Http\Response
*/

public function index(Request $request)
{

$users = $this->userRepository->getAgeLargerThan(10);
$locale = ($request['lang']) ? $request['lang'] : 'us';
$dateFormatPresenterFactory::bind($locale);

return view('users.index', compact('users'));
}
}

11 行

1
2
3
4
5
6
7
8
9
10
11
/** @var  UserRepository 注入的UserRepository */
protected $userRepository;

/**
* UserController constructor.
* @param UserRepository $userRepository
*/

public function __construct(UserRepository $userRepository)
{

$this->userRepository = $userRepository;
}

將相依的 UserRepository 注入到 UserController

23 行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Display a listing of the resource.
* @param Request $request
* @param DateFormatPresenterFactory $dateFormatPresenterFactory
* @return \Illuminate\Http\Response
*/

public function index(Request $request)
{

$users = $this->userRepository->getAgeLargerThan(10);
$locale = ($request['lang']) ? $request['lang'] : 'us';
$dateFormatPresenterFactory::bind($locale);

return view('users.index', compact('users'));
}

使用 $dateFormatPresenterFactory::bind() 切換 App::bind() 的 presenter 物件,如此 controller 將開放封閉,將來有新的語言需求,也不用修改 controller。

我們可以發現改用 factory pattern 之後,controller 有了以下的優點 :

  1. 符合 SOLID開放封閉 原則: 若將來有新的語言需求,controller 完全不用做任何修改。
  2. 符合SOLID依賴反轉原則 : controller 不再直接相依於 presenter,而是改由 factory 去建立 presenter。
  3. 可以做單元測試 : 可直接對各 presenter 做單元測試,不需要跑驗收測試就可以測試顯示邏輯。

Blade

使用 @inject 注入 presenter,讓 view 也可以如 controller 一樣使用注入的物件。

使用 presenter 的 showDateFormat() 將日期轉成想要的格式。

改用這種寫法,有幾個優點 :
  1. 依需求顯示不同格式的顯示邏輯改寫在 presenter,解決寫在 blade 不容易維護的問題。
  2. 可對顯示邏輯做重構物件導向
  3. 符合 SOLID開放封閉原則: 將來若有新的語言,對於擴展是開放的,只要新增 class 實踐 DateFormatPresenterInterface 即可;對於修改是封閉的,controller、factory interface、factory 與 view 都不用做任何修改。
  4. 不單只有 PHP 可以使用 service container,連 blade 也可以使用 service container,甚至搭配 service provider。
  5. 可單獨對 presenter 的顯示邏輯做單元測試。

View


若使用了 presenter 輔助 blade,再搭配 @inject() 注入到 view,view 就會非常乾淨,可專心處理將資料binding到HTML 的職責。

將來只有 layout 改變才會動到 blade,若是顯示邏輯改變都是修改 presenter。

Conclusion


  • Presenter 使得顯示邏輯從 blade 中解放,不僅更容易維護、更容易擴展、更容易重複使用,且更容易測試。

Sample Code


完整的範例可以在我的 GitHub 上找到。

2015-12-19