iT邦幫忙

第 12 屆 iThome 鐵人賽

1

本文同步更新於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類別圖說明。)

ʕ •ᴥ•ʔ:核心精神在於分離建造過程與產品本身的邏輯


上一篇
Day39. 建造者模式
下一篇
Day41. 備忘錄模式
系列文
你終究都要學設計模式的,那為什麼不一開始就學呢?57
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言