ぱろっと・すたじお

技術メモなどをまったりと / my site : http://parrot-studio.com/

Rubyっぽくリファクタリングするためのポイント(3)

久々にネタを拾ったのでメモ...φ(・ω・`)


今回はちょっと仕様が複雑です
しかも、あまりRubyと直接関係ない部分もありますが、
Rubyだと簡潔に書ける例ということで


<今回の仕様>

  • 各データには値A、フラグB、フラグCが存在する
  • データの状態に合わせて実行されるメソッド、method_[a..f]がある
  • 値Aに対して範囲の境界値x, y, z (x < y < z)が存在する
  • 以下の条件に従って、メソッドにデータを渡して処理させる(-は条件を指定しないの意)
No. A B C method
1 - true false method_a
2 x <= A < y false false method_b
3 y <= A < z false false method_c
4 A >= z false false method_d
5 - - true method_e
  • 条件に当てはまらないデータはmethod_fを実行する


まず、これを単純に並べてみますと・・・

if data.B && !data.C
  method_a(data)
elsif data.A >= x && data.A < y && !data.B && !data.C
  method_b(data)
elsif data.A >= y && data.A < z && !data.B && !data.C
  method_c(data)
elsif data.A >= z && !data.B && !data.C
  method_d(data)
elsif data.C
  method_e(data)
else
  method_f(data)
end

・・・こうなりますが、まさかこんな風に書く人はいませんよね?
(正直、この時点でコードが合ってるのかとても疑問ですが・・・(´・ω・`))


さすがにこれは冗長なので、条件を整理しましょう
フラグB/Cに着目すると・・・

if data.C
  method_e(data)
else
  if data.B
    method_a(data)
  else
    if data.A >= x && data.A < y
      method_b(data)
    elsif data.A >= y && data.A < z
      method_c(data)
    elsif data.A >= z
      method_d(data)
    else
      method_f(data)
    end
  end
end

・・・こうなりますね
これも間違ってはいないのですが、値の順序に着目すると・・・

if data.C
  method_e(data)
else
  if data.B
    method_a(data)
  else
    if data.A >= z
      method_d(data)
    elsif data.A >= y
      method_c(data)
    elsif data.A >= x
      method_b(data)
    else
      method_f(data)
    end
  end
end

・・・この辺が限界ですか
少なくとも、Javaならこう書くでしょう
(このコードも合ってるかいまいちわからん・・・(´・ω・`))



さて、ここまでの話はあまりRubyと関係がなく、
情報処理の基本的な話でしかありません
ここからが「Rubyっぽいコード」です


そもそも、Rubyで条件判定したい場合、
if〜elsif〜endや三項演算子なんかがメジャーですが、
意外と忘れられているのがcase〜when〜end文です


普通だと・・・

case val
when 'a'
  # do something
when 'b'
  # do something
else
  # do something
end

・・・こんな感じで、値の比較に使います


Javaだとここの条件には数値しか使えませんが、
Rubyは「===」で比較ができれば何でも対象にできます
(後述のおまけ参照)


このとき、case句に値を与えないと、各when句の文の真偽で値が決まります
つまり・・・

case   # 何も与えない
when false
  puts "ここは実行されない"
when true
  puts "ここが実行される"
else
  puts "ここも無視"
end
# => ここが実行される

・・・こう書けます


ということで、先ほどのぐちゃっとしたコードを書きなおしますと・・・

case
when data.C
  method_e(data)
when data.B
  method_a(data)
when data.A >= z
  method_d(data)
when data.A >= y
  method_c(data)
when data.A >= x
  method_b(data)
else
  method_f(data)
end

・・・素晴らしい(`・ω・´) b


case文は「最初に条件を満たした文」を実行するので、
こういった流れのコードが簡潔に書けるわけです


ということで、Rubyでelsifが多様されるようなら、
case文を使えないか考えてみてはいかがでしょう






<おまけ1>
Rubyにおいて「真偽」とは、「nilとfalseが偽で、他は全部真」ですので、
例えば・・・

case
when nil
  puts "when1"
when 'a'
  puts "when2"
end
# => when2

・・・こういうのもありです



<おまけ2>
case文の応用で、例えば
「数値が来たらそれを2倍するが、数値でなければそのまま返す」
なんてのが一文で書けます

def case_test(val)
  case val
  when Numeric # Numericを継承or実装したクラスのオブジェクト
    val * 2
  else
    val
  end
end

p case_test(2)      #=> 4
p case_test("test") #=> "test"

つまり、比較対象にクラスを渡すと、
そのクラスに属するかを比較するわけです


これを応用しますと・・・

case target
when Player
  # 対象がプレイヤーなら回復
  heal_to(target, atk)
when Monster
  # 対象がモンスターならダメージ
  damage_to(target, atk)
end

・・・こんなコードが書けまして、
実際に「ROgue(仮)」で使ってます