2005 6 7 8 9 10 11 12
2006 1 2 3 4 5 6 7 8 9 10 11 12
2007 1 2 3 4 5 6 7 8 9 10 11 12
2008 1 2 3 4 5 6 7 8 9 10 11 12
2009 1 2 3 4 5 6 7 8 9 10 11 12
2010 1 2 3 4 5 6 7 8 9 10 11 12
2011 1 2 3 4 5 6 7 8 9 10 11 12
2012 1 2 3 4 5 6 7 8 9 10 11 12
2013 1 2 3 4 5 6 7 8 9 10 11 12
2014 1 2 3 4 5 6 7 8 9 10 11 12
2015 1 2 3 4 5 6 7 8 9 10 11 12
2016 1 2 3 4 5 6 7 8 9 10 11 12
2017 1 2 3 4 5 6 7 8 9 10

ホーム

2011年11月12日

今さらStateパターンの話。

某所で「Stateパターンは重いからイヤだ」ということで、
こんな感じのコードを見かけたんだけど:

class CDPlayer
  def handle_event(event)
    case event
    when :play
      handle_play
    when :stop
      handle_stop
    end
  end

  def handle_play
    case @state
    when :stopping
      start_music
      @state = :playing
    when :playing
      pause_music
      @state = :pausing
    when :pausing
      resume_music
      @state = :playing
    end
  end

  def handle_stop
    if @state != :stopping
      stop_music
      @state = :stopping
    end
  end

  def start_music; end
  def pause_music; end
  def resume_music; end
  def stop_music; end
end

再生ボタンを二度押しするとポーズになるという仕様ね。

まぁ、イヤだといってるのにわざわざStateパターンを
使う必要もないんだけど:

class CDPlayer
  def handle_event(event)
    state_code = @state.handle_event(event)
    case state_code
    when :playing
      @state = @playing
    when :pausing
      @state = @pausing
    when :stopping
      @state = @stopping
    end
  end

  def start_music; end
  def pause_music; end
  def resume_music; end
  def stop_music; end
end

class Playing
  def handle_event(event)
    case event
    when :play
      @player.pause_music
      return :pausing
    when :stop
      @player.stop_music
      return :stopping
    end
  end
end

class Pausing
  def handle_event(event)
    case event
    when :play
      @player.resume_music
      return :playing
    when :stop
      @player.stop_music
      return :stopping
    end
  end
end

class Stopping
  def handle_event(event)
    if event != :stop
      @player.stop_music
    end
    return :stopping
  end
end

かなり適当なコードで申し訳ない。

まぁ、これを見ると、確かに「Stateパターンて重いよね」
という話になるかもしんないけど。

でも、思ったんだけど、Stateパターンでは、多態を使って
switch文をなくすことが強調されるわけだけど。でも、
GoFだとContextっていうのがいるよね。あれが実はポイント
なんじゃないかと思ったんだよね。

関心の分離という話があって:

関心の分離

Stateパターンで実現したいのは何かというと、状態遷移と
いう関心事を切り離したいのが本線なんじゃないかと
思ったわけ:

class CDPlayer
  def handle_event(event)
    @context.handle_event(event)
  end

  def start_music; end
  def pause_music; end
  def resume_music; end
  def stop_music; end
end

class CDPlayerContext
  def handle_event(event)
    state_code = @state.handle_event(event)
    case state_code
    when :playing
      @state = @playing
    when :pausing
      @state = @pausing
    when :stopping
      @state = @stopping
    end
  end
end

この関心の分離の話は、最初のコードにもいえるわけで:

class CDPlayer
  def handle_event(event)
    @context.handle_event(event)
  end

  def start_music; end
  def pause_music; end
  def resume_music; end
  def stop_music; end
end

class CDPlayerContext
  def handle_event(event)
    case event
    when :play
      @player.handle_play
    when :stop
      @player.handle_stop
    end
  end

  def handle_play
    case @state
    when :stopping
      @player.start_music
      @state = :playing
    when :playing
      @player.pause_music
      @state = :pausing
    when :pausing
      @player.resume_music
      @state = :playing
    end
  end

  def handle_stop
    if @state != :stopping
      @player.stop_music
      @state = :stopping
    end
  end
end

で、「Stateパターンは重いか?」の話に戻るんだけど。
自分はそんなに重いとは思わないんだよね。クラスが
増えるのは面倒っちゃ面倒だけど。でも、個々の状態
クラスは重くなりにくいし。上のコードでいえば、音楽の
再生なんかに関する機能はCDPlayerに集まってるでしょ。
で、状態クラスが重くなるんだったら、それこそState
パターンが活きてくる。

--

ところで、上のコードで「停止状態のときに停止ボタンが
押されたらどうするの?」という話があって:

class Stopping
  def handle_event(event)
    if event != :stop
      @player.stop_music
    end
    return :stopping
  end
end

あ、個人的には:

class Stopping
  def handle_event(event)
    @player.stop_music
    return :stopping
  end
end

って書きたいほうなんだけど。

で、このコードだと「停止状態から停止状態に遷移する」
ってことになるんだけど。こういう感じで「ある状態から
同じ状態に遷移する」っていうのは状態遷移でよくある話で。

+------+
|aState|<--+
+------+   |
    |      |
    +------+

こんな感じの遷移図が描かれるわけだけど。

でも、現実には、「同じ状態に遷移する」というのと、
「どこにも遷移しない」というのじゃ違うことがあるん
だよね。遷移の入り口と出口にフックを仕掛けるときとか。

「どこにも遷移しない」というのを実現するとすると:

class CDPlayerContext
  def handle_event(event)
    state_code = @state.handle_event(event)
    case state_code
    when :playing
      @state = @playing
    when :pausing
      @state = @pausing
    when :stopping
      @state = @stopping
    when :stay
      ;
    end
  end
end

class Stopping
  def handle_event(event)
    if event == :stop
      return :stay
    end
    @player.stop_music
    return :stopping
  end
end

ほら、ここでContextを分離しておいた効果が出てきた。
CDPlayerをいじらずに、遷移の仕方を変えることができた。

だから、やっぱり、状態クラスを抽出するよりも、まずは
Contextを抽出したほうがいいんだよね。

本家Permlink


Copyright © 1905 tko at jitu.org

バカが征く on Rails