Rubyに存在する値は、すべてオブジェクトである。オブジェクトは、状態(フィールド)と振る舞い(メソッド)を持つ。オブジェクトを作るには、オブジェクトの型を定義するためにclass(クラス)と呼ばれる雛形を記述する必要がある。
classの雛形は、以下のような形式をしている。
class クラス名
def initialize(引数)
# @フィールド = 引数
end
def foo_method
# @フィールドを使った処理
end
...
end
classの定義は、class + クラス名のキーワードから、endキーワードまでのブロックが範囲である。定義範囲には、0個以上の関数を定義することができる。このclass定義に定義される関数のことがメソッドである。メソッドには、いくつか特殊なメソッドが存在する。そのうちの一つが、initializeメソッド(別名コンストラクタ)と呼ばれるメソッドである。inializeメソッドは、クラス(雛形)からオブジェクト(インスタンス)が生成(インスタンス化)されるときに呼ばれるメソッドで、主にフィールドを初期化するために使われる。また、すべてのメソッドでは、@ (アットマーク)が先頭に付いた変数を共有で使用することが出来る。この変数のことをフィールドと呼ぶ。次に簡単なクラス定義の例を示す。
class Hello
def initialize(name)
# 同名であっても、@nameとnameは別物。
@name = name
end
def greet()
# コンストラクタで初期化された@name(フィールド)が、このメソッドでも呼び出すことができる。
"Hello, #{@name}"
end
end
:greet
定義されたクラスは、newメソッドを呼び出すことで、オブジェクトを生成することが出来る。生成されたオブジェクトは、他のオブジェクトと同じように変数に代入したり、代入しないままメソッドを続けざまに呼び出すことも出来る。
hello = Hello.new("OOP")
#<Hello:0x007f5b31a45810 @name="OOP">
hello.greet
"Hello, OOP"
Hello.new("hello").greet
"Hello, hello"
チャプター冒頭で、Rubyの存在する全ての値がオブジェクトであると述べた。それらを確かめる為に、一般的な値と演算子がオブジェクトとメソッド呼び出しにより実現出来ることを以下に示す。
1 + 1
2
1.+(1)
2
同様のことをpushした値を2倍にするDoubleArrayクラスを自分で定義してみる。このクラスは、値を配列の末尾に格納するpushメソッドの他に、同じ意味を持つ<<メソッドを持つ。これは、整数型の演算子(+)と同様に、pushの意味を持つ演算として定義している。Rubyでは、クラス自体もオブジェクトである。そのため、自身のメソッドを呼び出すには、自身を表す値(オブジェクト)selfを使い、メソッドを呼び出す。但し、普段は省略可能である。Rubyが普段トップレベル関数(クラス定義されていないメソッド)が記述出来ているように見える(手続き型のように見える)のは、トップレベルオブジェクトのメソッドとして定義し、かつ、selfが省略されているからである。メソッドを呼び出す対象のオブジェクト(ドットの左側)のことをレシーバと呼ぶ。
class DoubleArray
def initialize
@array = []
end
def push(val)
@array.push(val * 2)
end
def <<(val)
self.push(val)
# push(val) でも可 (レシーバの省略)
end
end
:<<
dblArr = DoubleArray.new()
#<DoubleArray:0x007f5b31f57330 @array=[]>
dblArr.push(1)
[2]
dblArr << 2
[2, 4]
オブジェクト指向を導入する理由は、コードを責任分割し、管理・再利用・拡張が行い易くするためである。具体的にはどういうことだろうか。オブジェクト指向を使用しない例と使用した例を比較してみよう。
人の名前を表したハッシュを定義し、イニシャルを表示する関数initialを定義しよう。以下のように書けるはずだ。
name1 = {first: 'Barack', family: 'Obama'}
{:first=>"Barack", :family=>"Obama"}
name2 = {first: 'George', middle: 'W', family: 'Bush'}
{:first=>"George", :middle=>"W", :family=>"Bush"}
def initial(name)
if(name[:middle].nil?)
"#{name[:first][0]}.#{name[:family][0]}."
else
"#{name[:first][0]}.#{name[:middle][0]}.#{name[:family][0]}."
end
end
:initial
initial(name1)
"B.O."
initial(name2)
"G.W.B."
このときに、ミドルネームを判断する条件式が直感的ではないと思い、ミドルネームがあるかどうかを判断する関数middle?に切り出したとする。
def middle?(name)
name[:middle].nil? == false
end
:middle?
すると、initialが以下のように再定義出来るはずだ。
def initial(name)
if(middle?(name))
"#{name[:first][0]}.#{name[:middle][0]}.#{name[:family][0]}."
else
"#{name[:first][0]}.#{name[:family][0]}."
end
end
:initial
initial(name1)
"B.O."
initial(name2)
"G.W.B."
以上が名前を定義するハッシュと、それに関する関数の組み合わせである。それでは、この場合の問題点はなんだろうか。大きな問題は、このハッシュと関数を使うユーザは、関数がどのように使われるか関数の仕様を知らなければならない点だ。initialは、関数名からイニシャルを返す関数と判断出来るかもしれない。しかしmiddle?は、ミドルネームが存在することを判断する関数か、ベクトルの中間点を返す関数なのか、別な意味を持つ関数なのか判断することが出来ない。Rubyは動的型付けであるため、関数に適切な値が渡されたかどうかは、実行時に判断するしかないため。以下のようなエラーが起こることが想定される。
initial({hoge: "foo", bar: "test"})
NoMethodError: undefined method `[]' for nil:NilClass <main>:4:in `initial' <main>:in `<main>' /usr/local/bundle/gems/iruby-0.2.1/lib/iruby/backend.rb:8:in `eval' /usr/local/bundle/gems/iruby-0.2.1/lib/iruby/backend.rb:8:in `eval' /usr/local/bundle/gems/iruby-0.2.1/lib/iruby/kernel.rb:104:in `execute_request' /usr/local/bundle/gems/iruby-0.2.1/lib/iruby/kernel.rb:64:in `run' /usr/local/bundle/gems/iruby-0.2.1/lib/iruby/command.rb:76:in `run_kernel' /usr/local/bundle/gems/iruby-0.2.1/lib/iruby/command.rb:33:in `run' /usr/local/bundle/gems/iruby-0.2.1/bin/iruby:5:in `<top (required)>' /usr/local/bundle/bin/iruby:23:in `load' /usr/local/bundle/bin/iruby:23:in `<main>'
さらにこのエラーが恐ろしいところは、エラー内容から何が間違いであったか想定できないことだ。この場合、この関数を仕様するユーザは、関数の中身を見なければならない。他にも、このハッシュが年齢、身長、体重などの要素が付き、それに関する関数が増えた時に、この問題の複雑度を爆発的に増すことになるだろう。また、nameという対象となるハッシュの参照回数が増えて煩雑なコードになっていると言う小さな問題もある。
オブジェクト指向は以下のような問題を解決するのに役立つ。先程までの問題をオブジェクト指向を用いて書き直してみる。コンストラクタでは、ファーストネーム、ミドルネーム、ファミリーネームのフィールドを初期化している。ミドルネームはデフォルト引数を用いることで省略することが出来る。initialやmiddle?メソッドでは、初期化されたフィールドを参照することで、コードが煩雑にならずに済んでいる。つまり、フィールドやメソッドはクラスNameに閉じ込められていると考えることが出来る。メソッドは、クラスNameに紐付いているため、引数を渡す必要が無く(フィールドだけで、処理が完結する場合に限る)、中身の処理を気にすることなく呼び出すことが可能になる。
class Name
def initialize(first, middle=nil, family)
@first = first
@middle = middle
@family = family
end
def initial()
if(middle?)
"#{@first[0]}.#{@middle[0]}.#{@family[0]}."
else
"#{@first[0]}.#{@family[0]}."
end
end
def middle?()
@middle.nil? == false
end
end
:middle?
クラス名が付くことにより、コードに意味が付き見やすくなり、
name1 = Name.new('Barack', 'Obama')
#<Name:0x007f5b3204c4c0 @first="Barack", @middle=nil, @family="Obama">
メソッドは、オブジェクトと紐付いているため、おかしな値を渡しエラーになることもなくなる。
name1.initial
"B.O."
name2 = Name.new('George', 'W', 'Bush')
#<Name:0x007f5b32038088 @first="George", @middle="W", @family="Bush">
name2.initial
"G.W.B."
オブジェクトは、オブジェクトを持つことが可能で、役割はクラスごとに分割されているため、複雑になりにくい。以下の例では、クラスHumanが、Nameオブジェクトと、Fixnumのageを持っている(HAS-Aの関係)。
class Human
def initialize(name, age)
@name = name
@age = age
end
def initial
@name.initial
end
def middle?
@name.middle?
end
def greater(target)
@age > target
end
end
Human.new(Name.new('Barack', 'Obama'), 55)
#<Human:0x007f5b320157e0 @name=#<Name:0x007f5b32015858 @first="Barack", @middle=nil, @family="Obama">, @age=55>
このように、Humanは、Nameオブジェクトとage(整数)によってインスタンス化されると、自然に読むことが出来る。
human1 = Human.new(Name.new('Barack', 'Obama'), 55)
#<Human:0x007f5b31ff5940 @name=#<Name:0x007f5b31ff59e0 @first="Barack", @middle=nil, @family="Obama">, @age=55>
human2 = Human.new(Name.new('George', 'W', 'Bush'), 70)
#<Human:0x007f5b31fe4cd0 @name=#<Name:0x007f5b31fe4d20 @first="George", @middle="W", @family="Bush">, @age=70>
human3 = Human.new(Name.new('Donald', 'Trump'), 70)
#<Human:0x007f5b31fb34f0 @name=#<Name:0x007f5b31fb3540 @first="Donald", @middle=nil, @family="Trump">, @age=70>
[human1, human2, human3]
[#<Human:0x007f5b31ff5940 @name=#<Name:0x007f5b31ff59e0 @first="Barack", @middle=nil, @family="Obama">, @age=55>, #<Human:0x007f5b31fe4cd0 @name=#<Name:0x007f5b31fe4d20 @first="George", @middle="W", @family="Bush">, @age=70>, #<Human:0x007f5b31fb34f0 @name=#<Name:0x007f5b31fb3540 @first="Donald", @middle=nil, @family="Trump">, @age=70>]
前節ではオブジェクト指向の利点を示すために、HumanとNameクラスの例を挙げた。Humanクラスのいくつかのメソッドは、Nameクラスのメソッドをただ呼び出すだけの無駄な形になってしまう。このような場合は、@nameフィールドを参照するための、いわゆるgetterと呼ばれるメソッドを定義する。getterを定義し、Humanクラスを再定義する。
class Human
def initialize(name, age)
@name = name
@age = age
end
def name # @nameフィールドを参照するための、getterメソッド
@name
end
def greater(target)
@age > target
end
end
:greater
human = Human.new(Name.new('Barack', 'Obama'), 55)
#<Human:0x007f5b31f56340 @name=#<Name:0x007f5b31f56390 @first="Barack", @middle=nil, @family="Obama">, @age=55>
human.name.initial # nameメソッドを参照し、initialメソッドを呼び出す。
"B.O."
これによりHumanに、Nameのメソッドを再定義する必要が無く、スッキリとした定義を保つことが出来る。
さらにインスタンスの状態を参照するだけでなく、状態が書き換わることを想定しよう。その場合は、状態の書き換えをするためのsetterメソッド定義する必要がある。仮に@ageフィールドが書き換え可能な形でHumanクラスを定義する。
class Human
def initialize(name, age)
@name = name
@age = age
end
def name # @nameフィールドを参照するための、getterメソッド
@name
end
def age=(a) # @ageフィールドを書き換えるための、setterメソッド。
@age = a
end
def greater(target)
@age > target
end
end
:greater
human = Human.new(Name.new('Barack', 'Obama'), 55)
#<Human:0x007f5b31ecb268 @name=#<Name:0x007f5b31ecb2b8 @first="Barack", @middle=nil, @family="Obama">, @age=55>
human.age = 56 # 誕生日!
56
human.greater(55) # 55より上なのでtrueを返す。
true
getter, setterの定義方法を説明したが、この2つのメソッドは、良く定義されるためRubyのaccessorと呼ばれる機能から自動的に定義することが可能である。accessorは、以下の3つの定義方法が可能である。
フィールドに対して、getterとsetterを自動定義する。
フィールドに対して、getterを自動定義する。
フィールドに対して、setterを自動定義する。
全てのフィールドに対して、attr_accessorを使用し、getter, setterを定義すれば良いと思うかもしれないが、不用意にgetter, setterを定義してしまうのは危険である。オブジェクト指向では、なるべくインスタンスの状態や不必要なメソッドへのアクセスを避け(隠蔽)、状態を意識すること無く、不用意な値の書き換えによるバグを避ける。このような考え方をカプセル化と呼ぶ。クラス定義では、そのとき必要最低限のaccessorのみを定義することで、不必要必要なアクセスを避けカプセル化を守っている。accessorを用いて、Humanの定義を再定義すると以下のようになる。
class Human
attr_reader :name # フィールド名のシンボルで指定する。 @name getter
attr_writer :age # @age setter
def initialize(name, age)
@name = name
@age = age
end
def greater(target)
@age > target
end
end
:greater
human = Human.new(Name.new('Barack', 'Obama'), 55)
#<Human:0x007f5b31e73e28 @name=#<Name:0x007f5b31e73e78 @first="Barack", @middle=nil, @family="Obama">, @age=55>
human.name.middle?
false
human.age = 57
57
human.age # ageは、attr_writer なので、getterは用意されない。
NoMethodError: undefined method `age' for #<Human:0x007f5b31e73e28> <main>:in `<main>' /usr/local/bundle/gems/iruby-0.2.1/lib/iruby/backend.rb:8:in `eval' /usr/local/bundle/gems/iruby-0.2.1/lib/iruby/backend.rb:8:in `eval' /usr/local/bundle/gems/iruby-0.2.1/lib/iruby/kernel.rb:104:in `execute_request' /usr/local/bundle/gems/iruby-0.2.1/lib/iruby/kernel.rb:64:in `run' /usr/local/bundle/gems/iruby-0.2.1/lib/iruby/command.rb:76:in `run_kernel' /usr/local/bundle/gems/iruby-0.2.1/lib/iruby/command.rb:33:in `run' /usr/local/bundle/gems/iruby-0.2.1/bin/iruby:5:in `<top (required)>' /usr/local/bundle/bin/iruby:23:in `load' /usr/local/bundle/bin/iruby:23:in `<main>'
前節では、カプセル化を「インスタンスの状態や不必要なメソッドへのアクセスを避け(隠蔽)、状態を意識すること無く、不用意な値の書き換えによるバグを避ける。」と説明した。accessorを用いて、外部からフィールドを守る事のみをカプセル化と呼ぶには不十分である。
要素が増える毎に格納する値が指数関数的に増加する配列クラスExponentialArrayを定義してみる。<<演算子では、メソッドが呼び出されるタイミングで、indexである@countフィールドが増加することと、追加される値valのべき乗を計算するとことと、配列本体(@array)に挿入する3つの操作がある。そのうち最初の2つの操作を抽象的に表すために、countUpメソッドとpowerメソッドとして切り分けている。一見すると、読みやすいコードになり、問題は無いかのように見える。しかし、実際には、countUp操作がpush(<<)操作と分離してしまっているため、オブジェクト外部から呼び出された場合、indexが増加されてしまい想定した結果と配列の値がことなってしまう。また、べき乗の値を計算方法を不必要に外部に公開し、ユーザに混乱を与えてしまうことも想定される。
class ExponentialArray
def initialize
@array = []
@count = 1
end
def <<(val)
countUp
@array.push(power(val))
end
def countUp
@count += 1
end
def power(val)
val ** @count
end
end
:power
expArray = ExponentialArray.new
#<ExponentialArray:0x007f5b31c22840 @array=[], @count=1>
expArray << 2
[4]
expArray << 2
[4, 8]
想定していないindexの増加。
expArray.countUp
4
expArray.countUp
5
結果、想定した値と実際の値がズレてしまう。
expArray << 2
[4, 8, 64]
べき乗の計算方法を公開しても、ユーザに混乱を与えるだけとなってしまう。
expArray.power(5)
15625
そこで、メソッドをprivate(非公開)にすることで、上のような事態を避けることができる。
class ExponentialArray
def initialize
@array = []
@count = 1
end
def <<(val)
_countUp
@array.push(_power(val))
end
# 以下は、privateメソッド
private
def _countUp
@count += 1
end
def _power(val)
val ** @count
end
# private :_countUp, :_power でも可。
end
:_power
privateメソッドには、クラス外部からはアクセス出来ない。
ExponentialArray.new._countUp
NoMethodError: private method `_countUp' called for #<ExponentialArray:0x007f5b32071798 @array=[], @count=1> <main>:in `<main>' /usr/local/bundle/gems/iruby-0.2.1/lib/iruby/backend.rb:8:in `eval' /usr/local/bundle/gems/iruby-0.2.1/lib/iruby/backend.rb:8:in `eval' /usr/local/bundle/gems/iruby-0.2.1/lib/iruby/kernel.rb:104:in `execute_request' /usr/local/bundle/gems/iruby-0.2.1/lib/iruby/kernel.rb:64:in `run' /usr/local/bundle/gems/iruby-0.2.1/lib/iruby/command.rb:76:in `run_kernel' /usr/local/bundle/gems/iruby-0.2.1/lib/iruby/command.rb:33:in `run' /usr/local/bundle/gems/iruby-0.2.1/bin/iruby:5:in `<top (required)>' /usr/local/bundle/bin/iruby:23:in `load' /usr/local/bundle/bin/iruby:23:in `<main>'
ExponentialArray.new._power
NoMethodError: private method `_power' called for #<ExponentialArray:0x007f5b32068eb8 @array=[], @count=1> <main>:in `<main>' /usr/local/bundle/gems/iruby-0.2.1/lib/iruby/backend.rb:8:in `eval' /usr/local/bundle/gems/iruby-0.2.1/lib/iruby/backend.rb:8:in `eval' /usr/local/bundle/gems/iruby-0.2.1/lib/iruby/kernel.rb:104:in `execute_request' /usr/local/bundle/gems/iruby-0.2.1/lib/iruby/kernel.rb:64:in `run' /usr/local/bundle/gems/iruby-0.2.1/lib/iruby/command.rb:76:in `run_kernel' /usr/local/bundle/gems/iruby-0.2.1/lib/iruby/command.rb:33:in `run' /usr/local/bundle/gems/iruby-0.2.1/bin/iruby:5:in `<top (required)>' /usr/local/bundle/bin/iruby:23:in `load' /usr/local/bundle/bin/iruby:23:in `<main>'
オブジェクト指向は、ある仕組みをオブジェクトに集約し、名前を付け意味を与え、ユーザにとって有益なインターフェースのみを提供するプログラミング手法だと言える。
以下に示す関数をColorクラスにより再定義せよ。
def rgb2hex(rgb)
rgb.map { |e| sprintf("%02x", e) }.join # sprintf -> "%002x" % e に書き換え可
end
:rgb2hex
def hex2rgb(hex)
r, g, b = hex[0..1], hex[2..3], hex[4..5]
[r, g, b].map { |e| e.to_i(16) }
end
:hex2rgb
rgb2hex([100, 25, 254]) # RGB値(Red Green Blue) を16進数表示(HTMLカラー)へ
"6419fe"
hex2rgb("6419fe") # HTMLカラーをRGB値へ
[100, 25, 254]
color = Color.new(100, 25, 254) # RGB値をコンストラクタに渡す。
puts color
=> 6419fe#(100, 25, 254)
p color.rgb
=> [100, 25, 254]
p color.hex
=> "6419fe"
また、Colorインスタンスとid属性を持つHTML要素を表すクラスHeaderを定義せよ。
header = Header.new("header", Color.new(100, 25, 254), "id1")
p header.html
=> "<h1>header</h1>"
p header.style
=> "header#id1 {color: #6419fe;}"
p header.color.rgb
[100, 25, 254]