Skip to content

Instantly share code, notes, and snippets.

@kyohei-shimada
Last active June 5, 2019 09:47
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save kyohei-shimada/c254ee9661042ab8a25e to your computer and use it in GitHub Desktop.
Save kyohei-shimada/c254ee9661042ab8a25e to your computer and use it in GitHub Desktop.
rubyの条件分岐

Rubyの条件分岐に関するオレオレまとめ

前提

  • 分岐は少なければ少ないほうがよい
  • とはいえ絶対に必要なのでどうすればよいか?
    • ネストを減らす
      • 分岐を仮に愚直にやっていくとn乗オーダーで処理が分かれていき,とてもじゃないが読めない
    • 統一感,対称性を持たせる
    • 適切なメソッド化をする
    • 分岐は処理の流れが変わるという重要な役割をもつため.分岐をするのであれば分岐をするという役割でメソッドを1つ切る(dispatcherが典型)
      • 深いネストやスパゲティを防止できる

if ... end

  • 特定の場合のみ追加的な処理をしたい場合
if File.exist?(path)
  File.delete(path)
end
#...

後置if

  • 特定の__例外的な__条件を事前に除きたい場合(いわゆるガード節)
class Node
  def parent
    return nil if self.root?
    
    # ルートノードのめんどくさいことは気にせず純粋に親ノードをとる処理に集中できる
  end
end

if ... else ... end

  • 明確に処理が2分割される場合
    • if の処理とelseの処理は対等な抽象度であり,相反するものが望ましい
    • 対等な抽象度になっていない場合,メソッド化やクラスを見直したほうがいい
def index
  if smart_phone?
    render 'sp_index.html'
  else
    render 'pc_index.html'
  end
end
# こういうのはふさわしくない
def index
  if smart_phone?
    return render 'sp_index.html'
  end
  render 'pc_index.html'
end

# 一方で,こういう場合は後置ifを使うほうがのぞましい
# (リダイレクトが例外的な処理である場合)
def index
  return redirect_to 'top_page' if smart_phone?
  
  render 'index.html'
end

if ... elsif ... else ... end

  • if ... else ... endの時と同様に対等であること
  • 単純に数が多い場合などはcaseを検討してもよいが,複数の条件が絡み合っている場合,2つ以上の役割をもたせている場合がほとんどなので,メソッドやクラスを考えなおす
  • 最後のelseに例外的な処理を書かないといけない場合,ガード節でシンプルになる場合がある
def cheese_io(arg)
  if arg.is_a?(IO)
    arg
  elsif arg.is_a?(String)
    File.open(arg)
  else
    StringIO.new(arg)
  end
end
# elseに例外
if xxx
  #xxx
elsif yyy
  #yyy
else
  raise ArgumentError
end

# 例外の条件を先に排出可能であれば
raise ArgumentError if zzz

if xxx
  #xxx
else
  #yyy
end

if?, unless?

  • if, unlessどっちを使うのか?
    • 基本的にはシンプルな方
# return [] if !users.empty?
return [] unless users.empty?
  • そもそも論理演算が複雑になっているものはその論理演算を一つのメソッドにするなどを検討する
# つらい
if user.logged_in? && !user.admin?
  # ...
end

# 非特権ユーザの判定であることがすぐにわかる
class User
  def non_privilege_user?
    logged_in? && !admin?
  end
end

if user.non_privilege_user?
  # ...
end
  • 反転した場合もシンプルさがかわらない場合,後続に続く処理にとって自然なほうを選ぶ
    • 下記の例の場合,siblingsにとって関心のあるのは自分がルートであるかどうかよりも,親がいるかどうかのほうが気になるのでBのほう
class Node
  def parent
    # 親ノードをとってくる処理
  end
  
  def root?
    parent == nil
  end
  
  def has_parent?
    !root?
  end

  # A
  def siblings
    return [] if root?
    parent.children.delete_if {|sibling| sibling == self }
  end
  
  # B
  def siblings
    return [] unless has_parent?
    parent.children.delete_if {|sibling| sibling == self }
  end
end

三項演算子( ... ? ... : ... )

  • ある条件に対し2つに値が別れ,その値を使用する場合.スイッチのイメージ
  • 条件文,true, falseの場合の処理が十分にシンプルになっていない場合,メソッドやクラスを考える
  • 成功したらオブジェクト,失敗したらnilのような場合のメソッドをtrue, falseに置き換える場合
    • ただしフラグ的になりがちなので気をつける
# それぞれのクラスに同一メソッドを定義しておいてポリモーフィックにできるとイイ感じになる
travel_plan = has_money? ? ExpensiveTravelPlan.new : CheapTravelPlan.new
travel_plan.price
travel_plan.term
# ?系メソッドを新たに生やす
def find?(element)
  find(element) ? true : false
end

case ... when

  • かなり柔軟に機能をもつので詳細は http://docs.ruby-lang.org/ja/2.2.0/doc/spec=2fcontrol.html#case 参照
  • if文となどとの大きな違いとして,__whenで指定したものを左辺,caseに指定したものを右辺__とし===演算子で比較される点
    • ===演算子はクラスによって定義がことなり,==と同じものもあれば全然違うものもある
    • 一般的には==はオブジェクトの等価性を評価するために使い,===は範囲をもたせたものに使うことがが多いので,多くの場合===のほうがtrueとなる範囲が広いことが多いが,全ては定義次第なので必ずしもそうとは限らない
    • 多くのオブジェクトでは==はequal,===はincludeやcoverのイメージで使われていることが多い
# Regex
# http://docs.ruby-lang.org/ja/2.2.0/method/Regexp/i/=3d=3d.html
# http://docs.ruby-lang.org/ja/2.2.0/method/Regexp/i/=3d=3d=3d.html
/^http/ == "http" #=> false
/^http/ === "http" #=> true

def protocol(url)
  case str
  when /^https/ then "HTTPS"
  when /^http/ then "HTTP"
  when /^ftp/ then "FTP"
  end
end
protocol("https://google.com") #=> "HTTPS"
protocol("http://example.com") #=> "HTTP"
protocol("unknown") #=> nil

# Range
# http://docs.ruby-lang.org/ja/2.2.0/class/Range.html
(1..10) == 0 #=> false
(1..10) == 1 #=> false
(1..10) == 2 #=> false

(1..10) === 0 #=> false
(1..10) === 1 #=> true
(1..10) === 5 #=> true

def rank(score)
  case score
  when (80...100) then "A"
  when (70...80)  then "B"
  when (60...70)  then "C"
  when (0...60)   then "D"
  end
end

rank(90) #=> "A"
rank(70) #=> "B"
rank(55) #=> "D"

その他

直接的には条件分岐ではないが || などもあったりする

条件分岐の粒度

(例)

あるゲームを考える.ゲームには100点満点のscoreがある.80点以上ならAランク, 70点以上80点未満ならBランク, 60点以上70点未満ならCランク,60未満ならDランクであり,A,Bランクの場合次のステージはボーナスステージになる.それ以外のC, D の場合通常のステージになる.

この時,スコアを受け取り,次のステージ情報を返すメソッドnext_stageを作成する (ボーナスステージ,通常のステージを表すクラスはBonusStage, NormalStageで予め定義済みとする)

愚直に書くとこんな感じ.

def next_stage(score)
  if score >= 80
    BonusStage.new
  elsif score >= 70
    BonusStage.new
  elsif score >= 60
    NormalStage.new
  else
    NormalStage.new
  end
end

あるいは,考慮外の数字などを考えたり,どうせ70点以上は全部BonusStageだからなどと考えたらこうなる

def next_stage(score)
  if score >= 70
    BonusStage.new
  elsif score >= 0
    NormalStage.new
  else
    raise "out of range"
  end
end

さらにはcase文をつかったりetc...

分岐の考え方

  • 何のための分岐なのか?
    • 違う意味の分岐を混ぜない

与えられた問題の分解

  • 想定外の値は?
  • 実は仕様はいくつかある
    • ランクを決定すること
    • ランクによって次のステージが変わること

上記の愚直に書いた例の場合ランクを決定することと,ランクによって次のステージが変わることが混ざってしまっている.さらに入力が想定外の範囲だった場合のバリデーションエラーも同一の分岐に混ざってしまっている. どこが主題なのかがわかりづらくなってしまい,コードからランクの情報が欠落してしまっている

# ゲームロジックのための分岐
def grade(score)
  # バリデーションのif(ここはガード節早期リターン or 例外)
  raise 'out of range' unless (0..100).include?
  
  case score
  when 80..100; 'A'
  when 70...80; 'B'
  when 60...70; 'C'
  when  0...60; 'D'
  end
end

# フローのための分岐
def dispatch_stage(grade)
  case grade
  when 'A', 'B'
    BonusStage.new
  when 'C', 'D'
    NormalStage.new
  end
end

def next_stage(score)
  dispatch_stage(grade(score))
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment