🎉

【PHP】Stateパターンについて

に公開

はじめに

オブジェクトが自身の状態変化によって自律的に振る舞いを切り替えるのがStateパターンです[1]。ここでは、携帯の動作モードを例に挙げてStateパターンについて解説します。

実装例

携帯の動作モードに、「省電力モード」と「通常モード」があるとします。バッテリー残量が20%未満のときは「省電力モード」になります。その後、バッテリー残量が80%以上に充電されると「通常モード」に戻ります。この状態遷移をStateパターンを使って実装してみました。

<?php

class BatteryLevel
{
    readonly int $value;

    public function __construct(int $batteryLevel)
    {
        if ($batteryLevel < 0 || $batteryLevel > 100) {
            throw new InvalidArgumentException('バッテリー残量は0以上100以下でなければなりません。');
        }

        $this->value = $batteryLevel;
    }
}

abstract class MobileOperationModeState
{
    abstract public function nextState(BatteryLevel $batteryLevel): MobileOperationModeState;
    abstract public function getModeName(): string;
}

class MobileBattery
{
    private MobileOperationModeState $currentState;

    public function __construct(private BatteryLevel $batteryLevel)
    {
        $this->currentState = NormalMode::getInstance();
    }

    public function updateBatteryLevel(BatteryLevel $batteryLevel): void
    {
        $this->batteryLevel = $batteryLevel;
        $this->currentState = $this->currentState->nextState($batteryLevel);
    }

    public function getCurrentMode(): string
    {
        return $this->currentState->getModeName();
    }
}


class PowerSavingMode extends MobileOperationModeState
{
    use Singleton;

    public function nextState(BatteryLevel $batteryLevel): MobileOperationModeState
    {
        return $batteryLevel->value >= 80 ? NormalMode::getInstance() : $this;
    }

    public function getModeName(): string
    {
        return '省電力モード';
    }
}

class NormalMode extends MobileOperationModeState
{
    use Singleton;

    public function nextState(BatteryLevel $batteryLevel): MobileOperationModeState
    {
        return $batteryLevel->value < 20 ? PowerSavingMode::getInstance() : $this;
    }

    public function getModeName(): string
    {
        return '通常モード';
    }
}
<?php

trait Singleton
{
    private static ?self $instance = null;

    private function __construct() {}

    public static function getInstance(): self
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
}
// 使用例
$batteryLevel = new BatteryLevel(100);
$mobileBattery = new MobileBattery($batteryLevel);
print $mobileBattery->getCurrentMode(); // 通常モード

$batteryLevel = new BatteryLevel(19);
$mobileBattery->updateBatteryLevel($batteryLevel);
print $mobileBattery->getCurrentMode(); // 省電力モード

$batteryLevel = new BatteryLevel(80);
$mobileBattery->updateBatteryLevel($batteryLevel);
print $mobileBattery->getCurrentMode(); // 通常モード


図1: クラス間の依存関係
MobileBatteryクラスのcurrentStateプロパティが、PowerSavingModeとNormalModeのどちらのインスタンスを保持しているかによって、getCurrentModeメソッドの戻り値が変化します。
また、updateBatteryLevelメソッドでバッテリー残量を更新するときに、そのバッテリー残量に応じてPowerSavingModeとNormalModeのインスタンスを自律的に切り替えています。

 public function updateBatteryLevel(BatteryLevel $batteryLevel): void
 {
     $this->batteryLevel = $batteryLevel;
     // バッテリー残量に応じて、携帯の動作モードのインスタンスを自律的に切り替えている
     $this->currentState = $this->currentState->nextState($batteryLevel);
 }

この部分がStateパターンの特徴で、内部でいつの間にか振る舞いを表すオブジェクトであるPowerSavingModeとNormalModeが切り替わっています。

利点

このパターンを使った場合の利点を下記に挙げます。

  • 可読性が向上する
  • 変更容易性が向上する
  • テスト容易性が向上する

それぞれの利点について解説します。

可読性が向上する

どのような場合に「省電力モード」や「通常モード」になるのかや、「省電力モード」と「通常モード」が何をすべきなのかが、それぞれの状態クラスにカプセル化されています。これにより、MobileBatteryクラスに複雑な条件分岐を定義する必要がなくなり、可読性が向上しています。

変更容易性が向上する

「省電力モード」と「通常モード」を切り替えるバッテリ残量のしきい値が変更されたり、新しい携帯の動作モードを追加したい場合に、MobileBatteryクラスを一切変更する必要がありません。各状態クラスを修正したり、追加するだけで済みます。これにより、変更の影響範囲が小さい範囲に閉じ、変更容易性が向上します。

テスト容易性が向上する

PowerSavingModeやNormalModeクラスは、状態を持っていなく隠れた入出力もありません。そのため、単純な出力値ベーステスト[2]での単体テストが可能であり、非常にテスト容易性が高いです。

まとめ

携帯の動作モードを例に、Stateパターンの仕組みと利点を解説しました。状態に関するロジックがそれぞれの状態クラスにカプセル化されることで、可読性や変更容易性、テスト容易性が向上します。
オブジェクトの状態によって振る舞いが大きく変わる場面に遭遇したら、Stateパターンの使いどころかもしれません。

脚注
  1. 田中ひさてる, PHPで理解するオブジェクト指向の活用 ちょうぜつソフトウェア設計入門, 2022年, 技術評論社, P255 ↩︎

  2. Vladimir Khorikov, 単体テストの考え方/使い方, 2022年, 株式会社 マイナビ, P168 ↩︎

Discussion