Day40. 范例:假期规划 (建造者模式)

本文同步更新於blog

情境:目前提供旅游行程的方式

<?php

namespace App\BuilderPattern\Vacation;

class Program
{
    /**
     * @return array
     */
    public function getDomesticTravel()
    {
        //高速铁路一日体验

        return [
            'from' => 'Kaohsiung',
            'to' => 'Taipei',
            'day' => 1,
            'transport' => 'High Speed Rail'
        ];
    }

    /**
     * @return array
     */
    public function getInternationalTravel()
    {
        //东京五日游

        return [
            'from' => 'Kaohsiung',
            'to' => 'Tokyo',
            'day' => 5,
            'transport' => 'Airplane',
            'hotel' => 'Disney Hotel'
        ];
    }
}

老板希望我们能提供更简便的方式,来规划不同的旅游行程。
让我们用建造者模式改造它。


需求一:实作旅游行程 (产品类别)

<?php

namespace App\BuilderPattern\Vacation;

class Itinerary
{
    /**
     * @var string
     */
    protected $from;

    /**
     * @var string
     */
    protected $to;

    /**
     * @var int
     */
    protected $day;

    /**
     * @var string
     */
    protected $hotel;

    /**
     * @var string
     */
    protected $transport;

    /**
     * @param string $name
     * @param string|int $value
     */
    public function __set($name, $value)
    {
        $this->$name = $value;
    }

    /**
     * @param string $name
     * @return string|int
     */
    public function __get($name)
    {
        return $this->$name;
    }

    /**
     * @return array
     */
    public function toArray()
    {
        $result = get_object_vars($this);

        foreach ($result as $name => $value) {
            if (is_null($value)) {
                unset($result[$name]);
            }
        }

        return $result;
    }
}

主要都是gettersetter方法。
当行程规划好时,我们会透过 toArray() 方法来输出。


需求二:实作行程建造者 (建造者类别)

  • 定义行程规划介面
<?php

namespace App\BuilderPattern\Vacation\Contracts;

use App\BuilderPattern\Vacation\Itinerary;

interface ItineraryPlanable
{
    public function from(string $from): self;

    public function to(string $to): self;

    public function spendDays(int $day): self;

    public function stayAt(string $hotel): self;

    public function travelBy(string $transport): self;

    public function getItinerary(): Itinerary;
}

  • 实作行程建造者
<?php

namespace App\BuilderPattern\Vacation;

use App\BuilderPattern\Vacation\Itinerary;
use App\BuilderPattern\Vacation\Contracts\ItineraryPlanable;

class ItineraryBuilder implements ItineraryPlanable
{
    /**
     * @var Itinerary
     */
    protected $itinerary;

    public function __construct()
    {
        $this->itinerary = new Itinerary();
    }

    /**
     * @param string $from
     * @return self
     */
    public function from(string $from): self
    {
        $this->itinerary->from = $from;
        return $this;
    }

    /**
     * @param string $to
     * @return self
     */
    public function to(string $to): self
    {
        $this->itinerary->to = $to;
        return $this;
    }

    /**
     * @param integer $day
     * @return self
     */
    public function spendDays(int $day): self
    {
        $this->itinerary->day = $day;
        return $this;
    }

    /**
     * @param string $hotel
     * @return self
     */
    public function stayAt(string $hotel): self
    {
        $this->itinerary->hotel = $hotel;
        return $this;
    }

    /**
     * @param string $transport
     * @return self
     */
    public function travelBy(string $transport): self
    {
        $this->itinerary->transport = $transport;
        return $this;
    }

    /**
     * @return Itinerary
     */
    public function getItinerary(): Itinerary
    {
        return $this->itinerary;
    }
}

行程建造者用了流式接口 (Fluent Interface),来增加程序码可读性。
我们待会会在指挥者类别中展示。

(注:此处也可以实作多个不同的行程建造者,来固定某些行程选项)


需求三:实作旅行社(指挥者类别)

<?php

namespace App\BuilderPattern\Vacation;

use App\BuilderPattern\Vacation\Contracts\ItineraryPlanable;

class TravelAgency
{
    /**
     * @var ItineraryPlanable
     */
    protected $itineraryBuilder;

    public function __construct(ItineraryPlanable $itineraryBuilder)
    {
        $this->itineraryBuilder = $itineraryBuilder;
    }

    /**
     * @return array
     */
    public function getHighSpeedRailItinerary()
    {
        $itinerary = $this->itineraryBuilder
            ->from('Kaohsiung')
            ->to('Taipei')
            ->travelBy('High Speed Rail')
            ->spendDays(1)
            ->getItinerary();

        return $itinerary->toArray();
    }

    /**
     * @return array
     */
    public function getFiveDaysTokyoItinerary()
    {
        $itinerary = $this->itineraryBuilder
            ->from('Kaohsiung')
            ->to('Tokyo')
            ->travelBy('Airplane')
            ->spendDays(5)
            ->stayAt('Disney Hotel')
            ->getItinerary();

        return $itinerary->toArray();
    }
}

透过旅行社 (指挥者类别),我们封装了行程的实作。
使得客户端不用知道行程的建造过程。

  • 最後修改原本的程序码
<?php

namespace App\BuilderPattern\Vacation;

use App\BuilderPattern\Vacation\TravelAgency;
use App\BuilderPattern\Vacation\ItineraryBuilder;

class Program
{
    /**
     * @return array
     */
    public function getDomesticTravel()
    {
        //高速铁路一日体验
        $itineraryBuilder = new ItineraryBuilder();
        $travelAgency = new TravelAgency($itineraryBuilder);
        return $travelAgency->getHighSpeedRailItinerary();
    }

    /**
     * @return array
     */
    public function getInternationalTravel()
    {
        //东京五日游
        $itineraryBuilder = new ItineraryBuilder();
        $travelAgency = new TravelAgency($itineraryBuilder);
        return $travelAgency->getFiveDaysTokyoItinerary();
    }
}


[单一职责原则]
我们将指挥者类别建造者类别产品类别,视为三种不同的职责。
由旅行社指挥行程建造者来构建行程。

[开放封闭原则]
当新增/修改行程时,我们只要调整指挥者类别。
当新增/修改行程内部的逻辑时,我们仅需修改产品类别。

[依赖反转原则]
指挥者类别依赖於抽象的建造者介面。
建造者类别实作抽象的建造者介面。

最後附上类别图:
https://ithelp.ithome.com.tw/upload/images/20201112/20111630E7gDW1wqvw.png
(注:若不熟悉 UML 类别图,可参考UML类别图说明。)

ʕ •ᴥ•ʔ:核心精神在於分离建造过程与产品本身的逻辑


<<:  特权蠕变(Privilege Creep)& 自由访问控制(DAC)

>>:  JS 立即函式 DAY61

Vue.js 从零开始:MVVM、渐进式框架

本篇会围绕网路上常讲到主题,有些面试题应该也会多少考到一些,至少面试时可以讲的出来。 宣告式的渲染 ...

【22】正规化方法 L1 与 L2 Regularizers 的比较实验

Colab连结 正规化 (Regularizers) 是在 Loss Function 中,多加一项...

Day27-useMemo

前言 前两天我们学习了React性能优化 memo 组件记忆 useCallback 函式参考记忆 ...

Springboot AJAX

Springboot AJAX ...

Day 27 初学者补给站 学习方向讨论

大家好~~欢迎来到第二十七篇 学习方向讨论 上一篇跟大家说到程序,如何自我学习,找寻方法,今天来讲别...