【PHPで学ぶデザインパターン入門】第3回 Stateパターン

【PHPで学ぶデザインパターン入門】第3回 Stateパターン

王

こんにちは、エンジニアの王です。

【PHPで学ぶデザインパターン入門】も、第1回「Strategyパターン」第2回「Decorator」に続き第3回目です。

今回の「State パターン」は、「状態」に関わる処理を行うときによく使うパターンだということを名前からでも用意に連想できると思います。

人間には「喜怒哀楽」、物質には「固体・液体・気体・プラズマ」、信号には「赤青黄」とそれぞれ状態があるように、物にはさまざまな「状態」があります。そして、状態が変われば物の振る舞い方も変わってくる場合が多いのです。

「State パターン」は、ころころ状態が変化する場合でも管理のしやすいコードを書くための一つの設計手段です。

「信号機」クラスを作ってみる

では、「信号機」クラスを例に考えてみましょう。

「信号機」クラスは、以下のメソッドを持っているとします。

  • to_green(緑に変える)
  • to_red(赤に変える)
  • to_yellow(黄色に変える)

そして、最終的にはこのように使うとします。

$traffic_light = new TrafficLight(); // 信号機のデフォルトの色は青です


$traffic_light->to_green();
// -> 既に青に変わっている


$traffic_light->to_red();
// -> 青から赤に変える


$traffic_light->to_yellow();
// -> 赤から黄色に変える


$traffic_light->to_green();
// -> 黄色から青に変える


$traffic_light->to_green();
// -> 既に青に変わっている


$traffic_light->to_yellow();
// -> 青から黄色に変える

実装コード

<?php
class TrafficLight {

	static $R = '赤', $G = '青', $Y = '黄色';
	private $state;

	function __construct() {
		$this->state = static::$G;
	}

	public function to_green() {
		if ( $this->state === static::$G ) {
			echo "既に青に変わっている";
		} else {
			echo $this->state . "から青に変える";
			$this->state = static::$G;
		}
	}

	public function to_red() {
		if ( $this->state === static::$R ) {
			echo "既に赤に変わっている";
		} else {
			echo $this->state . "から赤に変える";
			$this->state = static::$R;
		}
	}

	public function to_yellow() {
		if ( $this->state === static::$Y ) {
			echo "既に黄色に変わっている";
		} else {
			echo $this->state . "から黄色に変える";
			$this->state = static::$Y;
		}
	}

}

$traffic_light = new TrafficLight();
?>

<section>
	<h1><code>$traffic_light->to_green()</code></h1>
	<?php $traffic_light->to_green(); ?>
</section>

<section>
	<h1><code>$traffic_light->to_red()</code></h1>
	<?php $traffic_light->to_red(); ?>
</section>

<section>
	<h1><code>$traffic_light->to_yellow()</code></h1>
	<?php $traffic_light->to_yellow(); ?>
</section>

<section>
	<h1><code>$traffic_light->to_green()</code></h1>
	<?php $traffic_light->to_green(); ?>
</section>

<section>
	<h1><code>$traffic_light->to_green()</code></h1>
	<?php $traffic_light->to_green(); ?>
</section>

<section>
	<h1><code>$traffic_light->to_yellow()</code></h1>
	<?php $traffic_light->to_yellow(); ?>
</section>

出力結果:

$traffic_light->to_green()

既に青に変わっている

$traffic_light->to_red()

青から赤に変える

$traffic_light->to_yellow()

赤から黄色に変える

$traffic_light->to_green()

黄色から青に変える

$traffic_light->to_green()

既に青に変わっている

$traffic_light->to_yellow()

青から黄色に変える

1つのクラスで完結しています。全然悪くないと思います!

State パターンで書き換えると?

上記の「信号機」を「State パターン」に変えるとどうなるのか見て行きましょう。

<?php

// Stateの抽象クラス
abstract class TrafficLightState {
	/** @type TrafficLight $traffic_light */
	protected $traffic_light;

	function __construct( TrafficLight $traffic_light ) {
		// TrafficLightオブジェクトの参照を持たせる
		$this->traffic_light = $traffic_light;
	}

	/**
	 * TrafficLightオブジェクトの「状態」を設定する
	 *
	 * @param string $state 状態の名前
	 */
	function to_state( $state ) {
		$this->traffic_light->set_state( $this->traffic_light->get_state( $state ) );
	}

	public function to_green() {
		$from = $this->get_last_state();
		$this->to_state( 'green_state' );
		echo "{$from}から青に変える";
	}

	public function to_red() {
		$from = $this->get_last_state();
		$this->to_state( 'red_state' );
		echo "{$from}から赤に変える";
	}

	public function to_yellow() {
		$from = $this->get_last_state();
		$this->to_state( 'yellow_state' );
		echo "{$from}から黄色に変える";
	}

	/**
	 * 最後の信号機の色を取得する
	 * @return string
	 */
	private function get_last_state() {
		$current_state = $this->traffic_light->get_state();
		$last_state    = '青';

		if ( $current_state instanceof RedState ) {
			$last_state = '赤';
		}
		if ( $current_state instanceof YellowState ) {
			$last_state = '黄色';
		}

		return $last_state;
	}
}

// TrafficLightState抽象クラスの具象クラス。「状態」ごとに少しず実装が違う。
class GreenState extends TrafficLightState {

	public function to_green() {
		echo "既に青に変わっている";
	}
}

class RedState extends TrafficLightState {

	public function to_red() {
		echo "既に赤に変わっている";
	}

}

class YellowState extends TrafficLightState {

	public function to_yellow() {
		echo "既に黄色に変わっている";
	}

}


// 主役の TrafficLightクラス。
class TrafficLight {
	/** @type TrafficLightState $state */
	private $state;

	private $red_state;
	private $green_state;
	private $yellow_state;

	function __construct() {
		// TrafficLightオブジェクトに全ての状態を持たせる
		$this->red_state    = new RedState( $this );
		$this->green_state  = new GreenState( $this );
		$this->yellow_state = new YellowState( $this );

		// 青をデフォルトの状態にする
		$this->state = $this->green_state;
	}

	public function to_green() {
		// 処理をState(GreenState)に委託する
		$this->state->to_green();
	}

	public function to_red() {
		// 処理をState(RedState)に委託する
		$this->state->to_red();
	}

	public function to_yellow() {
		// 処理をState(YellowState)に委託する
		$this->state->to_yellow();
	}

	public function set_state( TrafficLightState $state ) {
		$this->state = $state;
	}

	public function get_state( $state = null ) {
		return $state ? $this->$state : $this->state;
	}
}


$traffic_light = new TrafficLight();
?>

<section>
	<h1><code>$traffic_light->to_green()</code></h1>
	<?php $traffic_light->to_green(); ?>
</section>

<section>
	<h1><code>$traffic_light->to_red()</code></h1>
	<?php $traffic_light->to_red(); ?>
</section>

<section>
	<h1><code>$traffic_light->to_yellow()</code></h1>
	<?php $traffic_light->to_yellow(); ?>
</section>

<section>
	<h1><code>$traffic_light->to_green()</code></h1>
	<?php $traffic_light->to_green(); ?>
</section>

<section>
	<h1><code>$traffic_light->to_green()</code></h1>
	<?php $traffic_light->to_green(); ?>
</section>

<section>
	<h1><code>$traffic_light->to_yellow()</code></h1>
	<?php $traffic_light->to_yellow(); ?>
</section>

出力結果:

$traffic_light->to_green()

既に青に変わっている

$traffic_light->to_red()

青から赤に変える

$traffic_light->to_yellow()

赤から黄色に変える

$traffic_light->to_green()

黄色から青に変える

$traffic_light->to_green()

既に青に変わっている

$traffic_light->to_yellow()

青から黄色に変える

コード量がかなり長くなりましたね!
これなら「普通」のやり方のほうがいいのでは?と思ってしまいそうですが…。

上の説明をすると、「状態」によって挙動が変わるメソッド(to_green, to_red, to_yellow)をすべて「Stateオブジェクト」に委託することにしたんです。それぞれの「状態オブジェクト(GreenState, RedState, YellowState)」内での処理は、その状態を前提としているので、いちいち現在の状態が何なのかを気にする必要がなくなったところにも注目してほしいです。

とは言え、コードの絶対量がかなり増えました。では、こうまでするメリットがどこにあるのかと言うと……。

メリット

  • 状態を判定せずに済む
  • 状態に関わる処理の部分を分離することで、メインクラス(TrafficLightクラス)の見通しがよくなる
  • メインクラスを拡張しやすくなる

確かにごく単純なプログラムなら、最初の書き方のほうがコード量が少なくていいのかもしれません。ただ、大掛かりのシステムを作るとなると話は別で、改修に改修を重ね、一つのクラスの中でコードがどんどん膨らんで行きますので、いわゆる「God Class」になってしまいます。

クラス図

State_Design_Pattern_UML_Class_Diagram

上記の例に照らし合わせると

  • Context → TrafficLight
  • State → TrafficLightState
  • ConcreteState → GreenState, RedState, YellowState
  • +handle( +request ) → to_green, to_red, to_yellow

複数の状態が同時に存在する場合は?

状態とは言っても、複数の状態が同時に存在する場合と、1つの状態しか存在しない場合があります。

水筒を例に説明したいと思います。
水筒の中の水を飲みたいとします。

このとき、水が入っている状態とそうでない状態があり、水が入っている状態なら「飲む」ことができます。

でも、そもそも水筒の蓋が閉まっている状態の場合、まず「開ける」必要がありますので、そのままでは飲めないわけです。

ここで、水筒に関わる2つの状態が同時に存在していることに気づくことができると思います。

  • 水筒に水が入っている状態
  • 水筒の蓋が開いている状態

この2つの状態が同時に発生して始めて水が飲めます。

このように、物には複数の状態が同時に存在する場合もあります。それぞれの状態が影響し合うこともしばしばあると思います。このような場合は、古典的なStateパターンでは対応しきれません。

状態が煩雑になるとコードが読みづらくなるのは言うまでもないので、Stateの概念を参考にしながら、自分なりのソリューションを提示しておきたいと思います。

その前に、まずは水筒でできることをリストアップしましょう(メソッド)。

  • 蓋をする
  • 蓋を開ける
  • 中の水を飲む
  • 中の水を補充

次に、水筒の状態もリストアップします。

  • 蓋が開いている
  • 蓋が閉まっている
  • 水がある
  • 水がない

仮に、「状態」が「蓋が閉まっている」場合に、「水を飲む」しようとしたら、まず「蓋を開ける」しなければならないよね?
逆に「蓋が開いている」状態ならば、そのまま「水を飲む」すればいいわけです。このように、状態次第で、同じ「水を飲む」行為でも、行う手順が違ってきます。

実装コード

では、実装コードです。かなり長いです……。

<?php
ini_set( 'display_errors', 1 );

class CapState {
	/** @type  WaterBottle $water_bottole */
	private $water_bottle;
	private $is_opened;

	function __construct( $water_bottle, $is_opened = false ) {
		$this->water_bottle = $water_bottle;
		$this->is_opened    = $is_opened;
	}

	public function open_cap() {
		if ( $this->is_opened ) {
			echo "蓋は既に開いている<br>";
		} else {
			$this->is_opened = true;
			echo "蓋を開けた<br>";
		}
	}

	public function close_cap() {
		if ( $this->is_opened ) {
			$this->is_opened = false;
			echo "蓋を閉めた<br>";
		} else {
			echo "蓋は既に閉まっている<br>";
		}
	}

	public function drink_water() {
		if ( ! $this->is_opened ) { //蓋が閉まっている場合、まず蓋を開ける
			$this->open_cap();
		}
	}

	public function fill_water() {
		$this->drink_water();
	}
}

class WaterState {
	/** @type  WaterBottle $water_bottole */
	private $water_bottle;
	private $has_water;

	function __construct( $water_bottle, $has_water = true ) {
		$this->water_bottle = $water_bottle;
		$this->has_water    = $has_water;
	}

	public function drink_water( $drink_amount ) {
		// Cap側の処理
		$this->water_bottle->get_cap_state()->drink_water();


		$the_rest = $this->water_bottle->get_water_amount() - $drink_amount; // 残りの水の量

		if ( ! $this->has_water ) { //水がない場合、まず水を補充する
			$this->fill_water();
		}

		if ( $the_rest < 0 ) {
			$this->has_water = false;
			$drink_amount    = $this->water_bottle->get_water_amount();
			$this->water_bottle->set_water_amount( 0 );
		} else {
			$this->water_bottle->set_water_amount( $the_rest );
		}
		echo $drink_amount . "mlの水を飲んだ。残り" . $this->water_bottle->get_water_amount() . "mlです<br>";
	}

	public function fill_water( $fill_amount = null ) {
		// Cap側の処理
		$this->water_bottle->get_cap_state()->fill_water();


		$water_amount = $this->water_bottle->get_water_amount();
		//
		$capacity = $this->water_bottle->get_capacity();
		if ( empty( $fill_amount ) ) {
			$fill_amount = $capacity;
		}

		if ( $capacity === $water_amount ) { //水がない場合、まず水を補充する
			echo "水は満タンです、補充する必要がない。<br>";

			return;
		}

		$amount_after_filled = $water_amount + $fill_amount;
		if ( $amount_after_filled < $capacity ) {
			$this->water_bottle->set_water_amount( $amount_after_filled );
		} else {
			$this->water_bottle->set_water_amount( $capacity );
		}
		echo "水を補充した。現在の水の量は" . $this->water_bottle->get_water_amount() . "mlです。<br>";
	}

}

class WaterBottle {
	private $capacity; // 水筒の最大容量
	private $water_amount; // 水の容量

	/** @type  WaterState $water_state */
	private $water_state; // 水の有りなしの状態を表すクラス
	/** @type  CapState $cap_state */
	private $cap_state; // 蓋の開閉の状態を表すクラス

	function __construct( $capacity, $water_amount = null ) {
		if ( empty( $water_amount ) ) {
			$water_amount = $capacity;
		}
		$this->capacity     = $capacity;
		$this->water_amount = ( $water_amount < 0 ) ? 0 : $water_amount;

		// 「状態」の初期化を行う
		if ( $this->water_amount === 0 ) {
			$this->water_state = new WaterState( $this, false ); //「水がない状態」のオブジェクトを作る
		} else {
			$this->water_state = new WaterState( $this ); //「水がある状態」のオブジェクトを作る
		}
		$this->cap_state = new CapState( $this ); //「蓋が閉まっている状態」のオブジェクトを作る 
	}

	public function open_cap() {
		$this->cap_state->open_cap();
	}

	public function close_cap() {
		$this->cap_state->close_cap();
	}

	public function drink_water( $amount ) {
		$this->water_state->drink_water( $amount );
	}

	public function fill_water( $amount = null ) {
		$this->water_state->fill_water( $amount );
	}


	public function get_water_amount() {
		return $this->water_amount;
	}

	public function get_capacity() {
		return $this->capacity;
	}

	public function set_water_amount( $water_amount ) {
		$this->water_amount = $water_amount;
	}

	public function set_capacity( $capacity ) {
		$this->capacity = $capacity;
	}

	public function get_water_state() {
		return $this->water_state;
	}

	public function get_cap_state() {
		return $this->cap_state;
	}
}


$water_bottle = new WaterBottle( 150 );
?>

<body>
<section>
	<h1><code>$water_bottle->open_cap();</code></h1>
	<?php $water_bottle->open_cap(); ?>
</section>

<section>
	<h1><code>$water_bottle->open_cap();</code></h1>
	<?php $water_bottle->open_cap(); ?>
</section>

<section>
	<h1><code>$water_bottle->close_cap();</code></h1>
	<?php $water_bottle->close_cap(); ?>
</section>

<section>
	<h1><code>$water_bottle->fill_water( 120 );</code></h1>
	<?php $water_bottle->fill_water( 120 ); ?>
</section>

<section>
	<h1><code>$water_bottle->drink_water( 40 );</code></h1>
	<?php $water_bottle->drink_water( 40 ); ?>
</section>

<section>
	<h1><code>$water_bottle->drink_water( 1000 );</code></h1>
	<?php $water_bottle->drink_water( 1000 ); ?>
</section>

<section>
	<h1><code>$water_bottle->fill_water();</code></h1>
	<?php $water_bottle->fill_water(); ?>
</section>

<section>
	<h1><code>$water_bottle->close_cap();</code></h1>
	<?php $water_bottle->close_cap(); ?>
</section>

<section>
	<h1><code>$water_bottle->fill_water();</code></h1>
	<?php $water_bottle->fill_water(); ?>
</section>
</body>

出力結果:

$water_bottle->open_cap();

蓋を開けた

$water_bottle->open_cap();

蓋は既に開いている

$water_bottle->close_cap();

蓋を閉めた

$water_bottle->fill_water( 120 );

蓋を開けた
水は満タンです、補充する必要がない。

$water_bottle->drink_water( 40 );

40mlの水を飲んだ。残り110mlです

$water_bottle->drink_water( 1000 );

110mlの水を飲んだ。残り0mlです

$water_bottle->fill_water();

水を補充した。現在の水の量は150mlです。

$water_bottle->close_cap();

蓋を閉めた

$water_bottle->fill_water();

蓋を開けた
水は満タンです、補充する必要がない。

メインクラス内での記述がスッキリしたのはよかったのですが、これじゃあコードが膨らみすぎて返って読みづらいですね……。

結論:無理にパターンを適用するよりも、ケース・バイ・ケースで「普通に」書いたほうがいいのかもです>_<。

まとめ

お気づきかもしれませんが、メソッド内で他のオブジェクトに処理を委託するところで、Strategyパターンと酷似していますよね。と言うか、クラス図を見る限り全く一緒ですね!

なぜわざわざ2つのパターンとして分類したのか。
忘れないでほしいのは、パターンを考えるときに、「意図」はすごく大事ということ。StrategyパターンとStateパターンでは「意図」しているところがそもそも違うんです。

Strategyは作成時に動的にメソッドの挙動を決めておき、作成が終わっても変動する予定はありません。

それに対して、Stateは「状態の変動」を意識していて作成後でも、メソッドの挙動が状態によってころころ変化します
状態が変化しないのなら、Stateパターンではなく、Strategyパターンだと言えます。

パターンの意図しているところを考えて、臨機応変に対応していきましょう。

 

【王(キング)!キング!キング!】

WordPressのWidget(ウィジェット)を自作する方法

WordPressをサポートするようになるPhpStorm 8の使い方とその特徴について

目指せキーボードマスター!マウス操作なしで生産性をUPするおすすめアプリ

初心者でも分かる!git rebaseの使い方を解説します

Web開発者に革命をもたらす!「Web Components」超入門

LIGはWebサイト制作を支援しています。ご興味のある方は事業ぺージをぜひご覧ください。

Webサイト制作の実績・料金を見る

この記事のシェア数

LIGの王です。ウェブの全てを学ぶ為、中国は四川省より日本にやってきました。王という名に恥じぬよう、ウェブ業界のKINGとなるべく日々頑張っております。よろしくお願いいたします。

このメンバーの記事をもっと読む