iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 25
1

本文同步更新於blog

情境:平台有三種身份,分別是訪客 (guest)會員 (member)付費會員(premium)

  1. 訪客藉由註冊 (register),可成為會員。
  2. 會員藉由訂閱 (subsribe),可成為付費會員。
  3. 付費會員藉由取消訂閱 (cancelSubscription),可變回會員。
  4. 會員及付費會員藉由刪除帳號 (deleteAccount),可變回訪客。
<?php

namespace App\StatePattern\Youtube;

use Exception;

class Program
{
    /**
     * @var string
     */
    protected $license;

    public function __construct()
    {
        $this->setLicense('guest');
    }

    public function register()
    {
        if ($this->license == 'premium') {
            return;
        }

        $this->license = 'member';
    }

    public function getLicense()
    {
        return $this->license;
    }

    /**
     * @param string $license
     */
    public function setLicense($license)
    {
        $this->license = $license;
    }

    public function subscribe()
    {
        if ($this->license == 'premium') {
            return;
        }

        if ($this->license == 'member') {
            $this->license = 'premium';
            return;
        }

        throw new Exception('You need to be a member before subscribing.');
    }

    public function cancelSubscription()
    {
        if ($this->license == 'premium') {
            $this->license = 'member';
            return;
        }

        throw new Exception('Sorry, you have not subscribed.');
    }

    public function deleteAccount()
    {
        if ($this->license == 'member' || $this->license == 'premium') {
            $this->license = 'guest';
            return;
        }

        throw new Exception('You need to be a member before deleting account.');
    }
}

隨著功能越來越多,每次新增功能時,
我們都會有許多複雜的條件式(判斷當前用戶狀態)。

讓我們用狀態模式改善它。


需求一:切分出不同的用戶狀態

  • 首先定義用戶狀態(抽象)
<?php

namespace App\StatePattern\Youtube\State;

abstract class UserState
{
    const LICENCE = 'undefined user';

    public function getLicense()
    {
        return $this::LICENCE;
    }

    abstract function register();

    abstract function subscribe();

    abstract function cancelSubscription();

    abstract function deleteAccount();
}
  • 訪客狀態
<?php

namespace App\StatePattern\Youtube\State;

use App\StatePattern\Youtube\Program;
use Exception;
use App\StatePattern\Youtube\State\UserState;

class GuestState extends UserState
{
    /**
     * @var Program
     */
    protected $program;

    /**
     * @var string
     */
    const LICENCE = 'guest';

    /**
     * @param Program $program
     */
    public function __construct(Program $program)
    {
        $this->program = $program;
    }

    public function register()
    {
        $this->program->setMemberState();
    }

    public function subscribe()
    {
        throw new Exception('You need to be a member before subscribing.');
    }

    public function cancelSubscription()
    {
        throw new Exception('Sorry, you have not subscribed.');
    }

    public function deleteAccount()
    {
        throw new Exception('You need to be a member before deleting account.');
    }
}
  • 會員狀態
<?php

namespace App\StatePattern\Youtube\State;

use App\StatePattern\Youtube\Program;
use Exception;
use App\StatePattern\Youtube\State\UserState;

class MemberState extends UserState
{
    /**
     * @var Program
     */
    protected $program;

    /**
     * @var string
     */
    const LICENCE = 'member';

    /**
     * @param Program $program
     */
    public function __construct(Program $program)
    {
        $this->program = $program;
    }

    public function register()
    {
        return;
    }

    public function subscribe()
    {
        $this->program->setPremiumState();
    }

    public function cancelSubscription()
    {
        throw new Exception('Sorry, you have not subscribed.');
    }

    public function deleteAccount()
    {
        $this->program->setGuestState();
    }
}
  • 付費會員狀態
<?php

namespace App\StatePattern\Youtube\State;

use App\StatePattern\Youtube\Program;
use App\StatePattern\Youtube\State\UserState;

class PremiumState extends UserState
{
    /**
     * @var Program
     */
    protected $program;

    /**
     * @var string
     */
    const LICENCE = 'premium';

    /**
     * @param Program $program
     */
    public function __construct(Program $program)
    {
        $this->program = $program;
    }

    public function register()
    {
        return;
    }

    public function subscribe()
    {
        return;
    }

    public function cancelSubscription()
    {
        $this->program->setMemberState();
    }

    public function deleteAccount()
    {
        $this->program->setGuestState();
    }
}
  • 最後修改原本的情境類別
<?php

namespace App\StatePattern\Youtube;

use App\StatePattern\Youtube\State\MemberState;
use App\StatePattern\Youtube\State\GuestState;
use App\StatePattern\Youtube\State\PremiumState;
use App\StatePattern\Youtube\State\UserState;

class Program
{
    /**
     * @var string
     */
    protected $license;

    /**
     * @var MemberState
     */
    protected $memberState;

    /**
     * @var GuestState
     */
    protected $guestState;

    /**
     * @var PremiumState
     */
    protected $premiumState;

    /**
     * @var UserState
     */
    protected $state;

    public function __construct()
    {
        $this->memberState = new MemberState($this);
        $this->guestState = new GuestState($this);
        $this->premiumState = new PremiumState($this);

        $this->setGuestState();
    }

    public function register()
    {
        $this->state->register();
    }

    public function getLicense()
    {
        return $this->state->getLicense();
    }

    public function subscribe()
    {
        $this->state->subscribe();
    }

    public function cancelSubscription()
    {
        $this->state->cancelSubscription();
    }

    public function setGuestState()
    {
        $this->state = $this->guestState;
    }

    public function setMemberState()
    {
        $this->state = $this->memberState;
    }

    public function setPremiumState()
    {
        $this->state = $this->premiumState;
    }

    public function getState()
    {
        return $this->state;
    }

    public function deleteAccount()
    {
        $this->state->deleteAccount();
    }
}

[單一職責原則]
我們將情境類別狀態類別視作兩種不同的職責。
透過委派來實現不同狀態下的行為。

[開放封閉原則]
修改既有狀態類別的行為,不會影響到全部的既有狀態類別。
(新增狀態類別時,可能會影響到)

[介面隔離原則]
情境類別依賴於抽象的用戶狀態 (User State)
不同的狀態類別實作抽象的用戶狀態 (User State)

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

ʕ •ᴥ•ʔ:透過測試,重構這個範例時有遇到一些小困難。
大家也可以挑戰看看。


上一篇
Day24. 狀態模式
下一篇
Day26. 觀察者模式
系列文
你終究都要學設計模式的,那為什麼不一開始就學呢?57
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言