6.1 オブジェクトとクラス定義

Rubyに存在する値は、すべてオブジェクトである。オブジェクトは、状態(フィールド)振る舞い(メソッド)を持つ。オブジェクトを作るには、オブジェクトの型を定義するためにclass(クラス)と呼ばれる雛形を記述する必要がある。

classの雛形は、以下のような形式をしている。

class クラス名
   def initialize(引数)
     # @フィールド = 引数
   end

   def foo_method
     # @フィールドを使った処理
   end
   ...
end

classの定義は、class + クラス名のキーワードから、endキーワードまでのブロックが範囲である。定義範囲には、0個以上の関数を定義することができる。このclass定義に定義される関数のことがメソッドである。メソッドには、いくつか特殊なメソッドが存在する。そのうちの一つが、initializeメソッド(別名コンストラクタ)と呼ばれるメソッドである。inializeメソッドは、クラス(雛形)からオブジェクト(インスタンス)が生成(インスタンス化)されるときに呼ばれるメソッドで、主にフィールドを初期化するために使われる。また、すべてのメソッドでは、@ (アットマーク)が先頭に付いた変数を共有で使用することが出来る。この変数のことをフィールドと呼ぶ。次に簡単なクラス定義の例を示す。

In [1]:
class Hello
    def initialize(name)
        # [email protected]
        @name = name
    end
    
    def greet()
        # [email protected](フィールド)が、このメソッドでも呼び出すことができる。
        "Hello, #{@name}"
    end
end
Out[1]:
:greet

定義されたクラスは、newメソッドを呼び出すことで、オブジェクトを生成することが出来る。生成されたオブジェクトは、他のオブジェクトと同じように変数に代入したり、代入しないままメソッドを続けざまに呼び出すことも出来る。

In [2]:
hello = Hello.new("OOP")
Out[2]:
#<Hello:0x007f5b31a45810 @name="OOP">
In [3]:
hello.greet
Out[3]:
"Hello, OOP"
In [4]:
Hello.new("hello").greet
Out[4]:
"Hello, hello"

コラム: 全ての値はオブジェクト

チャプター冒頭で、Rubyの存在する全ての値がオブジェクトであると述べた。それらを確かめる為に、一般的な値と演算子がオブジェクトとメソッド呼び出しにより実現出来ることを以下に示す。

In [5]:
1 + 1
Out[5]:
2
In [6]:
1.+(1)
Out[6]:
2

同様のことをpushした値を2倍にするDoubleArrayクラスを自分で定義してみる。このクラスは、値を配列の末尾に格納するpushメソッドの他に、同じ意味を持つ<<メソッドを持つ。これは、整数型の演算子(+)と同様に、pushの意味を持つ演算として定義している。Rubyでは、クラス自体もオブジェクトである。そのため、自身のメソッドを呼び出すには、自身を表す値(オブジェクト)selfを使い、メソッドを呼び出す。但し、普段は省略可能である。Rubyが普段トップレベル関数(クラス定義されていないメソッド)が記述出来ているように見える(手続き型のように見える)のは、トップレベルオブジェクトのメソッドとして定義し、かつ、selfが省略されているからである。メソッドを呼び出す対象のオブジェクト(ドットの左側)のことをレシーバと呼ぶ。

In [7]:
class DoubleArray
    def initialize
        @array = []
    end
    
    def push(val)
        @array.push(val * 2)
    end
    
    def <<(val)
        self.push(val)
        # push(val) でも可 (レシーバの省略)
    end
end
Out[7]:
:<<
In [8]:
dblArr = DoubleArray.new()
Out[8]:
#<DoubleArray:0x007f5b31f57330 @array=[]>
In [9]:
dblArr.push(1)
Out[9]:
[2]
In [10]:
dblArr << 2
Out[10]:
[2, 4]

なぜオブジェクト指向か?

オブジェクト指向を導入する理由は、コードを責任分割し、管理・再利用・拡張が行い易くするためである。具体的にはどういうことだろうか。オブジェクト指向を使用しない例と使用した例を比較してみよう。

人の名前を表したハッシュを定義し、イニシャルを表示する関数initialを定義しよう。以下のように書けるはずだ。

In [11]:
name1 = {first: 'Barack', family: 'Obama'}
Out[11]:
{:first=>"Barack", :family=>"Obama"}
In [12]:
name2 = {first: 'George', middle: 'W', family: 'Bush'}
Out[12]:
{:first=>"George", :middle=>"W", :family=>"Bush"}
In [13]:
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
Out[13]:
:initial
In [14]:
initial(name1)
Out[14]:
"B.O."
In [15]:
initial(name2)
Out[15]:
"G.W.B."

このときに、ミドルネームを判断する条件式が直感的ではないと思い、ミドルネームがあるかどうかを判断する関数middle?に切り出したとする。

In [16]:
def middle?(name)
    name[:middle].nil? == false
end
Out[16]:
:middle?

すると、initialが以下のように再定義出来るはずだ。

In [17]:
def initial(name)
    if(middle?(name))
        "#{name[:first][0]}.#{name[:middle][0]}.#{name[:family][0]}."
    else
        "#{name[:first][0]}.#{name[:family][0]}."
    end
end
Out[17]:
:initial
In [18]:
initial(name1)
Out[18]:
"B.O."
In [19]:
initial(name2)
Out[19]:
"G.W.B."

以上が名前を定義するハッシュと、それに関する関数の組み合わせである。それでは、この場合の問題点はなんだろうか。大きな問題は、このハッシュと関数を使うユーザは、関数がどのように使われるか関数の仕様を知らなければならない点だ。initialは、関数名からイニシャルを返す関数と判断出来るかもしれない。しかしmiddle?は、ミドルネームが存在することを判断する関数か、ベクトルの中間点を返す関数なのか、別な意味を持つ関数なのか判断することが出来ない。Rubyは動的型付けであるため、関数に適切な値が渡されたかどうかは、実行時に判断するしかないため。以下のようなエラーが起こることが想定される。

In [20]:
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という対象となるハッシュの参照回数が増えて煩雑なコードになっていると言う小さな問題もある。

オブジェクト指向は以下のような問題を解決するのに役立つ。先程までの問題をオブジェクト指向を用いて書き直してみる。コンストラクタでは、ファーストネーム、ミドルネーム、ファミリーネームのフィールドを初期化している。ミドルネームはデフォルト引数を用いることで省略することが出来る。initialmiddle?メソッドでは、初期化されたフィールドを参照することで、コードが煩雑にならずに済んでいる。つまり、フィールドやメソッドはクラスNameに閉じ込められていると考えることが出来る。メソッドは、クラスNameに紐付いているため、引数を渡す必要が無く(フィールドだけで、処理が完結する場合に限る)、中身の処理を気にすることなく呼び出すことが可能になる。

In [21]:
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
Out[21]:
:middle?

クラス名が付くことにより、コードに意味が付き見やすくなり、

In [22]:
name1 = Name.new('Barack', 'Obama')
Out[22]:
#<Name:0x007f5b3204c4c0 @first="Barack", @middle=nil, @family="Obama">

メソッドは、オブジェクトと紐付いているため、おかしな値を渡しエラーになることもなくなる。

In [23]:
name1.initial
Out[23]:
"B.O."
In [24]:
name2 = Name.new('George', 'W', 'Bush')
Out[24]:
#<Name:0x007f5b32038088 @first="George", @middle="W", @family="Bush">
In [25]:
name2.initial
Out[25]:
"G.W.B."

オブジェクトは、オブジェクトを持つことが可能で、役割はクラスごとに分割されているため、複雑になりにくい。以下の例では、クラスHumanが、Nameオブジェクトと、Fixnumのageを持っている(HAS-Aの関係)。

In [26]:
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)
Out[26]:
#<Human:0x007f5b320157e0 @name=#<Name:0x007f5b32015858 @first="Barack", @middle=nil, @family="Obama">, @age=55>

このように、Humanは、Nameオブジェクトとage(整数)によってインスタンス化されると、自然に読むことが出来る。

演習

  • to_s というフルネーム(文字列)を返すメソッドを作り、またインスタンス化時に、どのようになるか確認せよ。
  • 以下に示す配列を使い、60歳より大きく、ミドルネームを持つイニシャルの配列を生成せよ。
In [27]:
human1 = Human.new(Name.new('Barack', 'Obama'), 55)
Out[27]:
#<Human:0x007f5b31ff5940 @name=#<Name:0x007f5b31ff59e0 @first="Barack", @middle=nil, @family="Obama">, @age=55>
In [28]:
human2 = Human.new(Name.new('George', 'W', 'Bush'), 70)
Out[28]:
#<Human:0x007f5b31fe4cd0 @name=#<Name:0x007f5b31fe4d20 @first="George", @middle="W", @family="Bush">, @age=70>
In [29]:
human3 = Human.new(Name.new('Donald', 'Trump'), 70)
Out[29]:
#<Human:0x007f5b31fb34f0 @name=#<Name:0x007f5b31fb3540 @first="Donald", @middle=nil, @family="Trump">, @age=70>
In [30]:
[human1, human2, human3]
Out[30]:
[#<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>]

getter, setter, accessor

前節ではオブジェクト指向の利点を示すために、HumanNameクラスの例を挙げた。Humanクラスのいくつかのメソッドは、Nameクラスのメソッドをただ呼び出すだけの無駄な形になってしまう。このような場合は、@nameフィールドを参照するための、いわゆるgetterと呼ばれるメソッドを定義する。getterを定義し、Humanクラスを再定義する。

In [31]:
class Human
    def initialize(name, age)
        @name = name
        @age = age
    end
    
    def name  # @nameフィールドを参照するための、getterメソッド 
        @name
    end
    
    def greater(target)
        @age > target
    end
end
Out[31]:
:greater
In [32]:
human = Human.new(Name.new('Barack', 'Obama'), 55)
Out[32]:
#<Human:0x007f5b31f56340 @name=#<Name:0x007f5b31f56390 @first="Barack", @middle=nil, @family="Obama">, @age=55>
In [33]:
human.name.initial # nameメソッドを参照し、initialメソッドを呼び出す。
Out[33]:
"B.O."

これによりHumanに、Nameのメソッドを再定義する必要が無く、スッキリとした定義を保つことが出来る。

さらにインスタンスの状態を参照するだけでなく、状態が書き換わることを想定しよう。その場合は、状態の書き換えをするためのsetterメソッド定義する必要がある。仮に@ageフィールドが書き換え可能な形でHumanクラスを定義する。

In [34]:
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
Out[34]:
:greater
In [35]:
human = Human.new(Name.new('Barack', 'Obama'), 55)
Out[35]:
#<Human:0x007f5b31ecb268 @name=#<Name:0x007f5b31ecb2b8 @first="Barack", @middle=nil, @family="Obama">, @age=55>
In [36]:
human.age = 56 # 誕生日!
Out[36]:
56
In [37]:
human.greater(55) # 55より上なのでtrueを返す。
Out[37]:
true

getter, setterの定義方法を説明したが、この2つのメソッドは、良く定義されるためRubyのaccessorと呼ばれる機能から自動的に定義することが可能である。accessorは、以下の3つの定義方法が可能である。

  • attr_accessor

フィールドに対して、getterとsetterを自動定義する。

  • attr_reader

フィールドに対して、getterを自動定義する。

  • attr_writer

フィールドに対して、setterを自動定義する。

全てのフィールドに対して、attr_accessorを使用し、getter, setterを定義すれば良いと思うかもしれないが、不用意にgetter, setterを定義してしまうのは危険である。オブジェクト指向では、なるべくインスタンスの状態や不必要なメソッドへのアクセスを避け(隠蔽)、状態を意識すること無く、不用意な値の書き換えによるバグを避ける。このような考え方をカプセル化と呼ぶ。クラス定義では、そのとき必要最低限のaccessorのみを定義することで、不必要必要なアクセスを避けカプセル化を守っている。accessorを用いて、Humanの定義を再定義すると以下のようになる。

In [38]:
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
Out[38]:
:greater
In [39]:
human = Human.new(Name.new('Barack', 'Obama'), 55)
Out[39]:
#<Human:0x007f5b31e73e28 @name=#<Name:0x007f5b31e73e78 @first="Barack", @middle=nil, @family="Obama">, @age=55>
In [40]:
human.name.middle?
Out[40]:
false
In [41]:
human.age = 57
Out[41]:
57
In [42]:
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>'

privateメソッドによるカプセル化

前節では、カプセル化を「インスタンスの状態や不必要なメソッドへのアクセスを避け(隠蔽)、状態を意識すること無く、不用意な値の書き換えによるバグを避ける。」と説明した。accessorを用いて、外部からフィールドを守る事のみをカプセル化と呼ぶには不十分である

要素が増える毎に格納する値が指数関数的に増加する配列クラスExponentialArrayを定義してみる。<<演算子では、メソッドが呼び出されるタイミングで、indexである@countフィールドが増加することと、追加される値valのべき乗を計算するとことと、配列本体(@array)に挿入する3つの操作がある。そのうち最初の2つの操作を抽象的に表すために、countUpメソッドとpowerメソッドとして切り分けている。一見すると、読みやすいコードになり、問題は無いかのように見える。しかし、実際には、countUp操作がpush(<<)操作と分離してしまっているため、オブジェクト外部から呼び出された場合、indexが増加されてしまい想定した結果と配列の値がことなってしまう。また、べき乗の値を計算方法を不必要に外部に公開し、ユーザに混乱を与えてしまうことも想定される。

In [43]:
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
Out[43]:
:power
In [44]:
expArray = ExponentialArray.new
Out[44]:
#<ExponentialArray:0x007f5b31c22840 @array=[], @count=1>
In [45]:
expArray << 2
Out[45]:
[4]
In [46]:
expArray << 2
Out[46]:
[4, 8]

想定していないindexの増加。

In [47]:
expArray.countUp
Out[47]:
4
In [48]:
expArray.countUp
Out[48]:
5

結果、想定した値と実際の値がズレてしまう。

In [49]:
expArray << 2
Out[49]:
[4, 8, 64]

べき乗の計算方法を公開しても、ユーザに混乱を与えるだけとなってしまう。

In [50]:
expArray.power(5)
Out[50]:
15625

そこで、メソッドをprivate(非公開)にすることで、上のような事態を避けることができる。

In [51]:
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
Out[51]:
:_power

privateメソッドには、クラス外部からはアクセス出来ない。

In [52]:
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>'
In [53]:
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クラスにより再定義せよ。

In [54]:
def rgb2hex(rgb)
    rgb.map { |e| sprintf("%02x", e) }.join # sprintf -> "%002x" % e に書き換え可
end
Out[54]:
:rgb2hex
In [55]:
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
Out[55]:
:hex2rgb
In [56]:
rgb2hex([100, 25, 254]) # RGB値(Red Green Blue) を16進数表示(HTMLカラー)へ
Out[56]:
"6419fe"
In [57]:
hex2rgb("6419fe") # HTMLカラーをRGB値へ
Out[57]:
[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]

チェックリスト

  • オブジェクト指向を利用する利点は何か
  • カプセル化とは何か