【Ruby】モジュールとMix-inの使い方を中級者向けに解説

Ruby

こんにちは、かつコーチです。

クラスの基本を理解すると、次に出てくるのが「モジュール」です。

「クラスと何が違うのか」「includeすると何が起きるのか」が分からず、なんとなく雰囲気で使ってしまっている方も多いのではないでしょうか。

この記事では、RubyのモジュールMix-inの仕組みを、中級者の方向けに整理して解説します。

読み終える頃には、名前空間としての使い方と、機能を共有するMix-inとしての使い方の両方を、使い分けられるようになります。

モジュールとは?

モジュールの2つの役割

モジュールmodule)は、Rubyにおいて主に2つの役割を持つ仕組みです。

1つ目は、メソッドや定数を1つの名前の下にまとめる「名前空間」としての役割です。

2つ目は、クラスに機能を後から追加する「Mix-in」としての役割です。

クラスとの決定的な違いは、モジュールからはnewでインスタンスを作れないという点です。

module Greeting
end

Greeting.new
# => NoMethodError: undefined method `new' for Greeting:Module

モジュールはあくまで「機能や定数のまとまり」であり、それ自体が実体を持つことはありません。

基本の書き方:名前空間としてのモジュール

手順1:moduleで定数やメソッドをまとめる

複数のクラスや定数が同じ名前になりそうなとき、モジュールで囲むことで名前の衝突を防げます。

module Shop
  TAX_RATE = 0.10

  class Product
    def initialize(name, price)
      @name = name
      @price = price
    end

    def price_with_tax
      (@price * (1 + Shop::TAX_RATE)).to_i
    end
  end
end

product = Shop::Product.new("本", 1000)
puts product.price_with_tax # => 1100

Shop::Productのように::(コロン2つ)で階層をたどることで、モジュールの中の定数やクラスにアクセスできます。

これにより、別の場所でProductという名前のクラスが定義されていても、Shop::ProductOther::Productのように区別ができます。

手順2:module_functionで関数のように呼び出す

モジュールに定義したメソッドを、インスタンスを作らずに直接呼び出したい場合はmodule_functionを使います。

module Calculator
  module_function

  def add(a, b)
    a + b
  end
end

puts Calculator.add(1, 2) # => 3

数学的な計算処理や変換処理など、状態を持たない便利メソッド集を作りたいときによく使われる書き方です。

基本の書き方:Mix-inで機能を共有する

手順1:includeでクラスに機能を混ぜ込む

Rubyでは、クラスの多重継承(複数のクラスを同時に継承すること)ができません。

その代わりに使われるのが、モジュールをincludeする「Mix-in」という仕組みです。

module Flyable
  def fly
    "#{self.class}が飛んでいます。"
  end
end

class Bird
  include Flyable
end

class Airplane
  include Flyable
end

puts Bird.new.fly     # => Birdが飛んでいます。
puts Airplane.new.fly # => Airplaneが飛んでいます。

BirdAirplaneは直接の親子関係を持たない別々のクラスですが、同じFlyableモジュールをincludeすることで、どちらもflyメソッドを使えるようになります。

「継承関係にない複数のクラスに、共通の振る舞いを持たせたい」というときに、Mix-inは非常に有効です。

手順2:Comparableで比較演算を一括定義する

標準ライブラリにも、Mix-in用に設計されたモジュールがいくつもあります。

代表例がComparableです。

<=>(宇宙船演算子)を1つ定義するだけで、<>==などの比較演算子がすべて使えるようになります。

class Price
  include Comparable

  attr_reader :amount

  def initialize(amount)
    @amount = amount
  end

  def <=>(other)
    amount <=> other.amount
  end
end

a = Price.new(1000)
b = Price.new(2000)

puts a < b   # => true
puts a > b   # => false
puts [b, a].sort.map(&:amount) # => [1000, 2000]

自分で<>を1つずつ定義しなくても、<=>を実装するだけで比較・ソート機能がまとめて手に入るのは、Mix-inならではの恩恵です。

よくあるつまずきポイント・エラー対処

includeの書き忘れによるNoMethodError

❌ Before:includeを忘れてモジュールのメソッドを呼び出す

わたしが最初にMix-inを使ったとき、モジュールを定義しただけで満足してしまい、includeを書き忘れたことがあります。

module Flyable
  def fly
    "飛んでいます。"
  end
end

class Bird
end

puts Bird.new.fly
# => NoMethodError: undefined method `fly' for #<Bird:0x00007f9c8a0b0d18>

このエラーを見たとき、「モジュールにflyメソッドを定義したのに、なぜ呼び出せないんだろう」としばらく悩みました。

原因は単純で、BirdクラスにFlyableモジュールをincludeしていなかったからです。

✅ After:includeでモジュールを取り込む

module Flyable
  def fly
    "飛んでいます。"
  end
end

class Bird
  include Flyable
end

puts Bird.new.fly
# => 飛んでいます。

include Flyableを1行追加するだけで解決します。

NoMethodErrorが出たときは、まずancestorsメソッドでクラスの継承・Mix-in関係を確認するのがおすすめです。

puts Bird.ancestors
# => [Bird, Flyable, Object, Kernel, BasicObject]

Flyableが一覧に含まれていなければ、includeし忘れている証拠です。

まとめ

この記事のポイント

  • モジュールはインスタンスを作れず、「名前空間」と「Mix-in」の2つの役割を持つ
  • Shop::Productのように::で階層をたどると、名前の衝突を防げる
  • Mix-inは、Rubyにない多重継承の代わりに使われる仕組み
  • includeし忘れるとNoMethodErrorになるので、ancestorsで継承関係を確認すると原因が分かりやすい
  • Comparableのように、標準ライブラリのモジュールも積極的に活用できる

次に読むべき記事

モジュールの次は、テストの基本であるRSpecの書き方に進むと、クラス設計の理解がさらに深まります。

タグ: Ruby, 中級者向け, 基本文法

タイトルとURLをコピーしました