晴耕雨読

working in the fields on fine days and reading books on rainy days

OOP in Ruby

Rubyにおけるオブジェクト指向のまとめ

目次

クラスの定義

  • class というキーワードでクラスを宣言
  • クラス名.new でインスタンスの生成

Ruby

class Point
end

p = Point.new

コンストラクタ、フィールド、メソッド

  • コンストラクタは initialize メソッドに定義する
  • インスタンス変数の名前は @ から始まる
  • インスタンス変数はデフォルトで private
  • メソッドはデフォルトで public

Pointクラスの例

class Point
  # コンストラクタ(イニシャライザ)
  def initialize(x, y)
    @x, @y = x, y
  end

  def to_s
    "(#{@x}, #{@y})"
  end
end

p = Point.new(3, 5)
puts p.to_s
# => "(3, 5)"

クラスメソッド、クラス変数

  • クラスメソッドは def self.メソッド名 で定義する
  • クラス変数は @@クラス変数名 でアクセスする
class User
  @@user_counter = 0

  def initialize
    @@user_counter += 1
  end

  def self.user_count
    @@user_counter
  end
end

puts User.user_count  # => 0
5.times { User.new }
puts User.user_count  # => 5

アクセス権

  • インスタンス変数
    • デフォルトで private
    • attr_reader :インスタンス変数名 で読み取りのみ可能になる
    • attr_writer :インスタンス変数名 で書き込みのみ可能になる
    • attr_accessor :インスタンス変数名 で読み書き可能になる
    • attr_* 系のメソッドの引数は可変長なので、まとめて指定することも可能
  • メソッド
    • デフォルトで public
    • private メソッドを呼び出した後に定義したメソッドは全て private なメソッドになる
    • protected メソッドを呼び出した後に定義したメソッドは全て protected なメソッドになる
class AccessModifiers
  attr_reader :x  # インスタンス変数 x は読み取りのみ可能
  attr_writer :y  # インスタンス変数 y は書き込みのみ可能
  attr_accessor :z  # インスタンス変数 z は読み書き可能

  def initialize(x, y, z)
    @x, @y, @z = x, y, z
  end

  def public_method
  end

  private
  def private_method
    # プライベートメソッド
  end

  def private_method2
    # プライベートメソッド
  end

  protected
  def protected_method
    # プロテクトメソッド
  end
end

sample = AccessModifiers.new(123, "foo", :bar)
puts sample.x  # => 123
sample.z = :baz
puts sample.z  # => :baz

オーバーロード

  • Rubyではオーバーロードはできないが、デフォルト引数を取ることはできる。
class Point
  attr_reader :x, :y

  def initialize(x, y)
    @x, @y = x, y
  end

  def distance(other_point = nil)
    if other_point
      Math.sqrt(
        (self.x - other_point.x) ** 2 +
        (self.y - other_point.y) ** 2
      )
    else
      Math.sqrt(@x ** 2 + @y ** 2)
    end
  end
end

p1 = Point.new(3, 4)
p2 = Point.new(15, 20)
puts p1.distance     # => 5.0
puts p1.distance(p2) # => 20.0

self vs @ (補足)

先の例のように、 インスタンス変数にアクセスするときには基本的に @ を使うのだが、 そのインスタンス変数を attr_reader などに指定してある場合は、 プログラムの文脈によっては self からアクセスしたほうが英語として読み易い場合もある。

ただし、self から始める場合は、メソッド呼び出しとなるので、混乱しない程度に使おう。

class Foo
  attr_reader :foo

  def initialize
    @foo = "instance var @foo"
  end

  def foo
    "public method foo()"
  end

  def get_foo
    puts self.foo
    # ここの self.foo はメソッドを呼んでいるのか、
    # それともインスタンス変数を参照しているのか、プログラムを動かすまでよく分からない
  end
end

sample = Foo.new
sample.get_foo  # => "public method foo()"

実際、attr_* 系のメソッドはメタプログラミングの一つで、動的に getter や setter を定義しているだけである。

# attr_accessorの場合

class Foo
  attr_accessor :foo
end

# 上のコードは下のように展開される

class Foo
  # getter
  def foo
    @foo
  end

  # setter
  def foo=(value)
    @foo = value
  end
end

演算子の定義

Rubyで定義できる演算子一覧(優先度順)

  • [], []=
  • **
  • !, ~, +, - (単項演算子を定義するときは !@ のように @ をくっつけること)
  • *, /, %
  • +, -
  • >>, <<
  • &
  • ^, |
  • <=, <, >, >=
  • <=>, ==, ===, !=, =~, !~
class Point
  attr_reader :x, :y

  def initialize(x, y)
    @x, @y = x, y
  end

  def +(other)
    Point.new(self.x + other.x, self.y + other.y)
  end

  def -@
    Point.new(-@x, -@y)
  end

  def to_s
    "(#{@x}, #{@y})"
  end
end

puts Point.new(3, 5) + Point.new(-2, 5)
# => "(1, 10)"
puts -Point.new(3, 5)
# => "(-3, -5)"

継承、オーバーライド

  • 継承は class A < B
  • 同名のメソッドを定義すれば、オーバーライドになる。
class Point
  attr_reader :x, :y

  def initialize(x, y)
    @x, @y = x, y
  end

  def +(other)
    Point.new(self.x + other.x, self.y + other.y)
  end

  def to_s
    "(#{@x}, #{@y})"
  end
end

class Point3D < Point
  attr_reader :z

  def initialize(x, y, z)
    super(x, y)
    @z = z
  end

  def +(other)
    Point3D.new(self.x + other.x, self.y + other.y, self.z + other.z)
  end

  def to_s
    "(#{@x}, #{@y}, #{@z})"
  end
end

p1 = Point3D.new(1, 2, 3)
p2 = Point3D.new(4, 5, 6)
puts p1 + p2
# => "(5, 7, 9)"

モジュール

  • module というキーワードでモジュールを宣言
  • 使い方としては、名前空間としてのモジュールと、Mixin としてのモジュールの2種類ある

名前空間としてのモジュール

module UTF8
  def self.encode(text)
    # ...
    text
  end

  def self.decode(text)
    # ...
    text
  end
end

text = UTF8.encode("123abc")
data = UTF8.decode(text)

モジュールの全てのメソッドを公開する場合は、全てのメソッドの前に self. をつける代わりに、 module_function を使うこともできる。

module UTF8
  module_function

  def encode(text)
    # ...
    text
  end

  def decode(text)
    # ...
    text
  end
end

text = UTF8.encode("123abc")
data = UTF8.decode(text)

Mixin としてのモジュール

module SayGreeting
  def good_morning
    "good morning!"
  end

  def good_night
    "good night!"
  end
end

class Person
  include SayGreeting
end

alice = Person.new
puts alice.good_morning  # => "good morning!"

特異メソッドと特異クラス

特異メソッド(Singleton Method)とは、あるオブジェクトにだけ所属するメソッドのことです。

obj = "Alice"
other_obj = "Bob"

# objに対する、特異メソッドの定義
def obj.greet
  "My name is #{self}"
end

puts obj.greet       # => "My name is Alice"
puts other_obj.greet # => NoMethodError

上の例では、インスタンスに対して特異メソッドを定義しましたが、 クラス自身もオブジェクトなのでクラスにも特異メソッドを定義することができます。

ただし、クラスの特異メソッドは通常「クラスメソッド」などと呼ばれます。

class A
  def initialize
    # ...
  end
end

# クラスAに対する、特異メソッドの定義
def A.method
  "A's class method"
end

puts A.method  # => "A's class method"

一般的に、クラスメソッドを定義するときは、次のようにクラスの宣言の内部で行います。

class A
  # クラスオブジェクトを明示する場合
  def A.method
    "A's class method"
  end

  # selfというキーワードは、今いるスコープのクラスオブジェクトを返してくれる
  def self.method
    "A's class method"
  end

  def initialize
    # ...
  end
end

特異クラス(Eigenclass)とは、そのオブジェクトだけが継承する無名のクラスのことです。 ちなみに、特異メソッドは特異クラスのメソッドとして定義されます

Rubyにおいては、特異クラスは自分で定義できないので、 代わりに 特異クラスをオープンする といい、class << obj と書きます。

特異クラスをオープンすることによって、特異メソッドをまとめて定義することができます。

obj = "Alice"
other_obj = "Bob"

# objの特異クラスをオープンし、特異メソッドを定義する
class << obj
  def greet
    "My name is #{self}"
  end

  def say_hello
    "Hello!"
  end
end

puts obj.greet       # => "My name is Alice"
puts other_obj.greet # => NoMethodError

上の例では、インスタンスに対して特異クラスをオープンしましたが、 クラス自身もオブジェクトなのでクラスの特異クラスもオープンすることができます。

同様に、クラスの特異クラスをオープンして、定義した特異メソッドは通常「クラスメソッド」などと呼びます。

class A
  def initialize
    # ...
  end
end

# クラスAに対する、特異メソッドの定義
class << A
  def method
    "A's class method"
  end

  def method2
    "A's class method2"
  end
end

puts A.method  # => "A's class method"

一般的に、特異クラスをオープンしてクラスメソッドを定義するときは、次のようにクラスの宣言の内部で行います。

class A
  class << self
    def method
      "A's class method"
    end

    def method2
      "A's class method2"
    end
  end

  def initialize
    # ...
  end
end

puts A.method   # => "A's class method"
puts A.method2  # => "A's class method2"

特異クラスの参照(補足)

この話は聞くと混乱するかもしれないので補足程度ですが、 特異クラスをオープンした中で self を使うことで、特異クラス自身を参照することができます。

class A
  def A.eigenclass
    class << A
      self
    end
  end
end

puts A.eigenclass      # => #<Class:A>
puts A.eigenclass.name # =>(無名のクラスなので、クラス名はない)

リフレクションとメタプログラミング

リフレクション とは、プログラム自身が自分の状態や構造を解析することである。

Rubyにおいて メタプログラミング とは、プログラムを動的に拡張することである。 具体的には、動的にメソッドを追加・変更・削除したり、動的にクラスを定義したりする。

リフレクションとメタプログラミングは非常に相性が良い。 処理の流れとしては、 リフレクションによってプログラム自身(クラスの継承関係など)を解析し、 その結果に基づいてメタプログラミング(動的にメソッドを追加するなど)を行う。

ここではリフレクションとメタプログラミングとメタプログラミングについて一から説明するつもりはないので、 クラス周辺にまつわるメタプログラミングについてだけ簡単に説明していく。

オープンクラス

  • Rubyの全てのクラスは拡張可能である
  • ただし安易に拡張することを モンキーパッチ と呼び、 プログラム全体に影響がおよぶ可能性があるので、極力避けるべきである
class String
  def to_alphanumeric
    self.gsub(/[^\w\s]+/, '')
  end
end

"abc 123 $%& edf".to_alphanumeric  # => "abc 123  edf"

リファインメント

リファインメント(Refinements)を使うと、オープンクラスの適用範囲を制限することができます。 モジュールで定義された拡張を有効にするには using キーワードを使います

# リファインメントの定義
module StringExtender
  refine String do
    def to_alphanumeric
      self.gsub(/[^\w\s]+/, '')
    end
  end
end


class SandBox
  using StringExtender
  # => このスコープでのみ、Stringへの拡張メソッドが使えるようになる
end

動的メソッド生成

  • 動的にメソッドを定義するには Moduleクラスのプライベートメソッド define_method を使う

以下の例は、クラスマクロと動的メソッド生成の組み合わせ

class Shop
  @@tax = 1.05

  def self.define_item(name, price)
    define_method(name) do
      "#{name} is ¥#{price * @@tax}"
    end
  end

  define_item("apple",  200)
  define_item("banana", 100)
  define_item("cherry", 300)
end

alice_shop = Shop.new
alice_shop.apple  # => "apple is ¥210.0"
alice_shop.banana # => "banana is ¥105.0"

フックメソッド

特定のイベントが発生したときに、自動で実行されるメソッドを フックメソッド と呼ぶ

  • 自クラスが継承されたときに呼ばれるメソッド self.inherited
  • 自モジュールが Mixin されたときに呼ばれるメソッド self.included
class A
  def self.inherited(subclass)
    puts "#{self} was extended by #{subclass}"
  end
end

class B < A
end
# => A was extended by B

class C < B
end
# => B was extended by C
module A
  def self.included(subclass)
    puts "#{self} was included by #{subclass}"
  end
end

class B
  include A
end
# => A was included by B

Mixinしたときに、インスタンスメソッドとクラスメソッドの両方を取り込む技

  • Mixinされるモジュールの self.included フックに Mixinする側のクラスの extend メソッドで、クラスメソッドを送り込む
module A
  def self.included(subclass)
    subclass.extend(ClassMethods)
  end

  # クラスメソッドになるメソッドは、ClassMethodsに定義する
  module ClassMethods
    def class_method
      "this is class method."
    end
  end

  # インスタンスメソッドになるメソッドは、普通に定義する
  def instance_method
    "this is instance method."
  end
end


class B
  include A
end

B.class_method  # => "this is class method."
B.new.instance_method  # => "this is instance method."