AASM Gemで学ぶRubyステートマシン入門|Stateデザインパターンから実践的な使い方まで
今回は、ステートマシン(状態遷移マシン)の仕組みと、RubyプロジェクトでAASM gemを使ってステートマシンを活用する方法を解説します。
まずは身近な例から考えてみましょう。
信号機を想像してください。信号機は「赤」「黄」「青」のいずれかの状態を取ります。
そして、色が変わるとき、次に何色になるかは現在の色によって決まっています。

ここでは、視覚障害のある方が渡れるタイミングを音で知らせるタイプの信号機だとしましょう。
さて、ここからが本題です。
あなたがこの信号機のソフトウェアを開発することになったとします。
毎回どの音を鳴らすべきか、次はどの色に変わるべきか——それをどう判断すればよいのでしょうか?
素朴に書くなら、次のようなif文になるかもしれません。
if @light.state == "green" @light.play_green_sound end if @light.state == "green" @light.change_to_yellow end # ...
このように書くと、状態チェックのコードがプログラム中のあちこちに散らばってしまいます。
どうすれば改善できるでしょうか?
オブジェクト指向設計の原則を適用していくと、自然とStateデザインパターンに行き着きます。
Stateデザインパターンとは?
Stateデザインパターンは、ステートマシンを実装するための手法の一つです。
必要な構成要素は3つあります。
Contextクラス:現在の状態を保持しているクラスStateクラス:個々の状態が実装すべきメソッドを定義する基底クラス- 各状態ごとのクラス:
Stateクラスを継承した具象クラス
信号機の例では、ContextがTrafficLight(信号機)そのものです。
そして状態はGreen(青)、Red(赤)、Yellow(黄)の3つになります。
それぞれの状態が自分自身の振る舞いをすべて知っているのがポイントです。
大きなメリットは何でしょうか?
各状態が自分自身を把握しているため、現在の状態を逐一チェックする必要がありません。これはつまり、複雑性の温床となりがちな条件分岐を大幅に減らせることを意味します。
信号機の実装例
実際のコードでこのパターンを見てみましょう。
まずはTrafficLightクラスです。
class TrafficLight
def initialize
@state = nil
end
def next_state(klass = Green)
@state = klass.new(self)
@state.beep
@state.start_timer
end
end
続いて、基底となるStateクラスです。
class State
def initialize(light)
@light = light
end
def beep
end
def next_state
end
def start_timer
end
end
そう、これら3つのメソッドは空です。
Javaなどの他の言語では、このような「インターフェース」を定義するのが一般的ですが、Rubyではあまり普及していません。ここでは説明のためにあえて定義しています。
ただし、initializeメソッドだけは全状態で共有したいポイントです。どの状態も、状態遷移を通知するためにContext(TrafficLightオブジェクト)への参照を必要とするからです。
さて、3つの状態クラスは互いによく似ていますので、ここでは1つだけコードを紹介します。
こちらがGreen(青)の状態です。
class Green < State
def beep
puts "Color is now green"
end
def next_state
@light.next_state(Yellow)
end
def start_timer
sleep 5; next_state
end
end
このように、各状態は「次の状態へどう切り替えるか」「いつ切り替えるか」を自分自身で知っています。
AIゲームでの応用例
ステートマシンは、現在の状態に依存するゲームの攻略にも活用できます。たとえば「RubyWarrior」というプログラミングゲームが良い例です。
RubyWarriorでは、プレイヤーオブジェクトと盤面が与えられます。
目標は次の2つです。
- 盤面上の敵をすべて倒すこと
- HPを0以上に保ったまま出口に到達すること
1ターンに1回だけ行動でき、レベルをクリアするには毎回最適な選択をする必要があります。
その選択には「現在の状態」を把握することが役立ちます。だからこそ、ステートマシンが有効なソリューションになるのです。
具体例を見てみましょう。
class Attacking < State
def play(warrior)
warrior.attack!
@player.set_state(Healing) unless enemy_found?(warrior)
end
end
これは戦士が取りうる状態の一つです。視界内に敵がいなくなったら、Healing(回復)状態へ移行して、戦闘によるダメージを回復します。
AASM Gemの使い方
現在の状態を管理しつつ、不正な遷移を防ぎたい場合は、AASMのようなステートマシン用gemを使うのがおすすめです。
AASMは、イベント(照明のスイッチを押すようなもの)をトリガーとして、別の状態への遷移を起こすという考え方で設計されています。
サンプルコードはこちらです。
require 'aasm'
class Light
include AASM
aasm do
state :on, :off
event :switch do
transitions :from => :on, :to => :off, :if => :on?
transitions :from => :off, :to => :on, :if => :off?
end
end
end
このクラスの使い方は以下の通りです。
light = Light.new p light.on? # true light.switch p light.on? # false
このステートマシンでは、「off」状態のときにのみ「on」状態へ遷移できます。逆方向も同様です。さらに、状態遷移の前後で特定のコードを実行するコールバック(before/after)も複数登録できます。
こうしたコールバックで実現できる処理の例を挙げます。
- メールの送信
- 状態変更のログ記録
- ライブ監視ダッシュボードの更新
さらに、AASMにはActiveRecordを使って現在の状態をデータベースに保存するオプションもあります。Railsアプリケーションとの相性も抜群です。
まとめ
今回は、ステートマシンの基本概念、Stateデザインパターン、そしてAASM gemの使い方を学びました。
条件分岐だらけのコードを、状態ごとに整理された美しい設計にリファクタリングできるのがステートマシンの魅力です。ぜひ自分のプロジェクトでも試してみてください。
新しいアイデアを、実際に手を動かして練習してみましょう 🙂
-
Rubyのmapメソッド完全ガイド!配列・ハッシュのデータ変換を実例で解説
Rubyのmapメソッドは、配列(Array)・ハッシュ(Hash)・範囲(Range)といったコレクションに対して使用できる強力なメソッドです。 mapの主な用途は、データの変換です。 例えば、文字列の配列があった場合、すべての文字列を順番に処理して、各文字を大文字に変換することができます。 また、Userオブジェクトのリストがある場合も同様です。 それらを変換して、対応するメールアドレスや電話番号など、Userクラスに定義された任意の属性のリストを作成できます。 それでは、具体的な使い方を見ていきましょう! Ruby mapメソッドの基本構文 mapの構文は次のようになっています。 a
-
Rubyの配列(Array)クラスの使い方徹底解説!基本操作と便利なメソッド一覧
配列(Array)とは? 配列はRubyに組み込まれたクラスで、0個以上の要素をリスト形式で保持することができます。さらに、要素の追加・アクセス・繰り返し処理を簡単に行えるメソッドも多数用意されています。 もし配列が存在しなければ、値ごとに個別の変数を用意しなければならず、管理が非常に大変になります。 例: a = 1 b = 2 c = 3 しかし、配列を使えば次のように書けます: numbers = [1, 2, 3] さらに嬉しいことに、配列の中には何でも入れられます! 例えば: 数値 文字列 さらに別の配列!(これは多次元配列になります) それでは、配列を最大限に活用できる