こんにちは、かつコーチです。
クラスの基本を理解すると、次に出てくるのが「モジュール」です。
「クラスと何が違うのか」「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::ProductとOther::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が飛んでいます。
BirdとAirplaneは直接の親子関係を持たない別々のクラスですが、同じ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, 中級者向け, 基本文法