【Ruby】クラスとモジュール その2

「はじめてのRuby」を読んでのメモ。

前回 【Ruby】クラスとモジュール - seconの日記 の続き。


クラスを拡張する

既存のクラスにメソッドを追加

既に定義されているクラスにメソッドを追加することも出来る。

# String クラスに文字列中の単語数を数えるメソッド count_word を追加

class String
  def count_word
    ary = self.split(/\s+/)    # レシーバを空白文字で分解
    return ary.size            # 分解後の配列の要素数を返す
  end
end

str = "Just Another Ruby Newbie"
p str.count_word               #=> 4


継承

継承により、既存のクラスに変更を加えず新しい機能を追加したり、部分的にカスタマイズして新しいクラスを作ることが出来る。

class RingArray < Array    # スーパークラスを指定
  def [](i)                # 演算子 [] の定義
    idx = i % size         # 新しいインデックスを求める
    super(idx)             # スーパークラスの同名のメソッドを呼ぶ (この場合 Array#[])
  end
end

wday = RingArray["", "", "", "", "", "", ""]
p wday[6]     #=> "土"
p wday[11]    #=> "木"             
p wday[-2]    #=> "金"

RingArray クラスは、配列サイズより大きなインデックスを指定して参照を行うと、はみ出した部分を先頭からさかのぼってインデックスの計算を行う。


スーパークラスを指定せずに定義した場合、Object クラスの直接のサブクラスとなる。
Object クラスはプログラムを作る際に便利なようにたくさんのメソッドを持っているが、余計なメソッドを排除したい場合は最低限のメソッドしか持たない BasicObject クラスを指定するのがよい。

alias と undef

alias

既に存在するメソッドに別の名前を割り当てる場合に使う。
単にメソッドに別名をつけるだけでなく、既に存在するメソッドの定義を変更する場合に、元のメソッドを別名で呼び出せるように保存しておくために使われる。

class C1                   
  def hello                
    "Hello"
  end
end

class C2 < C1            # C1 クラスを継承
  alias old_hello hello  # 別名 old_hello を設定

  def hello              # hello を再定義
    "#{old_hello}, again"
  end
end

obj = C2.new
p obj.old_hello    #=> "Hello"
p obj.hello        #=> "Hello, again"


undef

定義されたメソッドをなかったことにしたい場合に使う。

class C3
  def hi
    "Hi"
  end

  undef hi
end

obj = C3.new
p obj.hi    #=> Error (NoMethodError)


モジュール

モジュールは Ruby の特徴的な機能の1つで、モジュールは処理の部分だけをまとめる機能である。

  • モジュールはインスタンスを持つことが出来ない
  • モジュールは継承できない

この2つがクラスとモジュールの異なる点である。

モジュールの使い方

名前空間の提供

名前空間は、メソッドや定数、クラスの名前を区別して管理する単位のこと。モジュールはそれぞれ独立した名前空間を提供するので、同じ名前のメソッドでもモジュールが異なれば別のものとして扱われる。
size や start などの一般的な名前は既に使わることが多いので、モジュールの内部に名前を定義することで、衝突を防ぐことが出来る。

Mix-in による機能の提供

モジュールをクラスに混ぜ合わせることを Mix-in という。クラスの中で include を使うと、モジュールに含まれるメソッドや定数をクラスの中に取り込むことが出来る。
クラスの継承に似ているが、

  • 2つのクラスは似たような機能を持っているだけで同じ種類(クラス)と考えたくない
  • Ruby の継承は複数のスーパークラスを持てない仕様になっているため、すでに継承を行っていると上手く共通機能を追加できない

といったケースに Mix-in を使って柔軟に対応することが出来る。

モジュールを作る

module HelloModule          # モジュールの定義
  Version = "1.0"           # 定数の定義

  def hello(name)
    puts "Hello, #{name}."
  end

  module_function :hello    # hello をモジュール関数として公開
end

p HelloModule::Version      #=> "1.0"
HelloModule.hello("Alice")  #=> Hello,Alice

include HelloModule         # モジュールの持つメソッドや定数名を現在の名前空間に取り込む
p Version                   #=> "1.0"
hello ("Alice")             #=> Hello, Alice


メソッドの定義

モジュール内で定義したメソッドを使うためには、メソッドをモジュール関数として公開する必要がある。

def hello(name)
  puts "Hello, #{name}."
end

module_function :hello

また、メソッド内で self (レシーバ)を参照すると、そのモジュールを得られる。

module FooModule
  def foo
    p self
  end
  module_function :foo
end

FooModule.foo  #=> FooModule


Mix-in

# モジュールをクラスに include

module M
  def meth
    "meth"
  end
end

class C
  include M 
end

c = C.new
p c.meth    #=> meth
# include されているかの判定

p C.include?(M)  #=> true

クラスCのインスタンスに対してメソッド呼び出しを行うと、クラスC、モジュールM、Object クラス(クラスCのスーパークラス)の順にメソッドを検索し、最初に見つかったものを実行する。

# 継承関係を調べる

p C.ancestors   #=> [C, M, Object, HelloModule, Kernel, BasicObject]
p C.superclass  #=> Object


メソッド検索のルール
  • 継承の関係と同じように、元のクラスで同じ名前のメソッドが定義されている場合はそちらが優先される。
module M
  def meth
    "M#meth"
  end
end

class C
  include M  # M をインストール
  def meth
    "C#meth"
  end
end

c = C.new
p c.meth     #=> C#meth
  • 同じクラスに複数のモジュールを include した場合は、後から include したものが優先される。
module M1

end

module M2

end

class C
  include M1
  include M2
end

p C.ancestors    #=> [C, M2, M1, Object, HelloModule, Kernel, BasicObject]
  • include が入れ子になった場合も検索順は一列に並ぶ。
module M1

end

module M2

end

module M3
  include M2
end

class C
  include M1
  include M3
end

p C.ancestors    #=> [C, M3, M2, M1, Object, HelloModule, Kernel, BasicObject]
  • 同じモジュールを2回以上 include した場合、2回目以降は無視される。
module M1

end

module M2

end

class C
  include M1
  include M2
  include M1
end

p C.ancestors    #=> [C, M2, M1, Object, HelloModule, Kernel, BasicObject]

extend メソッド

モジュールを特異クラスに include し、オブジェクトにモジュールの機能を追加する。
extend メソッドでは、クラスを超えてオブジェクト単位にモジュールの機能を利用できるようになる。

module Edition
  def edition(n)
    "#{self} 第{n}版"
  end
end

str = "たのしいRuby"
str.extend(Edition)    #=> モジュールをオブジェクトに Mix-in する

p str.edition(4)       #=> "たのしいRuby 第4版"


クラスと Mix-in

Ruby のクラスはそれ自体が Class クラスのオブジェクトとして提供されている。また、クラスメソッドはクラスをレシーバとするメソッドである。つまり、クラスメソッドはクラスオブジェクトに対するインスタンスメソッドである。そのようなメソッドは次の2つである。

  • Class クラスのインスタンスメソッド
  • クラスオブジェクトの特異メソッド

クラスを継承すると、これらのメソッドはサブクラスにもクラスメソッドとして引き継がれる。

# extend メソッドに酔ってクラスメソッドを追加し、
# include メソッドによってインスタンスメソッドを追加する

module ClassMethods      # クラスメソッドのためのモジュール
  def cmethod
    "class method"
  end
end

module InstanceMethods   # インスタンスメソッドのためのモジュール
  def imethod
    "instance method"
  end
end


class MyClass
  # extend するとクラスメソッドを追加できる
  extend ClassMethods
  # include するとインスタンスメソッドを追加できる
  include InstanceMethods
end

p MyClass.cmethod        #=> "class method"
p MyClass.new.imethod    #=> "instance method"


オブジェクト指向プログラミングの特徴

カプセル化

オブジェクトが管理するデータをオブジェクトの外部から直には操作できないようにし、変更したり参照したりするときは必ずメソッドを呼び出させるようにすること。
カプセル化のメリットは主に2つあり、1つは不整合なデータをオブジェクトに設定してプログラムの挙動がおかしくなるといったことを防げるようになることである。Ruby ではもともとカプセル化が強制されていて(オブジェクト外部からインスタンス変数に直にアクセス出来ない)、 attr_accessor などのアクセスメソッドはむやみに使用せず必要な物だけを公開するべきである。
もう1つは、具体的なデータや処理をオブジェクトの内部に隠蔽して抽象的に表現できることだ。オブジェクトの内部で保持する具体的なデーが構造が変更されても、外部から見えるメソッドの名前や機能に変化がなければクラスの利用者は内部の変化を気にせず使うことが出来る。クラスを作成する側も、適切なメソッドを用意しておけばクラスの利用側のことを気にせず内部を変更できる。

ポリモルフィズム

オブジェクト指向の用語で、1つのメソッド名が複数のオブジェクトに属することをいう。多相性、多様性ともいう。

# to_s メソッドの例

obj = Object.new    # Object
str = "Ruby"        # String
num = Math::PI      # Float

p obj.to_s    #=> "#<Object:0x00000002f4ad38>"
p str.to_s    #=> "Ruby"
p num.to_s    #=> "3.141592653589793"

いずれも to_s メソッドを使っているが、実際の文字列を作る手順はオブジェクトが表現するデータによって異なっている。String クラスも Float クラスも Object クラスから派生しているが、 Object クラスから継承した to_s メソッドを定義しなおして、よりふさわしい文字列を返すバージョンの to_s メソッドを提供している。

ダックタイピング

ポリモルフィズムを積極的に活用した考え方。

オブジェクトを特徴づけるのは実際の種類(クラスとその継承関係)ではなく、そのオブジェクトがどのように振る舞うか(どんなメソッドを持っているか)である。

「アヒルのように歩きアヒルのように鳴くものはアヒルに違いない」という格言から来ている。

# 文字列を含む配列から文字列を取り出して、
#その要素に含まれるアルファベットを小文字にして返す

def fetch_and_downcase(ary, index)
  if str = ary[index]
    return str.downcase
  end
end

ary = ["Boo", "Foo", "Woo"]
p fetch_and_downcase(ary,1)    #=> "foo"

この fetch_and_downcase メソッドが、引数として渡されるオブジェクトに期待しているのは、

  • ary[index]という形式で要素が取り出せる
  • 取り出した要素が downcase メソッドを持っていること

である。この条件を満たしていればよいので、配列ではなくハッシュを渡して使うことも出来る。

hash = {0 => "Boo", 1 => "Foo", 2 => "Woo"}
p fetch_and_downcase(hash,1)    #=> "foo"

「同じ操作を行えるならば実際は違うものであってもその違いを気にしない」、「実際は違うものであっても同じ名前のメソッドを用意することで処理を共通化できる」というのがダッチタイピングの考え方である。


モジュールとクラスについては、プログラミングに慣れてからもう一度復習したほうが良さそう。



1/16 追記
ポリモルフィズム」が「ポリモルフィルム」と書かれていたのを修正。
ポリモーフィズムとも呼ばれる。