範例:假期規劃 (建造者模式)

Pattern: 建造者模式

Class Diagram: 假期規劃


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?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'
];
}
}

老闆希望我們能提供更簡便的方式,來規劃不同的旅遊行程。
讓我們用建造者模式改造它。


需求一:實作旅遊行程 (產品類別)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<?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() 方法來輸出。


需求二:實作行程建造者 (建造者類別)

  • 定義行程規劃介面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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;
}

  • 實作行程建造者
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<?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) ,來增加程式碼可讀性。
我們待會會在指揮者類別中展示。

(註:此處也可以實作多個不同的行程建造者,來固定某些行程選項)


需求三:實作旅行社(指揮者類別)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?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();
}
}

透過旅行社 (指揮者類別),我們封裝了行程的實作。
使得客戶端不用知道行程的建造過程。

  • 最後修改原本的程式碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?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();
}
}


[單一職責原則]
我們將指揮者類別建造者類別產品類別,視為三種不同的職責。
由旅行社指揮行程建造者來構建行程。

[開放封閉原則]
當新增/修改行程時,我們只要調整指揮者類別。
當新增/修改行程內部的邏輯時,我們僅需修改產品類別。

[依賴反轉原則]
指揮者類別依賴於抽象的建造者介面。
建造者類別實作抽象的建造者介面。

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