若将显示逻辑都写在 view,会造成 view 肥大而难以维护,基于 SOLID 原则,我们应该使用 Presenter 模式辅助 view,将相关的显示逻辑封装在不同的 presenter,方便中大型项目的维护。
Version
Laravel 5.1.22
显示逻辑
显示逻辑中,常见的如 :
- 将数据显示不同资料 : 如
性别字段为M,就显示Mr.,若性别字段为F,就显示Mrs.
。 - 是否显示某些数据 : 如
根据域值是否为Y,要不要显示该字段
。 - 依需求显示不同格式 : 如
依照不同的语系,显示不同的日期格式
。
Presenter
将数据显示不同资料
如性别字段为M,就显示Mr.,若性别字段为F,就显示Mrs.
,初学者常会直接用 blade 写在 view。
在中大型项目,会有几个问题 :
- 由于 blade 与 HTML 夹杂,不太适合写太复杂的程序,只适合做一些简单的 binding,否则很容易流于传统 PHP 的意大利面程序。
- 无法对显示逻辑做重构与面向对象。
比较好的方式是使用 presenter :
- 将相依物件注入到 presenter。
- 在 presenter 内写格式转换。
- 将 presenter 注入到 view。
UserPresenter.php
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 内修改。
将数据显示不同格式
的显示逻辑改写在 presenter,解决写在 blade 不容易维护的问题。- 可对显示逻辑做重构与面向对象。
是否显示某些数据
如根据域值是否为Y,要不要显示该字段
,初学者常会直接用 blade 写在 view。
在中大型项目,会有几个问题 :
- 由于 blade 与 HTML 夹杂,不太适合写太复杂的程序,只适合做一些简单的 binding,否则很容易流于传统 PHP 的意大利面程序。
- 无法对显示逻辑做重构与面向对象。
比较好的方式是使用 presenter :
- 将相依物件注入到 presenter。
- 在 presenter 内写格式转换。
- 将 presenter 注入到 view。
UserPresenter.php
app/Presenters/UserPresenter.php
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 ''; } }
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
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
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
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。
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')); }
这种写法虽然可行,但有几个问题 :
- 违反 SOLID 的开放封闭原则 : 若将来有新的语言需求,只能不断去修改
index()
,然后不断的新增elseif
,就算改用switch
也是一样。 - 违反 SOLID 的依赖反转原则 : controller 直接根据语言去 new 相对应的 class,高层直接相依于低层,直接将实作写死在程序中。33依赖反转原则 : 高层不应该依赖于低层,两者都应该要依赖抽象;抽象不要依赖细节,细节要依赖抽象。
- 无法单元测试 : 由于 presenter 直接 new 在 controller,因此要测试时,无法对 presenter 做 mock。
比较好的方式是使用 Factory Pattern。
DataFormatPresenterFactory.php
app/Presenters/DateFormatPresenterFactory.php
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 Factory
的 create()
去取代 new 建立对象。
这里当然可以在 create()
去写 if...elseif
去建立 presenter 对象,不过这样会违反 SOLID的开放封闭原则,比较好的方式是改用 App::bind()
,直接根据 $locale
去 binding 相对应的 class,这样无论在怎么新增语言与日期格式,controller 与 Presenter Factory 都不用做任何修改,完全符合开放封闭原则。
Controller
UserController.php
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 行
/** @var UserRepository 注入的UserRepository */ protected $userRepository; /** * UserController constructor. * @param UserRepository $userRepository */ public function __construct(UserRepository $userRepository) { $this->userRepository = $userRepository; }
将相依的 UserRepository
注入到 UserController
。
23 行
/** * 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 有了以下的优点 :
- 符合 SOLID 的开放封闭 原则: 若将来有新的语言需求,controller 完全不用做任何修改。
- 符合SOLID 的依赖反转原则 : controller 不再直接相依于 presenter,而是改由 factory 去建立 presenter。
- 可以做单元测试 : 可直接对各 presenter 做单元测试,不需要跑验收测试就可以测试显示逻辑。
Blade
使用 @inject
注入 presenter,让 view 也可以如 controller 一样使用注入的对象。
使用 presenter 的 showDateFormat()
将日期转成想要的格式。
- 将
依需求显示不同格式
的显示逻辑改写在 presenter,解决写在 blade 不容易维护的问题。 - 可对显示逻辑做重构与面向对象。
- 符合 SOLID 的开放封闭原则: 将来若有新的语言,对于扩展是开放的,只要新增 class 实践
DateFormatPresenterInterface
即可;对于修改是封闭的,controller、factory interface、factory 与 view 都不用做任何修改。 - 不单只有 PHP 可以使用 service container,连 blade 也可以使用 service container,甚至搭配 service provider。
- 可单独对 presenter 的显示逻辑做单元测试。
View
若使用了 presenter 辅助 blade,再搭配 @inject()
注入到 view,view 就会非常干净,可专心处理将数据binding到HTML
的职责。
将来只有 layout 改变才会动到 blade,若是显示逻辑改变都是修改 presenter。
Conclusion
- Presenter 使得显示逻辑从 blade 中解放,不仅更容易维护、更容易扩展、更容易重复使用,且更容易测试。
Sample Code
完整的范例可以在我的GitHub上找到。