Ruby

Rubyのモジュールまとめ

Rubyのモジュールについて調べたのでまとめます。
動作確認はRuby 2.4.2でやってます。

Rubyにおけるモジュールについて

モジュールの特徴

  • インスタンスを生成ができない
  • 継承ができない

モジュールの使われ方

  • 名前空間として利用する
  • Mixin

    • include -> モジュールのメソッドをクラスのインスタンスメソッドとして継承ツリーの上位に取り込む
    • prepend -> モジュールのメソッドをクラスのインスタンスメソッドとして継承ツリーの下位に取り込む
    • extend -> モジュールのメソッドをオブジェクトのクラスメソッドとして取り込む
  • モジュール関数として利用する

定義

module Foo

end

名前空間として利用する

モジュールの定義の中にネストした別のモジュールやクラスを定義することで名前空間として利用できます。こうすることで同じ名前のクラス・メソッドなどを名前空間で分別することができます。名前空間のモジュールやクラスへのアクセスは::を使用します。

module MotorCycle
  module Onroad
    class MotoGP
      def foo(bar)
        p bar
      end
    end
  end

  module Offroad
    class MotocrossGP
      def foo(bar)
        p "#{bar} MotocrossGP_class!!!"
      end
    end
  end
end

MotorCycle::Onroad::MotoGP # => MotorCycle::Onroad::MotoGP
mx_gp = MotorCycle::Offroad::MotocrossGP.new
mx_gp.foo('hoge') # => "hoge MotocrossGP_class!!!"

Mixin

ざっくりとクラスとモジュールの違いとしては、以下の通りで、同じ意図を持ったメソッドをモジュールにまとめて見通しを良くしたりするのに使われます。

クラス:インスタンス化できる。メソッドを定義できる。
モジュール:メソッドを定義できるが、インスタンス化できない。

そして、そのモジュールをクラスに取り込むことをMixinと言います。
Rubyの継承は1つのスーパークラスから受け継ぐ、単一継承のみ許可されており、多重継承が許可されておりません。しかし、クラスにモジュールを取り込む(Mixin)ことで多重継承のようなものを実現可能とします。ここではクラスにモジュールを取り込む方法として以下の3つの方法を説明します。

  • include
  • prepend
  • extend

include

モジュールに定義されたメソッドをクラスにインスタンスメソッドとして取り込みます。モジュールをクラスに取り込むにはinclude を使用します。
インスタンスメソッドとして取り込むので、メソッドのレシーバはインスタンスです。

module Foo
  def foo_method
    p 'foo!!!'
    bar_method(self.class) # ここでのselfはこのモジュールを取り込んだクラスのインスタンスを返す
  end
end

class Bar
  include Foo

  def bar_method(val)
    p val
  end
end

bar = Bar.new
bar.foo_method # => "foo!!!"
               # => Bar

継承ツリー

include を使用すると、そのクラスとスーパークラスの間にincludeされたモジュールが内部的に定義されます。
以下の通り、継承ツリーはBar -> Foo(をラップしたクラス) -> SuperBar となるため、メソッドの探索もBarから順に探索し、最初に見つかったメソッドが実行されます。

module Foo
end

class SuperBar
end

class Bar < SuperBar
  include Foo
end

p Bar.ancestors # => [Bar, Foo, SuperBar, Object, Kernel, BasicObject]

モジュールから取り込んだメソッドのオーバーライド

モジュールから取り込んだメソッドをオーバーライドするには以下のように行います。

module Foo
  def foo_method
    p 'foo_method!!!'
  end
end

class Bar
  include Foo

  def foo_method
    p 'bar_class!!!'
    super  # モジュール側に定義されたメソッドが呼び出される
  end
end

bar = Bar.new
bar.foo_method # => "bar_class!!!"
               # => "foo_method!!!"

prepend (ruby2.0~)

モジュールのメソッドをクラスのインスタンスメソッドとして継承ツリーの下位に取り込みます。前述のincludeとの違いは継承ツリーの下位に取り込まれるため、こちらの方がメソッドの実行順として優先されることです。

module Foo
  def foo_method
    p 'foo_module!!!'
    super
  end
end

class Bar
  prepend Foo

  def foo_method
    p 'bar_class!!!'
  end
end

bar = Bar.new
bar.foo_method # => "foo_module!!!"
               # => "bar_class!!!"

よって、継承ツリーはincludeと異なり、Foo(をラップしたクラス) => Bar => SuperBarとなります。

module Foo
end

class SuperBar
end

class Bar < SuperBar
  prepend Foo
end

p Bar.ancestors # => [Foo, Bar, SuperBar, Object, Kernel, BasicObject]

extend

モジュールをオブジェクトの特異クラスとして取り込み、モジュールのメソッドを特異メソッドとして使用することができます。

オブジェクト(クラス)のクラスメソッドとして取り込む

以下のようにクラス内でextendするとモジュールのメソッドをオブジェクトのクラスメソッドとして取り取り込むことができます。クラスメソッドなのでインスタンスからは呼び出せず、クラスをレシーバとしてクラスから直接呼び出します。

module Foo
  def foo_method
    p 'foo_method!!!'
  end
end

class Bar
  extend Foo
end

Bar.foo_method # => "foo_method!!!"
オブジェクト(インスタンス)の特異メソッドとして取り込む

以下のようにObjectのインスタンスにextendすると、そのインスタンスにモジュールのメソッドを取り込むことができます。

module Foo
  def foo_method
    p 'foo_method!!!'
  end
end

obj = Object.new
obj.extend Foo
obj.foo_method # => "foo_method!!!"

# 特異クラスの継承ツリーを確認するとFooが追加されている
p obj.singleton_class.ancestors # => [#<Class:#<Object:0x00007f7e55aed190>>, Foo, Object, Kernel, BasicObject]

モジュール関数として利用する

module_functionを使用するとモジュール関数として利用することができます。

モジュール関数とは、プライベートメソッドであると同時に モジュールの特異メソッドでもあるようなメソッドです。

https://docs.ruby-lang.org/ja/2.4.0/method/Module/i/module_function.html

module Foo
  def foo_method
    p 'foo_method!!!'
  end
  module_function :foo_method
end

# モジュールから直接呼び出す方法(モジュールの特異メソッドとして呼んでいる)
Foo.foo_method # => "foo_method!!!"

# includeして直接呼び出す方法
include Foo
foo_method # => "foo_method!!!"

モジュール関数を複数定義する場合は以下のようにmodule_function以下に続けてメソッドを定義します。

module Foo
  module_function

  def foo_method_a
  end
  def foo_method_b
  end
end

参考

『パーフェクトRuby』