欢迎光临
我们一直在努力

如何使用 Presenter 模式?

若将显示逻辑都写在 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

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

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 内修改。 改用这种写法,有几个优点 :

  • 是否显示某些数据的显示逻辑改写在 presenter,解决写在 blade 不容易维护的问题。
  • 可对显示逻辑做重构面向对象

依需求显示不同格式如依照不同的语系,显示不同的日期格式,初学者常会直接用 blade 写在 view。11blade、mutator 与 presenter 的比较,详细请参考如何依各种语言显示不同日期格式?blade很难维护在中大型项目,会有几个问题 :

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

比较好的方式是使用 presenter :

  • 将相依物件注入到 presenter。
  • 在 presenter 内写不同的日期格式转换逻辑。
  • 将 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() 使用 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'));
}

 

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

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

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

Controller

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'));
    }
}

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


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

赞(0)
版权归原作者所有,如有侵权请告知。达维营-前端网 » 如何使用 Presenter 模式?

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址