読者です 読者をやめる 読者になる 読者になる

せかいや

いまいるここを、おもしろく http://sekai-in-the-box.appspot.com/

【プログラミング】リファクタリング。(状態の管理の統一。情報の隠蔽化。インスタンス変数の使いどころ)

 
これでリファクタリング完了!って師匠にメールしたら、返事が来たよ。

 

変数名の選択はよくなったと思いますよ。
でも、まだ十分じゃないと思います。

まず状態の管理が文字列やったり数値やったりで
よくわからないです

@history.length % 2 == 0
ってなってるけど、これどういう意味かわからない。

さらに言えば、000000の前半が人間で後半が非人間っていう情報を
複数箇所で使ってるよね
そんなにその情報に依存するなら、隠蔽化したほうがいいと思うよ

あと、インスタンス変数って必要ですかね
素直に引数に渡すほうがいいような気がしますが。
@patternsってインスタンス変数?定数なんやないすかね

確かに。。
一個ずつみていこう。

 

その1。状態の管理をStringで統一

状態の管理が文字列やったり数値やったりで
よくわからないです

確かに。
でも文字列で持たせないと
"000000" が0になって扱いにくいし、
文字列のままだと「人間が右岸に合計何人いるのか」が計算しづらい。
この計算ね↓

 previous = previous_string.split("").map{|a|a.to_i}
 p1 = previous[0,3].inject(:+)

 
そうか!合計数を求めるのは足し算ではなくて
String#countを使えばいいんだ!

 p1 = previous[0,3].count("1")

こう書けばいいんだ。

■改善前

def dejavu?(state)
  @history.each_with_index do |previous_string, i|
    previous = previous_string.split("").map{|a|a.to_i}
    p1 = previous[0,3].inject(:+)
    p2 = previous[3,3].inject(:+)
    s1 = state[0,3].inject(:+)
    s2 = state[3,3].inject(:+)
    return true if (p1 == s1) && (p2 == s2) && (i%2 == (@history.length)%2)
  end
  false
end

■改善後

def dejavu?(state, history)
  history.each_with_index do |previous, i|
    p1 = previous[0,3].count("1")
    p2 = previous[3,3].count("1")
    s1 = state[0,3].count("1")
    s2 = state[3,3].count("1")
    return true if (p1 == s1) && (p2 == s2) && (i%2 == (history.length)%2)
  end
  false
end

良くなった!

 

その2

@history.length % 2 == 0
ってなってるけど、これどういう意味かわからない。

うーん、、

if history.length%2 == 0 #右から左へ

ってコメント書いてるからなあ。。
メソッドにして切り出すことも出来るけど大げさになりすぎるので、ここはこのままで。


 

その3。情報を隠蔽化

000000の前半が人間で後半が非人間っていう情報を
複数箇所で使ってるよね
そんなにその情報に依存するなら、隠蔽化したほうがいいと思うよ

 
たしかに。
メソッド化しよう。

■改善前

p1 = previous[0,3].inject(:+)
p2 = previous[3,3].inject(:+)
s1 = state[0,3].inject(:+)
s2 = state[3,3].inject(:+)

■改善後

previous_m = missionaries_state(previous).count("1")
previous_c = cannibals_state(previous).count("1")
state_m = missionaries_state(state).count("1")
state_c = cannibals_state(state).count("1")

変数名も、可読性向上のために変更。

 

その4。トップレベルでインスタンス変数は避ける

あと、インスタンス変数って必要ですかね
素直に引数に渡すほうがいいような気がしますが。
@patternsってインスタンス変数?定数なんやないすかね

 
なるほど。。

インスタンス変数にするか否かは、
複数のメソッドから参照するという点で、
インスタンス変数にするほうが見やすくなる(引数が減ることも含めて)と考えたので、インスタンス変数化したのですが。
引数に渡したほうが素直かなあ。。

気持ちは分かりますが、それってつまりはグローバル変数と同じでしょ?
グローバル変数が更新されていくのって、あまり良くはないです。

なるほど。
変数のスコープを安易に広げるのはよくない。
それに、明示的に作ったクラスのインスタンス変数ならわかるけど、トップレベル(Objectクラスの特異クラス)のインスタンス変数、というのはあまり行儀が良くない。

引数が減って綺麗に見えるけど、インスタンス変数の使いどころは考えないと。

修正後コード全量

def cases
  answers = ["000000"] #宣宣宣食食食が左岸
  6.times do |i|
    copy = answers.dup
    copy.each do |state|
      new_state = state.dup
      new_state[i] = "1"
      answers << new_state
    end
  end
  answers
end

def missionaries_state(state)
  state[0,3]
end
def cannibals_state(state)
  state[3,3]
end

#食べられないケースだけ抽出
def sieve(cases)
  answers = []
  cases.each do |caze|
    a = missionaries_state(caze).count("1")
    b = cannibals_state(caze).count("1")
    #全宣教師がどちらかの端に存在 もしくは 左岸&&右岸がOK
    answers << caze if (a==3 || a==0) || ((3-a)>=(3-b) && a>=b)
  end
  answers
end

def possible?(state, next_state, history)
  return false if state == next_state
  total_mover = 0
  if history.length%2 == 0 #右から左へ
    state.split("").each_with_index do |n, i|
      if n == "0"
        return false if next_state[i] != "0" #0->1(右岸に移動)は出来ない
      else
        total_mover += 1 if next_state[i] != "1"
      end
    end
    return false if total_mover > 2 #ボートの定員は2名
  else #左から右へ
    state.split("").each_with_index do |n, i|
      if n == "1"
        return false if next_state[i] != "1" #1->0(左岸に移動)は出来ない
      else
        total_mover += 1 if next_state[i] != "0"
      end
    end
    return false if total_mover > 2
  end
  true
end

def dejavu?(state, history)
  history.each_with_index do |previous, i|
    previous_m = missionaries_state(previous).count("1")
    previous_c = cannibals_state(previous).count("1")
    state_m = missionaries_state(state).count("1")
    state_c = cannibals_state(state).count("1")
    return true if (i%2 == (history.length)%2) && (previous_m == state_m) && (previous_c == state_c)
  end
  false
end

PATTERNS = sieve(cases)
def solve_missionaries_cannibals(state, history)
  history << state
  return true if state == "111111"
    PATTERNS.each do |next_state|
      if possible?(state, next_state, history) && !dejavu?(next_state, history)
        return true if solve_missionaries_cannibals(next_state, history) == true
      end
    end
  history.pop
end

history = []
solve_missionaries_cannibals("000000", history)
p history

良くなった!