継承について
オブジェクト指向に存在する継承という能力があるそうです。クラス定義した中身を書かずして利用したり、中で定義されているものをそのクラスで使いやすいように改造して使えるようになったりします。
メリット
- 機能拡張、いわゆる、プラガブルな仕組みを作ることができる能力
デメリット
- 継承させすぎによる保守不全(大元を変更した瞬間、継承先で不具合が多発するなど・・・)
実際に使ってみる
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
class MyBilliardTable < BilliardTable
def print_status
puts "step = #{@ball.step}, x = #{@ball.x}, y = #{@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
(6..16).each do |times|
puts "3 x #{times}のビリヤード台で、実行します"
bt = MyBilliardTable.new(length_x: 3, length_y: times, ball: Ball.new)
bt.cue
end
また引き続き、以前でも書いたコードを使って継承をさせてみます。実際このコードの中で継承させている部分は、
class MyBilliardTable < BilliardTable
def print_status
puts "step = #{@ball.step}, x = #{@ball.x}, y = #{@ball.y}"
end
end
ここの部分です。不等号を使って親クラスを指定することで、クラス継承をすることができます。今回は出力する部分のメソッドを変更しています。
そして、この定義したクラスを使ってオブジェクトを生成してみます。
(6..16).each do |times|
puts "3 x #{times}のビリヤード台で、実行します"
bt = MyBilliardTable.new(length_x: 3, length_y: times, ball: Ball.new)
bt.cue
end
処理部分は上記の部分になります。出力が変更された形になっているはずですし、また、MyBilliardTableクラスでは定義していませんが、BilliardTabelクラスで定義しているメソッドはちゃんと機能していることが理解できると思います。
余談
余談ですが、オブジェクト指向でクラス設計することによって、BilliardTableをオブジェクトにできることによって、盤面にパターンを持たせることができるようになりました。今回は3かけxの盤面を表現しています。
puts "3 x #{times}のビリヤード台で、実行します"
以前の記事で書いたベタ書きのコードもやろうと思えばできますが、それは二重のループを作ることになりますし、ループさせる分、コードが一回り大きくなります、つまり、コードの階層が深まります。結果として、読みづらく、修正しにくいです。想像するだけで、改修しにくい、読みにくいコードになると思います。
こういうような条件が増えた時の表現のしやすさ、書きやすさもやはりオブジェクト指向の利点と言えると思います。
yieldについて
規模の小さいものであれば、ブロック単位でことが済ませられるyieldを使って機能拡張をすることも可能です。
yieldは「取って変わる」の意味の通り、statusメソッドの部分を取って代わるようにしています。
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 cue_2
if block_given?
puts "blockがあります"
else
puts "blockを指定してください"
return
end
loop do
@ball.move
yield 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 status
{
"step" => @ball.step,
"x" => @ball.x,
"y" => @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
bt.cue_2 do |status|
puts "ステップ数は#{status["step"]}でx座標は#{status["x"]}、y座標は#{status["y"]}です"
end
拡張したい機能の規模の大小で、継承を用いるか、yieldを使ってブロック渡しをするかを見極めると良いと思われます。