範例:各國婚禮(訪問者模式)

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
<?php

namespace App\VisitorPattern\Wedding;

class Program
{
/**
* @param string $weddingType
* @return string
*/
public function getWedding($weddingType)
{
switch ($weddingType) {
case 'Chinese':
echo
'新郎:中式囍袍
新郎:黑色秀禾鞋
新娘:龍鳳褂
新娘:紅色秀禾鞋
';
break;

case 'Japanese':
echo
'新郎:繡有家紋的和服
新郎:雪駄
新娘:純潔的白無垢
新娘:草履
';
break;
}
}
}

熟稔設計模式的我們,一眼就看出來改寫的方向。

讓我們抽出新郎與新娘!


需求一:抽出新郎 (BrideGroom) 與新娘 (Bride) 類別

  • 新郎類別
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
<?php

namespace App\VisitorPattern\Wedding;

class BrideGroom
{
/**
* @param string $weddingType
*/
public function getClothes($weddingType)
{
switch ($weddingType) {
case 'Chinese':
echo
"新郎:中式囍袍\n";
break;

case 'Japanese':
echo
"新郎:繡有家紋的和服\n";
break;
}
}

/**
* @param string $weddingType
*/
public function getShoes($weddingType)
{
switch ($weddingType) {
case 'Chinese':
echo
"新郎:黑色秀禾鞋\n";
break;

case 'Japanese':
echo
"新郎:雪駄\n";
break;
}
}
}
  • 新娘類別
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
<?php

namespace App\VisitorPattern\Wedding;

class Bride
{
/**
* @param string $weddingType
*/
public function getClothes($weddingType)
{
switch ($weddingType) {
case 'Chinese':
echo
"新娘:龍鳳褂\n";
break;

case 'Japanese':
echo
"新娘:純潔的白無垢\n";
break;
}
}

/**
* @param string $weddingType
*/
public function getShoes($weddingType)
{
switch ($weddingType) {
case 'Chinese':
echo
"新娘:紅色秀禾鞋\n";
break;

case 'Japanese':
echo
"新娘:草履\n";
break;
}
}
}
  • 最後改寫既有程式碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

namespace App\VisitorPattern\Wedding;

class Program
{
/**
* @param string $weddingType
*/
public function getWedding($weddingType)
{
$brideGroom = new BrideGroom();
$bride = new Bride();

$brideGroom->getClothes($weddingType);
$brideGroom->getShoes($weddingType);

$bride->getClothes($weddingType);
$bride->getShoes($weddingType);
}
}

正當我們得意洋洋之時,老闆說了一個令人震驚的需求。

Boss:「隨著版圖擴張,我們之後要支援印度、烏克蘭等各國的婚禮服裝。」

經過觀察我們可以發現,不過是哪一國的婚禮,
主角皆是新郎與新娘,且都需要取得服裝與鞋子。

資料結構穩定的
變動的是服裝與鞋子的操作

讓我們用訪問者模式改寫它!


需求二:配合版圖的擴張,實作訪問者模式

  • 定義婚禮角色介面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

namespace App\VisitorPattern\Wedding\Contracts;

use App\VisitorPattern\Wedding\Contracts\WeddingType;

interface WeddingRole
{
/**
* @param WeddingType $weddingType
*/
public function getClothes($weddingType);

/**
* @param WeddingType $weddingType
*/
public function getShoes($weddingType);
}

  • 定義婚禮類型介面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

namespace App\VisitorPattern\Wedding\Contracts;

interface WeddingType
{
/**
* @param WeddingRole $role
*/
public function getClothes($role);

/**
* @param WeddingRole $role
*/
public function getShoes($role);
}

WeddingRole是原本的**元素類別 (Element)**。

WeddingType則是原本元素類別中的操作,會成為我們的**訪問者類別 (Visitor)
根據傳入的
元素類別 (Element)**,而有對應的行為。


  • 修改原本的新郎類別
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
<?php

namespace App\VisitorPattern\Wedding;

use App\VisitorPattern\Wedding\Contracts\WeddingRole;
use App\VisitorPattern\Wedding\Contracts\WeddingType;

class BrideGroom implements WeddingRole
{
/**
* @var string
*/
public $name = 'BrideGroom';

/**
* @param WeddingType $weddingType
*/
public function getClothes($weddingType)
{
$weddingType->getClothes($this);
}

/**
* @param WeddingType $weddingType
*/
public function getShoes($weddingType)
{
$weddingType->getShoes($this);
}
}
  • 修改原本的新娘類別
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
<?php

namespace App\VisitorPattern\Wedding;

use App\VisitorPattern\Wedding\Contracts\WeddingRole;
use App\VisitorPattern\Wedding\Contracts\WeddingType;

class Bride implements WeddingRole
{
/**
* @var string
*/
public $name = 'Bride';

/**
* @param WeddingType $weddingType
*/
public function getClothes($weddingType)
{
$weddingType->getClothes($this);
}

/**
* @param WeddingType $weddingType
*/
public function getShoes($weddingType)
{
$weddingType->getShoes($this);
}
}

BrideGroom與Bride會由客戶端將WeddingType傳入(第一次分派)
之後再將自己傳給WeddingType (第二次分派)。


  • 實作中式婚禮
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\VisitorPattern\Wedding\Type;

use App\VisitorPattern\Wedding\Contracts\WeddingType;
use App\VisitorPattern\Wedding\Contracts\WeddingRole;

class ChineseWedding implements WeddingType
{
/**
* @param WeddingRole $role
*/
public function getClothes($role)
{
$roleName = $role->name;

switch ($roleName) {
case 'BrideGroom':
echo
"新郎:中式囍袍\n";
break;

case 'Bride':
echo
"新娘:龍鳳褂\n";
break;
}
}

/**
* @param WeddingRole $role
*/
public function getShoes($role)
{
$roleName = $role->name;

switch ($roleName) {
case 'BrideGroom':
echo
"新郎:黑色秀禾鞋\n";
break;

case 'Bride':
echo
"新娘:紅色秀禾鞋\n";
break;
}
}
}
  • 實作日式婚禮
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\VisitorPattern\Wedding\Type;

use App\VisitorPattern\Wedding\Contracts\WeddingType;
use App\VisitorPattern\Wedding\Contracts\WeddingRole;

class JapaneseWedding implements WeddingType
{
/**
* @param WeddingRole $role
*/
public function getClothes($role)
{
$roleName = $role->name;

switch ($roleName) {
case 'BrideGroom':
echo
"新郎:繡有家紋的和服\n";
break;

case 'Bride':
echo
"新娘:純潔的白無垢\n";
break;
}
}

/**
* @param WeddingRole $role
*/
public function getShoes($role)
{
$roleName = $role->name;

switch ($roleName) {
case 'BrideGroom':
echo
"新郎:雪駄\n";
break;

case 'Bride':
echo
"新娘:草履\n";
break;
}
}
}

各國婚禮會根據傳入婚禮角色得不同,而有不同的行為。


  • 實作婚禮類型工廠,方便客戶端呼叫
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

namespace App\VisitorPattern\Wedding;

use App\VisitorPattern\Wedding\Contracts\WeddingType;
use ReflectionClass;

class WeddingTypeFactory
{
/**
* @param string $weddingType
* @return WeddingType
*/
public function create($weddingType)
{
$namespace = 'App\VisitorPattern\Wedding\Type';
$className = $weddingType . 'Wedding';

$reflector = new ReflectionClass($namespace . '\\' . $className);
return $reflector->newInstance();
}
}
  • 實作物件結構類別,用來放入元素,便於我們實現遍歷。方便客戶端的呼叫。
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
<?php

namespace App\VisitorPattern\Wedding;

use App\VisitorPattern\Wedding\Contracts\WeddingRole;
use App\VisitorPattern\Wedding\Contracts\WeddingType;

class Composite
{
/**
* @var WeddingRole[]
*/
protected $children = [];

/**
* @param WeddingRole $role
* @return void
*/
public function add(WeddingRole $role)
{
$this->children[$role->name] = $role;
}

/**
* @param WeddingRole $component
* @return void
*/
public function remove(WeddingRole $role)
{
unset($this->children[$role->name]);
}

/**
* @param WeddingType $weddingType
* @return void
*/
public function display(WeddingType $weddingType)
{
foreach ($this->children as $child) {
$child->getClothes($weddingType);
$child->getShoes($weddingType);
}
}
}

  • 最後修改既有程式碼
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
<?php

namespace App\VisitorPattern\Wedding;

use App\VisitorPattern\Wedding\Contracts\WeddingType;
use App\VisitorPattern\Wedding\WeddingTypeFactory;
use App\VisitorPattern\Wedding\Composite;
use App\VisitorPattern\Wedding\BrideGroom;
use App\VisitorPattern\Wedding\Bride;

class Program
{
/**
* @var WeddingTypeFactory
*/
protected $weddingTypeFactory;

public function __construct()
{
$this->weddingTypeFactory = new WeddingTypeFactory();
}

/**
* @param string $weddingType
*/
public function getWedding($weddingType)
{
$weddingType = $this->createWeddingType($weddingType);

$composite = new Composite();

$brideGroom = new BrideGroom();
$bride = new Bride();

$composite->add($brideGroom);
$composite->add($bride);

$composite->display($weddingType);
}

/**
* @param string $weddingType
* @return WeddingType
*/
private function createWeddingType($weddingType)
{
return $this->weddingTypeFactory->create($weddingType);
}
}


[單一職責原則]
我們將婚禮角色(資料結構)婚禮類型(操作)視作兩種不同的職責。

[開放封閉原則]
新增/修改婚禮類型時,不會修改到所有的程式碼。

[介面隔離原則]
婚禮角色介面:會根據客戶端傳入的婚禮類型,再將自己傳入後,完成行為。
婚禮類型介面:會根據傳入的婚禮角色,完成行為。

[依賴反轉原則]
依賴於抽象的婚禮角色介面與婚禮類型介面。

ʕ •ᴥ•ʔ:揉合許多模式的範例。