iT邦幫忙

第 12 屆 iThome 鐵人賽

1
Software Development

你終究都要學設計模式的,那為什麼不一開始就學呢?系列 第 56

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

本文同步更新於blog

情境:這是一間國際婚禮公司

<?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) 類別

  • 新郎類別
<?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;
        }
    }
}
  • 新娘類別
<?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;
        }
    }
}
  • 最後改寫既有程式碼
<?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:「隨著版圖擴張,我們之後要支援印度、烏克蘭等各國的婚禮服裝。」

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

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

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


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

  • 定義婚禮角色介面
<?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);
}

  • 定義婚禮類型介面
<?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),而有對應的行為。


  • 修改原本的新郎類別
<?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);
    }
}
  • 修改原本的新娘類別
<?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 (第二次分派)。


  • 實作中式婚禮
<?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;
        }
    }
}
  • 實作日式婚禮
<?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;
        }
    }
}

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


  • 實作婚禮類型工廠,方便客戶端呼叫
<?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();
    }
}
  • 實作物件結構類別,用來放入元素,便於我們實現遍歷。方便客戶端的呼叫。
<?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);
        }
    }
}

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


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

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

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

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

最後附上類別圖:
https://ithelp.ithome.com.tw/upload/images/20201227/201116303BK9AIg3gk.png
(註:若不熟悉 UML 類別圖,可參考UML類別圖說明。)

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


上一篇
Day55. 訪問者模式
下一篇
Day 57. 系列完結心得
系列文
你終究都要學設計模式的,那為什麼不一開始就學呢?57

尚未有邦友留言

立即登入留言