如何使用Presenter模式
若将显示逻辑都写在 View,会造成 View 代码臃肿而难以维护,基于 SOLID 原则,应该使用 Presenter 模式辅助 View,将相关的显示逻辑封装在不同的 Presenter ,方便中大型项目的维护。
版本
Lararvel 5.4.17
显示逻辑
在实际开发中,显示逻辑常见的如下:
将资料显示不同资料: 如
性别字段为 M,就显示 Mr.,若性别字段为 F,就显示 Mrs.
是否显示某些资料:如
根据字段值是否等于 T,要不要显示改字段
依需求显示不同格式:如
依不同的语系,显示不同的日期格式
Presenter
将资料显示不同资料
如 性别字段为 M,就显示 Mr.,若性别字段为 F,就显示 Mrs.
,我们可能会直接用 blade 写在 view 里,如下:
<!DOCTYPE html>
<html lang="{{ config('app.locale') }}">
<head>
<title>Users</title>
</head>
<body>
<div class="flex-center position-ref full-height">
@foreach($users as $user)
<div>
<h2>@if($user->gender == 'm'){{ "Mr." }} @else {{ "Mrs." }} @endif {{$user->name}}</h2>
<h2>{{ $user->email }}</h2>
</div>
@endforeach
</div>
</body>
</html>
在中大型项目中,会有几个问题:
由于 Blade 与 Html 夹杂,不太适合写太复杂的代码,只适合做一些简单的 binding ,否则很容易写成传统的 PHP 的意大利面代码
无法对显示逻辑做重构与物件导向
比较好的方式是使用 Presenter,具体步骤如下:
将相依无间注入到 Presenter
在 presenter 内写格式转换
将 Presenter 注入到 View
定义UserPresenter
app\Presenters\UserPersenter.php
代码如下:
<?php
namespace App\Presenters;
/**
* Class UserPresenter
*
* @package App\Presenters
*/
class UserPresenter
{
/**
* @param string $gender
* @param string $name
*
* @return string
*/
public function getFullName($gender, $name)
{
return $gender == 'M' ? 'Mr. ' . $name : 'Mrs. ' . $name;
}
}
将原本在 blade 中用 @if(){ .. }@else .. @endif
写的逻辑改写在 Presenter 中。
视图中使用UserPresenter
使用 @inject()
注入 UserPresenter
,让 View 可以如 Controller 一样使用注入的物件。
将来如乱显示逻辑怎么修改,都不用改到 Blade ,直接在相关 Presenter 中修改即可。
<!DOCTYPE html>
<html lang="{{ config('app.locale') }}">
<head>
<title>Users</title>
</head>
<body>
<div class="flex-center position-ref full-height">
@inject('userPresenter','App\Presenters\UserPresenter')
@foreach($users as $user)
<div>
{{--<h2>@if($user->gender == 'm'){{ "Mr." }} @else {{ "Mrs." }} @endif {{$user->name}}</h2>--}}
<h2>{{ $userPresenter->getFullName($user->gender,$user->name) }}</h2>
<h2>{{ $user->email }}</h2>
</div>
@endforeach
</div>
</body>
</html>
改用这种重写,有几个优点:
将资料显示不同个格式的显示逻辑改写在 presenter,解决了 blade 不容易维护的问题
可以显示逻辑做重构于物件导向
是否显示某些资料
如 根据字段值是否为 T ,要不要显示该字段
,我们常常会直接用 blade 写在 View 中。
<!DOCTYPE html>
<html lang="{{ config('app.locale') }}">
<head>
<title>Users</title>
</head>
<body>
<div class="flex-center position-ref full-height">
@foreach($users as $user)
<h2>{{ $user->name }}</h2>
@if($user->is_hidden == 'F')
<h2>{{ $user->email }}</h2>
@endif
@endforeach
</div>
</body>
</html>
在中大型项目中,会有几个问题:
由于 blade 与 HTML 夹杂,不太适合写太复杂的业务代码,只适合做一些简单的 binding,否则很容易写成传统的 PHP 的意大利面代码
无法对显示逻辑做重构与物件导向
比较好的方式是使用 Presenter,具体步骤如下:
将相依无间注入到 Presenter
在 presenter 内写格式转换
将 Presenter 注入到 View
app\Presenters\UserPresenter.php
代码:
<?php
namespace App\Presenters;
use App\User;
/**
* Class UserPresenter
*
* @package App\Presenters
*/
class UserPresenter
{
/**
* @param \App\User $user
*
* @return string
*/
public function showEmail(User $user)
{
if ($user->is_hidden == 'F') {
return '<h2>' . $user->email '</h2>';
}
return '';
}
}
将 @if() .. @endif
的 boolean 判断封装在 Presenter 内,改由 Presenter 负责输出 HTML。
<!DOCTYPE html>
<html lang="{{ config('app.locale') }}">
<head>
<title>Users</title>
</head>
<body>
<div class="flex-center position-ref full-height">
@inject('userPresenter','App\Presenters\UserPresenter')
@foreach($users as $user)
<h2>{{ $user->name }}</h2>
{!! $userPresenter->showEmail($user) !!}
@endforeach
</div>
</body>
</html>
使用 @inject()
注入 UserPresenter
,让 View 也可以如 Controller 一样使用注入的物件。
{!! !!}
会保留原来的 HTML 格式。
将来无论显示逻辑怎么修改,都不用改到 Blade ,直接在 Presenter 内修改。
改用这种写法,有几个优点:
是否显示某些资料
的显示逻辑改为在 Presenter,解决写在 Blade 不容易维护的问题可对显示逻辑做重构与物件导向
依需求显示不同格式
如 按照不同的语系,显示不同的日期格式
,我们常常会直接用 Blade 写在 View 里。 如下:
<!DOCTYPE html>
<html lang="{{ config('app.locale') }}">
<head>
<title>Users</title>
</head>
<body>
<div class="flex-center position-ref full-height">
@foreach($users as $user)
<div>
@if(App::getLocale() == 'uk')
<h2>{{ $user->created_at->format('d M, Y') }}</h2>
@elseif(App::getLocale() == 'tw')
<h2>{{ $user->creaetd_at->format('Y/m/d') }}</h2>
@else
<h2>{{ $user->created_at->formate('M d, Y') }}</h2>
@endif
</div>
@endforeach
</div>
</body>
</html>
在中大型的醒目中,会有几个问题:
由于 Blade 与 HTML 夹杂,不太适合写太复杂的代码,只适合做一些简单的 binding,否则很容易写成传统的 PHP 的意大利面代码
无法对显示逻辑做重构与物件导向
比较好的方式是使用 Presenter,具体步骤如下:
将相依无间注入到 Presenter
在 presenter 内写不同的日期格式转换逻辑
将 Presenter 注入到 View
定义接口
定义接口代码 app\Presenters\DataFormatPresenterInterface.php
,具体代码如下:
<?php
namespace App\Presenters;
use Carbon\Carbon;
/**
* Interface DateFormatPresenterInterface
*
* @package App\Presenters
*/
interface DateFormatPresenterInterface
{
/**
* 显示日期格式
*
* @param \Carbon\Carbon $data
*
* @return string
*/
public function showDateFormat(Carbon $data);
}
定义了 showDateFormat()
,各语言必须在 showDateFormat()
使用 Carbon 的 format()
去转换日期格式。
一些Presenter
app\Presenters\DateFormatPresenterTW.php
,具体代码内容如下:
<?php
namespace App\Presenters;
use Carbon\Carbon;
/**
* Class DateFormatPresenterTw
*
* @package \App\Presenters
*/
class DateFormatPresenterTw implements DateFormatPresenterInterface
{
/**
* @param \Carbon\Carbon $date
*
* @return string
*/
public function showDateFormat(Carbon $date)
{
return $date->format('Y/m/d');
}
}
app\Presenters\DateFormatPresenterUk.php
,具体代码内容如下:
<?php
namespace App\Presenters;
use Carbon\Carbon;
/**
* Class DateFormatPresenterUk
*
* @package \App\Presenters
*/
class DateFormatPresenterUk implements DateFormatPresenterInterface
{
/**
* @param \Carbon\Carbon $data
*
* @return string
*/
public function showDateFormat(Carbon $data)
{
return $data->format('d M, Y');
}
}
app\Presenters\DateFormatPresenterUs.php
,具体代码内容如下:
<?php
namespace App\Presenters;
use Carbon\Carbon;
/**
* Class DateFormatPresenterUs
*
* @package \App\Presenters
*/
class DateFormatPresenterUs implements DateFormatPresenterInterface
{
/**
* @param \Carbon\Carbon $date
*
* @return string
*/
public function showDateFormat(Carbon $date)
{
return $date->format('M d,Y');
}
}
以上类都实现了 DateFormatPresenterInterface
接口,并将转换成相对应国家日期格式的 Carbon 的 format()
写在 showDateFormat()
内。
Presenter 工厂
由于每个语言的日期格式都是一个 presenter 物件,那势必遇到一个最基本的问题: 我们必须根据不同的语言去实例化不同的 Presenter 物件
,我们可能会在 Controller 中去 实例化。如下:
/**
* @param \Illuminate\Http\Request $request
*
* @return int
*/
public function index(Request $request)
{
$locate = 'hk';
switch ($locate){
case 'uk':
$presenter = new DateFormatPresenterUk();
break;
case 'tw':
$presenter = new DateFormatPresenterTw();
break;
default:
$presenter = new DateFormatPresenterUs();
}
return $presenter;
}
这种写法虽然可行,但是有如下问题:
违反了 SOLID 的开放封闭原则:若将来有新的语言需求,只能不断去修改
index()
,然后不断的新增elseif()
,计算改用switch{ .. }
也是一样违反了 SOLID 的依赖反转原则:Controller 直接根据语言去实例化对应的 Class ,高层直接相依于底层,直接将实例化对象写死在代码里
无法单元测试:由于 Presenter 直接 New 在 Controller ,因此要测试时,无法对 Presenter 做 mock
定义工厂
比较好的解决方式是使用 Factory Pattern
app/Presenters/DateFormatPresenterFactory.php
内容如下:
<?php
namespace App\Presenters;
/**
* Class DateFormatPresenterFactory
*
* @package \App\Presenters
*/
class DateFormatPresenterFactory
{
/**
* @param $locale
*
* @return \Illuminate\Foundation\Application|mixed
*/
public static function bind($locale)
{
return app()->singleton(DateFormatPresenterInterface::class, 'App\Presenters\DateFormatPresenter' . ucwords($locale));
}
}
使用 Presenter Factory 的 create()
去取代 new 建立物件。
这里当然可以在 create()
里去写 if () { ... } else { ... }
去建立 Presenter 物件,不过这样会违反 SOLID 的开放封闭原则,比较好的方式是改用 App::bind()
,直接根据 $locale
去 binding 相对应的 Class,这样无论再怎么新增语言与日期格式, Controller 与 Presenter Factory 都不用做任何修改,完全符合开放封闭原则。
控制器调用
app\Http\Controllers\UserController.php
中的内容,如下:
public function index(Request $request, DateFormatPresenterFactory $dataFormatPresenterFactory)
{
$locate = 'uk';
$presenter = $dataFormatPresenterFactory::bind($locate);
dd($presenter->showDateFormat(Carbon::now()));
return $presenter;
}
使用 $dataFormatPresenterFactory::bind()
切换 app()
的 Presenter 物件,如此 Controller 将开放封闭,将来有新的语言新增或者修改需求,也不用修改 Controller
Blade 调用
<!DOCTYPE html>
<html lang="{{ config('app.locale') }}">
<head>
<title>Users</title>
</head>
<body>
<div class="flex-center position-ref full-height">
@inject('dateFormatPresenter','App\Presenters\DateFormatPresenterInterface')
@foreach($users as $user)
<div>
<h2><?php print_r($dateFormatPresenter->showDateFormat($user->created_at)); ?></h2>
</div>
@endforeach
</div>
</body>
</html>
使用 @inject()
注入 Presenter ,让 View 也可以如 Controller 一样使用注入的物件
使用 Presenter 的 showDateFormate()
将日期转换成预计的格式
使用这种写法有几个优点
将
依需求显示不同的格式
的显示逻辑写在 Presenter ,解决写在 Blade 不容易维护的问题可对显示逻辑做重构与物件导向
符合 SOLID 的开放闭合原则:将来若有新的语言,对于拓展是开放的,只要新增 Class 实现
DateFormatPresenterInterface
接口即可;对于修改是封闭的, Controller、FactoryInterface、Factory 与 View 都不用做任何修改不单只有 PHP 可以使用 Service Container,连 Blade 也可以使用 Service Container,甚至搭配 Service Provider
可单独对 Presenter 的显示逻辑做单元测试
若使用了 Presenter 辅助 Blade ,在搭配
@inject()
注入到 View,View就会非常干净,可专心处理将资料binding到HTML
的职责将来只有 Layout 改变才会动到 Balde ,若是显示逻辑改变都是修改 Presenter
最后
Presenter 使得显示逻辑从Blade 中解放,不仅更容易维护、更容易扩展、更容易重复使用且更容易测试