- 分岐は少なければ少ないほうがよい
- とはいえ絶対に必要なのでどうすればよいか?
- ネストを減らす
- 分岐を仮に愚直にやっていくとn乗オーダーで処理が分かれていき,とてもじゃないが読めない
- 統一感,対称性を持たせる
- 適切なメソッド化をする
- 分岐は処理の流れが変わるという重要な役割をもつため.分岐をするのであれば分岐をするという役割でメソッドを1つ切る(dispatcherが典型)
- 深いネストやスパゲティを防止できる
- ネストを減らす
- 特定の場合のみ追加的な処理をしたい場合
if File.exist?(path)
File.delete(path)
end
#...
- 特定の__例外的な__条件を事前に除きたい場合(いわゆるガード節)
class Node
def parent
return nil if self.root?
# ルートノードのめんどくさいことは気にせず純粋に親ノードをとる処理に集中できる
end
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 ... 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どっちを使うのか?
- 基本的にはシンプルな方
# 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
- かなり柔軟に機能をもつので詳細は 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