[Ruby] How to write code that thinks object-oriented Ruby

5 minute read

What is object-oriented?

It’s a design method, not a grammar. When I first heard it, I thought it was some kind of grammar or syntax, but that’s not the case. It seems that the correct understanding is that Ruby has an object-oriented syntax that makes it easy to write.

Why write object-oriented

–Greatly improves code readability
–Easy to visualize invisible bugs

I think there is something in it. This time, I will focus on this point and summarize it.

First, try writing without thinking about object orientation

toriaezuugoku.rb


x_max = ARGV[0]
y_max = ARGV[1]

if !x_max || !y_max
  puts "Please specify an argument"
  exit 1
end

x_max =  x_max.to_i
y_max =  y_max.to_i

#Status
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

This is the code I wrote in the previous article. It is written so that all the processing flows from top to bottom without defining a function. This is the hardest code to read. This is because the specifications are such that the function cannot be read unless the actual processing is followed by predicting what is being done from the variable definition. Let’s give up trying to read what this code is doing over time here and look at the rewritten code next.

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 "Please specify an argument"
  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

This time I wrote it using the function definition and hash. Looking at the loop statement, it seems that the state of the hash content of state is manipulated by a method called move_ball, and x and y when the ball bounces with boundary_x and boundary_y in the conditional expression. I wonder if the coordinates are changed by reflect_x and reflect_y … It’s becoming something that can’t be read.
If the processing of the contents defined by the function is correct, the reader will be able to easily understand what he is doing and what this code is doing by reading only here. There is such a merit if the part of when the state is changed is divided into functions.
Also, using hashes makes state management safer than the previous code, which was expressed using many global variables, and is still a little more flexible as the number of balls increases. (If the number of balls is unreasonably large or unknown, we can not handle it)
However, it is still not enough in pursuit of readability. Also, this code, which only returns nil if there is a mistake such as a hash or a typo, makes the error hard to find. The larger the product, the harder it is to discover.

So it ’s an object-oriented turn.

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 "Please specify an argument"
	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

You will be able to write as above. Divide the state of the ball and the state of the billiard table into classes, define the state operation method required for the function of the ball in the ball class, and define the processing of the operation on the table that moved the ball in the billiard class. So, in fact, if you want to know the general behavior of this code, you can grasp it by reading the processing system of the billiard class and the part that receives command line arguments, that is, the code part written outside the class. I can. The English naming convention is also much easier to read.
Finally, when you execute cue (poke) against bt (billiard table), the ball moves. Very easy to read. Also, since the state operation is defined in a fairly detailed manner, I think that the ease of modifying the code and the ease of calling the method have also improved. I think this makes it easier to fix bugs than the two solid code and hash code I mentioned earlier.

Summary

I tried to rewrite the two-step code from the solid writing process that I wrote first, but I understood how difficult it is to read the code I wrote first, so to speak, it is dirty code. ..
In rails, which is a framework made to make class design by object orientation easy, I was writing the processing of the code like I wrote at the beginning, so I was creating yabe code. Was even more prominent and well understood.