オブジェクト指向とは
設計手法であって、文法では無い。最初聞いたときは何かの文法か構文の類か何かだと思っていましたが、そうでは無いんですね。Rubyはオブジェクト指向に沿って書きやすいように構文を揃えてくれている、というのが正しい理解のようです。
どうしてオブジェクト指向で書くのか
- 非常にコードの可読性が向上する
- 見えないバグを可視化しやすい
ことにあるのでは無いかと思っています。今回はここに焦点を置いて、まとめていこうと思います。
まずは、オブジェクト指向を考えないで書いてみる
x_max = ARGV[0]
y_max = ARGV[1]
if !x_max || !y_max
puts "引数を指定してください"
exit 1
end
x_max = x_max.to_i
y_max = y_max.to_i
#状態
x = 1
y = 1
step = 1
x_way = 1
y_way = 1
puts " #{step} (#{x},#{y})"
x += 1
y += 1
loop do
step += 1
puts " #{step} (#{x},#{y})"
if y == 1 && x == x_max || x == 1 && y == y_max || x == 1 && y == 1 || x == x_max && y == y_max
puts "GOAL!!"
break
elsif x == x_max
x_way = -1
elsif y == y_max
y_way = -1
elsif x == 1
x_way = 1
elsif y == 1
y_way = 1
end
x += x_way
y += y_way
end
以前の記事で書いたコードです。関数定義もせず、上から下へ、全ての処理が流れるように処理されるように書いています。これが1番読みにくいコードとなります。変数の定義から何をしているのか予想して実際の処理を追わなければ、機能を読めない仕様になっているからです。このコードが何をしているのかをここで時間をかけて読もうとするのを諦めて次で書き換えたコードを見ていきます。
def move_ball(hash)
hash["x"] += hash["x_way"]
hash["y"] += hash["y_way"]
hash["step"] += 1
end
def reflect_x(hash)
if hash["x_way"] == 1
hash["x_way"] = -1
elsif hash["x_way"] == -1
hash["x_way"] = 1
end
end
def reflect_y(hash)
if hash["y_way"] == 1
hash["y_way"] = -1
elsif hash["y_way"] == -1
hash["y_way"] = 1
end
end
def goal?(hash, x_max, y_max)
hash["y"] == 1 && hash["x"] == x_max \
|| hash["x"] == 1 && hash["y"] == y_max \
|| hash["x"] == 1 && hash["y"] == 1 \
|| hash["x"] == x_max && hash["y"] == y_max
end
def boundary_x?(hash, x_max)
hash["x"] == x_max || hash["x"] == 1
end
def boundary_y?(hash, y_max)
hash["y"] == y_max || hash["y"] == 1
end
x_max = ARGV[0]
y_max = ARGV[1]
if !x_max || !y_max
puts "引数を指定してください"
exit 1
end
x_max = x_max.to_i
y_max = y_max.to_i
state = {
"x" => 1,
"y" => 1,
"step" => 1,
"x_way" => 1,
"y_way" => 1
}
puts " #{state["step"]} (#{state["x"]},#{state["y"]})"
loop do
move_ball(state)
puts " #{state["step"]} (#{state["x"]},#{state["y"]})"
if goal?(state, x_max, y_max)
puts "GOAL!!"
break
elsif boundary_x?(state, x_max)
reflect_x(state)
elsif boundary_y?(state, y_max)
reflect_y(state)
end
end
今度は関数定義とハッシュを利用して書いてみました。こうしてみると、loopの文のところを見て見ると、どうやら、stateというハッシュの中身の状態をmove_ballというメソッドで操作して、条件式の中でboundary_xとboundary_yでボールがバウンドした時にxとy座標をreflect_xとreflect_yで向きを変えているのかな・・・と読めなくは無いものになってきています。
関数定義した中身の処理が正しいとするならば、読む側の人はここだけ読めば、楽に何をやっているのか、このコードは何をするモノなのかが分かるようになります。状態をどういう時に変更しているのかの部分を細かく関数に分断していくとこういうメリットがあります。
また、ハッシュを使えば、多くのグローバル変数を使って表現していた前のコードより、状態の管理を安全にできますし、ボールの数が増えたりしてもまだ多少の融通が効きます。(ボールの数が無茶苦茶多かったり、未知数だった場合、対応できないですが)
が、まだ、読みやすさの追求においてまだ足りていないです。また、ハッシュにもし、タイポといったミスがあったとして、nilが返るだけになるこのコードは、エラーが発見しにくいものになります。大きいプロダクトであればあるほど発見しずらくなります。
ではオブジェクト指向の出番ですね
class Ball
attr_accessor :x, :y, :x_way, :y_way, :step
def initialize(x: 1, y: 1, x_way: 1, y_way: 1, step: 1)
@x = x
@y = y
@x_way = x_way
@y_way = y_way
@step = step
end
def move
@x += @x_way
@y += @y_way
@step += 1
end
def reflect_x
if @x_way == 1
@x_way = -1
elsif @x_way == -1
@x_way = 1
end
end
def reflect_y
if @y_way == 1
@y_way = -1
elsif @y_way == -1
@y_way = 1
end
end
def goal?(x_max, y_max)
@x == x_max && y == 1 \
|| @x == 1 && @y == y_max \
|| @x == 1 && @y == 1 \
|| @x == x_max && @y == y_max
end
def boundary_x?(x_max)
@x == x_max || @x == 1
end
def boundary_y?(y_max)
@y == y_max || @y == 1
end
end
class BilliardTable
attr_accessor :length_x, :length_y, :ball
def initialize(length_x: nil, length_y: nil, ball: nil)
@length_x = length_x
@length_y = length_y
@ball = ball
end
def cue
print_status
loop do
@ball.move
print_status
if @ball.goal?(@length_x, @length_y)
puts "GOAL!!"
break
elsif @ball.boundary_x?(@length_x)
@ball.reflect_x
elsif @ball.boundary_y?(@length_y)
@ball.reflect_y
end
end
end
def print_status
puts "#{@ball.step}, (#{@ball.x}, #{@ball.y})"
end
end
x_max = ARGV[0]
y_max = ARGV[1]
if !x_max || !y_max
puts "引数を指定してください"
exit 1
end
x_max = x_max.to_i
y_max = y_max.to_i
ball = Ball.new()
bt = BilliardTable.new(length_x: x_max, length_y: y_max, ball: ball)
bt.cue
上記のように書くことができるようになります。ボールの状態とビリヤード台の状態をクラスで分け、ボールの機能に必要な状態操作のメソッドを、ballクラスに、ボールを動かした台の上での操作の処理ををbilliardクラスにそれぞれ定義することで、実質、このコードの概ねの所作を知るためというのであれば、billiardクラスの処理系統と、コマンドライン引数を受け取る部分、つまり、クラスの外で書かれているコード部分を読めば、掴むことができます。英語の命名規則もグッと見やすくなっています。
最終的に、bt(ビリヤード台)に対して、cue(突く)を実行するとボールが動くということですね。非常に読みやすいです。また、状態操作をかなり細かく定義しているので、コードの修正のしやすさや、メソッドの呼びやすさといったものも上がっていると思います。これによりバグ修正が先にあげた二つのベタ書きのコードやハッシュで書いたコードより容易になると言えると思います。
まとめ
1番最初に書いたベタ書きの処理から、2段階コードを書き直してみましたが、自分の最初に書いたコードがいかに読みづらく、言ってしまえば、汚いコードであるかが理解できました。
オブジェクト指向によるクラス設計が簡単にできるように作られたフレームワークであるところのrailsに私は1番最初に書いたようなコードの処理をガリガリ書いていたので、ヤベーコードを生み出していたということが更に際立ってよく理解できました。