Laravel框架中使用 Presenter 模式

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

Version

Laravel 5.1.22

显示逻辑

显示逻辑中,常见的如:

  • 将数据显示不同数据:如性别字段为M,就显示Mr.,若性别字段为F,就显示Mrs.。

  • 是否显示某些数据:如根据字段值是否为Y,要不要显示该字段。

  • 依需求显示不同格式:如依照不同的语系,显示不同的日期格式。

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.phpnamespace 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. 无法对显示逻辑做重构与面向对象。

  3. 违反SOLID的开放封闭原则:若将来要支持新的语系,只能不断地在blade新增if…else。(开放封闭原则:软件中的类别、函式对于扩展是开放的,对于修改是封闭的。)

比较好的方式是使用presenter:

  1. 将相依物件注入到presenter。

  2. 在presenter内写不同的日期格式转换逻辑。

  3. 将presenter注入到view。

DateFormatPresenterInterface.php

app/Presenters/DateFormatPresenterInterface.phpnamespace 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.phpnamespace 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.phpnamespace 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.phpnamespace 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'));}

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

  1. 违反SOLID的开放封闭原则:若将来有新的语言需求,只能不断去修改index(),然后不断的新增elseif,就算改用switch也是一样。

  2. 违反SOLID的依赖反转原则:controller直接根据语言去new相对应的class,高层直接相依于低层,直接将实作写死在程序中。(依赖反转原则:高层不应该依赖于低层,两者都应该要依赖抽象;抽象不要依赖细节,细节要依赖抽象)

  3. 无法单元测试:由于presenter直接new在controller,因此要测试时,无法对presenter做mock。
    比较好的方式是使用Factory Pattern
    DataFormatPresenterFactory.php


    使用Presenter Factory的create()去取代new建立物件。
    这里当然可以在create()去写if…elseif去建立presenter物件,不过这样会违反SOLID的开放封闭原则,比较好的方式是改用App::bind(),直接根据$locale去binding相对应的class,这样无论在怎么新增语言与日期格式,controller与Presenter Factory都不用做任何修改,完全符合开放封闭原则。
    Controller
    UserController.php

    1. app/Presenters/DateFormatPresenterFactory.php

    2. namespace App\Presenters;

    3. use Illuminate\Support\Facades\App;

    4. class DateFormatPresenterFactory

    5. {

    6. /**

    7.  * @param string $locale

    8.  */

    9. public static function bind(string $locale)

    10. {

    11. App::bind(DateFormatPresenterInterface::class,

    12. 'MyBlog\Presenters\DateFormatPresenter_' . $locale);

    13. }

    14. }

app/Http/Controllers/UserController.phpnamespace 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有了以下的优点:

  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