LoginSignup
0
2

More than 3 years have passed since last update.

オブジェクト指向で考えるコードの書き方 Ruby

Last updated at Posted at 2020-08-03

オブジェクト指向とは

 設計手法であって、文法では無い。最初聞いたときは何かの文法か構文の類か何かだと思っていましたが、そうでは無いんですね。Rubyはオブジェクト指向に沿って書きやすいように構文を揃えてくれている、というのが正しい理解のようです。

どうしてオブジェクト指向で書くのか

  • 非常にコードの可読性が向上する
  • 見えないバグを可視化しやすい

ことにあるのでは無いかと思っています。今回はここに焦点を置いて、まとめていこうと思います。

まずは、オブジェクト指向を考えないで書いてみる

toriaezuugoku.rb
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番読みにくいコードとなります。変数の定義から何をしているのか予想して実際の処理を追わなければ、機能を読めない仕様になっているからです。このコードが何をしているのかをここで時間をかけて読もうとするのを諦めて次で書き換えたコードを見ていきます。

methodtohash.rb
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が返るだけになるこのコードは、エラーが発見しにくいものになります。大きいプロダクトであればあるほど発見しずらくなります。

ではオブジェクト指向の出番ですね

object.rb
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番最初に書いたようなコードの処理をガリガリ書いていたので、ヤベーコードを生み出していたということが更に際立ってよく理解できました。

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2