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类别图说明。)

ʕ •ᴥ•ʔ:揉合许多模式的范例。


<<:  iOS APP 开发 OC 第七天, nil 跟 NULL 一样吗?

>>:  风险暴露(risk exposure)

[Q&A] 02 专案前进的路线被什麽卡住了?

ISMS 启动需要事先取得高阶主管授权,在取得足够的授权之後开始推广一系列的资安相关活动。 如果发生...

[DAY 22] 试题反映理论

试题反映理论 在试题反映理论(Item Response Theory, IRT)中 能用作因素来解...

iOS APP 开发 OC 第二十天,自动释放池

tags: OC 30 day 自动释放池的原理 存入到自动释放池中的对象,在自动释放池被销毁的时候...

Day 16. 常见模板 Template App Apache / Nginx by Zabbix agent 介绍

Hi 大家今天要跟大家介绍 App 样板,主要是 Web 服务。 我们主要的服务都是基本上都是 LA...

[DAY30] DDD学习资源与完赛感言

DDD 学习资源 ddd-crew 里面有许多关於 DDD 各个面向的 repo,其中这个 repo...