datchの日記

気がついたら社会人。気になる技術的なことについて少しずつ書いていけたらと思っております。

【デザインパターン】ステートパターン 後編

現在、内定者でチームを組んでサービスを作っているのですが、やはりサービスを考えて作るというのは面白いです。
もちろん、自分たちが見たいものばかりに目を向けていると残念な仕上がりになってしまうので、そこら辺は実際に触ってみて色々と深堀りしていければと思います。
さて、今回は久しぶりのデザインパターンで【State】パターンの後編になります!

参考書



さらっと読める「OREILLY Head First デザインパターン」を参考にしながら書いています。
しばらくはこいつを参考にデザインパターンについて書いていくよ!

前回までのあらすじ



前回(【デザインパターン】ステートパターン 前編 - datchの日記)はガムボールマシーンという、状態遷移表が定義された機械のソースコードを書いていきました。
Stateパターンを用いずに状態毎に異なる状態への遷移に対してのアクションをすべて定義しようとすると、状態がひとつ増えるごとにクラスが大きくなっているのがわかりました。

Stateパターン



さて、ガムボールマシンをStateパターンで記述する前に、どのようなパターンか確認してみる。

Compositeパターンクラス図

引用したクラス図と状態遷移図を元にして、今回のガムボールマシンで例えてみる。

Stateパターンには以下の3つのクラスが存在する。

  1. State : 状態のインターフェース
  2. ConcreteState : 状態の具象クラス
  3. Context : 複数のStateを保有するクラス。つまり、複数の状態を持つオブジェクトを指します。

これをガムボールマシンの例に置き換えてみる。

クラス図での名前ガムボールマシンでの例
ConcreteStateNoQuarter, HasQuarter, GumballSold, OutOfGumballs
ContextGumballMachine
handleinsertsQuarter, ejectsQuarter, turnsCrank, DipenseGumball

以上のように対応している。
このように

  1. 状態遷移図のそれぞれの状態に対して、ConcreteStateを割り当てていく。
  2. Context側で管理、運用する
  3. ConcreteStateが受け取った命令を取得する

ConcreateStateがContextの中で変わることで、その振る舞い(状態による動作の違い)も変えられるという便利なパターンですね。

具体的な使い方



Stateパターンを使う前にここでお約束の
_人人人人人人人人人_
> 突然の仕様変更 <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y ̄
なんと、取り締まり役が「10人に1人当たり」という仕様を追加してくれ、という要望が来ました。

以前のStateパターンを知らない状態だったら、GumballMachineクラスが肥大化している所でした。
ところが今はStateパターンという武器を知っているので柔軟に問題に対応できると
ではStateパターンを使ってガムボールマシンを書いていきましょう。


まずはContextのGumballMachineから。

public class GumballMachine
{
  State soldOutState;
  State noQuarter;
  State hasQuarterState;
  State soldState;

  State state = soldOutState;
  int count = 0;

  public GumballMachine(int numberGumballs)
  {
  	soldOutState = new SoldOutState(this);
  	noQuarterState = new NoQuarterState(this);
  	hasQuarterState = new HasQuarterState(this);
  	soldState = new SoldState(this);
  	this.count = numberGumballs;
  	if (numberGumballs > 0)
  	{
  		state = noQuarterState;
  	}
  }

  public void insertQuarter()
  {
  	state.insertQuarter();
  }

  public void ejectQuarter()
  {
  	state.ejectQuareter();
  }

  public void turnCrank()
  {
  	state.turnCrank();
  	state.dispence();
  }

  public void setState(State state)
  {
  	this.state = state;
  }

  public void releaseBall()
  {
  	System.out.println("ガムボールがスロットから転がり出てきます");
  	if (count != 0)
  	{
  		count = count - 1;
  	}
  }

  // 以下省略 getter, setterなど
}

ソースコードを見ての通り、Contextは複数の状態を持てるのが見えると思います。
では、どこで状態を遷移させるのかというと、Stateクラスの中で状態遷移を行います

そしてはインターフェースのStateクラスをみていきます。

public interface State
{
  insertQuarter();
  ejectQuarter();
  turnCrank();
  dispense();
}

それぞれのrequest()を定義したので、あとはこれをクラスが継承して具象クラスを定義してあげるだけです。

// 25セントを投入していない(お金を入れていない)状態
public class NoQuarterState implements State
{
	GumballMachine gumballMachine;

	public NoQarterState(GumballMachine gumballMachine)
	{
		this.gumballMachine = gumballMachine;
	}

	public void insertQuarter()
	{
		System.out.println("25セントを投入しました");
		gumballMachine.setState(gumballMachine.gethasQarterState());
	}

	public void ejectQuarter()
	{
		System.out.println("25セントを投入していません");
	}

	public void turnCrank()
	{
		System.out.println("クランクを回しましたが、25セントを投入していません")
	}

	public void dispnece()
	{
		System.out.println("まずは支払いをする必要があります");
	}
}

insertQuarter()の処理に注目してみましょう。
お金を入れてない状態ではお金を入れる動作以外は受け付けないようになっています。
そして、正常なリクエストがきた場合のみ、Contextの状態をStateから変化させます。

あとは同様に状態によって実装を行っていく感じですね。

さて、前回の一つにまとめていたソースに比べてかなりコードが単純明快になったのがわかると思います。
これ以降に状態が追加されてもクラスの追加とContextのrequestを追加するだけで済み、元のソースコードに大きな変更を加えずに、クラスを追加するだけで良いというプログラムを書く上でもっとも避けたい変更を最小限に済ませる、という目的が遂げられました!

おわりに



しばらくぶりのデザインパターンでしたが、Stateパターンが非常にシンプルで目的もわかりやすかったので書きやすい記事でした。
後は残すところCompountパターンのみとなりましたが、このまま最後まで駆け抜けていければと思います!
そして、次の資料を何にしようか検討中です。