Laravel框架中如何使用 Presenter 模式?

吴宇 2019-11-19 57℃ 0条

若将显示逻辑都写在view,会造成view肥大而难以维护,基于SOLID原则,我们应该使用Presenter模式辅助view,将相关的显示逻辑封装在不同的presenter,方便中大型项目的维护。

       若将显示逻辑都写在 view,会造成view肥大而难以维护,基于 SOLID 原则,我们应该使用 Presenter 模式辅助 view,将相关的显示逻辑封装在不同的presenter,方便中大型项目的维护。

       显示逻辑中,常见的如:

  • 将数据显示不同数据:如性别字段为M,就显示Mr.,若性别字段为F,就显示Mrs.。
  • 是否显示某些数据:如根据字段值是否为Y,要不要显示该字段。
  • 依需求显示不同格式:如依照不同的语系,显示不同的日期格式。

Presenter

将数据显示不同数据

       如性别字段为M,就显示Mr.,若性别字段为F,就显示Mrs.,初学者常会直接用 blade 写在 view

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Users</title>
</head>
<body>
    <div>
        @foreach($users as $user)
            <div>
                <h2>@if('M' == $user->gender) {{'Mr.'}} @else {{'Mrs.'}} @endif {{$user->name}}</h2>
                <h2>{{$user->email}}</h2>
            </div>
        @endforeach
    </div>
</body>
</html>

       在中大型项目,会有几个问题:

  1. 由于 blade 与HTML夹杂,不太适合写太复杂的程序,只适合做一些简单的binding。
  2. 无法对显示逻辑做重构与面向对象。

       比较好的方式是使用 presenter

  1. 将相依物件注入到 presenter
  2. presenter 内写格式转换。
  3. presenter 注入到 view

       例如:下面是 UserPresenter.php

app/Presenters/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

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Users</title>
</head>
<body>
    <div>
        @inject('userPresenter', 'MyBlog\Presenters\UserPresenter')
        @foreach($users as $user)
            <div>
                <h2>{{$userPresenter->getFullName($user->gender, $user->name)}}</h2>
                <h2>{{$user->email}}</h2>
            </div>
        @endforeach
    </div>
</body>
</html>

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

       将来无论显示逻辑怎么修改,都不用改到 blade ,直接在 presenter 内修改。

       改用这种写法,有几个优点:

  1. 将数据显示不同格式的显示逻辑改写在 presenter,解决写在 blade 不容易维护的问题。
  2. 可对显示逻辑做重构与面向对象。

是否显示某些数据

       如根据字段值是否为Y,要不要显示该字段,初学者常会直接用 blade 写在 view

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Users</title>
</head>
<body>
    <div>
        <?php $locale = App::getLocale(); ?>
        @foreach($users as $user)
            <div>
                <h2>{{$user->name}}</h2>
                <h2>{{$user->email}}</h2>
                @if('uk' == $locale)
                    <h2>{{$user->created_at->format('d M, Y')}}</h2>
                @elseif('tw' == $locale)
                    <h2>{{$user->created_at->format('Y/m/d')}}</h2>
                @else
                    <h2>{{$user->created_at->format('M d, Y')}}</h2>
                @endif
            </div>
        @endforeach
    </div>
</body>
</html>

       在中大型项目,会有几个问题:

  1. 由于blade与HTML夹杂,不太适合写太复杂的程序,只适合做一些简单的binding,否则很容易流于传统PHP的意大利面程序。
  2. 无法对显示逻辑做重构与面向对象。
  3. 违反SOLID的开放封闭原则:若将来要支持新的语系,只能不断地在blade新增 if…else 。(开放封闭原则:软件中的类别、函式对于扩展是开放的,对于修改是封闭的。)

       比较好的方式是使用 presenter

  1. 将相依物件注入到 presenter
  2. presenter 内写不同的日期格式转换逻辑。
  3. presenter 注入到 view

    例如,下面是 DateFormatPresenterInterface.php

app/Presenters/DateFormatPresenterInterface.php
namespace App\Presenters;
use Carbon\Carbon;
interface DateFormatPresenterInterface
{
    /**
     * 显示日期格式
     * @param Carbon $date
     * @return string
     */
    public function showDateFormat(Carbon $date) : string;
}

       定义了 showDateFormat() ,各语言必须在 showDateFormat() 使用 Carbonformat() 去转换日期格式。

       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 ,并将转换成英国日期格式的 Carbonformat() 写在 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,并将转换成台湾日期格式的Carbonformat()写在 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,并将转换成美国日期格式的Carbonformat()写在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'));
}

       这种写法虽然可行,但有几个问题:

  1. 违反SOLID的开放封闭原则:若将来有新的语言需求,只能不断去修改 index(),然后不断的新增 elseif ,就算改用 switch 也是一样。
  2. 违反 SOLID 的依赖反转原则:controller 直接根据语言去new相对应的 class,高层直接相依于低层,直接将实作写死在程序中。(依赖反转原则:高层不应该依赖于低层,两者都应该要依赖抽象;抽象不要依赖细节,细节要依赖抽象)
  3. 无法单元测试:由于 presenter 直接new在 controller,因此要测试时,无法对 presentermock

       比较好的方式是使用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都不用做任何修改,完全符合开放封闭原则。

       UserController.php

app/Http/Controllers/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'));
    }
}

       使用 $dateFormatPresenterFactory::bind() 切换 App::bind()presenter 物件,如此 controller 将开放封闭,将来有新的语言需求,也不用修改 controller

       我们可以发现改用factory pattern之后,controller 有了以下的优点:

  1. 符合 SOLID 的开放封闭原则:若将来有新的语言需求,controller 完全不用做任何修改。
  2. 符合 SOLID 的依赖反转原则:controller 不再直接相依于 presenter ,而是改由factory去建立 presenter
  3. 可以做单元测试:可直接对各 presenter 做单元测试,不需要跑验收测试就可以测试显示逻辑。

Blade

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Users</title>
</head>
<body>
<div>
    @inject('dateFormatPresenter', 'MyBlog\Presenters\DateFormatPresenterInterface')
    @foreach($users as $user)
        <div>
            <h2>{{$user->name}}</h2>
            <h2>{{$user->email}}</h2>
            <h2>{{$dateFormatPresenter->showDateFormat($user->created_at)}}</h2>
        </div>
    @endforeach
</div>
</body>
</html>

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

       使用 presentershowDateFormat() 将日期转成想要的格式。

       改用这种写法,有几个优点:

  1. 将依需求显示不同格式的显示逻辑改写在 presenter ,解决写在blade不容易维护的问题。
  2. 可对显示逻辑做重构与面向对象。
  3. 符合 SOLID 的开放封闭原则:将来若有新的语言,对于扩展是开放的,只要新增class实践 DateFormatPresenterInterface 即可;对于修改是封闭的,controllerfactory interfacefactoryview都不用做任何修改。
  4. 不单只有PHP可以使用 service container,连 blade 也可以使用 service container ,甚至搭配 service provider
  5. 可单独对 presenter 的显示逻辑做单元测试。

View

       若使用了 presenter 辅助 blade ,再搭配 @inject() 注入到 viewview就会非常干净,可专心处理将数据binding到HTML的职责。

       将来只有layout改变才会动到 blade ,若是显示逻辑改变都是修改 presenter

Conclusion

       Presenter使得显示逻辑从blade中解放,不仅更容易维护、更容易扩展、更容易重复使用,且更容易测试。

标签: phplaravel

非特殊说明,本博所有文章均为博主原创。