『何となく』から卒業!Rubyモジュールの使い分けを体系的に理解する
はじめに
「このクラス、本当にクラスである必要があるのだろうか?」
業務中にコードレビューでこんな指摘を受けたことはありませんか?私は最近、まさにこの問題に直面しました。状態を持たない純粋な計算処理をクラスで実装していたところ、「これはモジュールの方が適切では?」というフィードバックを受けたのです。
この経験をきっかけに、Rubyのモジュールについて改めて深く学び直しました。include、extend、module_functionの使い分けや、Rails開発でよく使うActiveSupport::Concernとの使い分けなど、「何となく」理解していた部分を体系的に整理してみました。
同じように「何となく」モジュールを使用している方の参考になれば幸いです。
きっかけとなったリファクタリング事例
まずは、私がこの学習を始めるきっかけとなった実際のコードを紹介します。
Before: クラスでの実装
# ECサイトの配送料計算クラス
class ShippingCalculator
# 地域別の基本配送料(円)
BASE_SHIPPING_RATES = {
tokyo: 500,
osaka: 600,
hokkaido: 800,
okinawa: 1000
}.freeze
# 重量による追加料金(1kg単位)
WEIGHT_SURCHARGE_RATES = {
tokyo: 100,
osaka: 120,
hokkaido: 150,
okinawa: 200
}.freeze
def self.calculate_shipping_fee(region, weight)
base_fee = BASE_SHIPPING_RATES.fetch(region, 700)
weight_fee = WEIGHT_SURCHARGE_RATES.fetch(region, 130) * weight
base_fee + weight_fee
end
def self.calculate_express_fee(region, weight)
standard_fee = calculate_shipping_fee(region, weight)
standard_fee + 300 # 速達料金
end
def self.free_shipping_threshold(region)
region == :okinawa ? 8000 : 5000
end
end
After: module_functionでの実装
# ECサイトの配送料計算モジュール
module ShippingCalculator
# 地域別の基本配送料(円)
BASE_SHIPPING_RATES = {
tokyo: 500,
osaka: 600,
hokkaido: 800,
okinawa: 1000
}.freeze
# 重量による追加料金(1kg単位)
WEIGHT_SURCHARGE_RATES = {
tokyo: 100,
osaka: 120,
hokkaido: 150,
okinawa: 200
}.freeze
def calculate_shipping_fee(region, weight)
base_fee = BASE_SHIPPING_RATES.fetch(region, 700)
weight_fee = WEIGHT_SURCHARGE_RATES.fetch(region, 130) * weight
base_fee + weight_fee
end
def calculate_express_fee(region, weight)
standard_fee = calculate_shipping_fee(region, weight)
standard_fee + 300 # 速達料金
end
def free_shipping_threshold(region)
region == :okinawa ? 8000 : 5000
end
module_function :calculate_shipping_fee, :calculate_express_fee, :free_shipping_threshold
end
なぜリファクタリングが必要だったのか
このコードを見直してみると、以下の特徴があることがわかります
- 状態を持たない: インスタンス変数を使用していない
- 純粋な計算処理: 入力に対して決まった出力を返すだけ
-
インスタンス化不要:
newして使用することがない - 複数箇所で利用: 様々なコントローラーやサービスから呼び出される
これらの特徴を持つ機能は、クラスよりもモジュールの方が適切です。特にmodule_functionを使用することで、より意図が明確になります。
Rubyモジュールの3つの使用パターン
Rubyでモジュールを使用する際の基本的な3つのパターンを整理してみましょう。
| パターン | 用途 | 呼び出し方 | 使用場面 |
|---|---|---|---|
include |
インスタンスメソッド追加 | obj.method_name |
モデルに共通の振る舞いを追加 |
extend |
クラスメソッド追加 | Class.method_name |
クラスに共通のクラスメソッドを追加 |
module_function |
直接呼び出し可能 | Module.method_name |
ユーティリティ・計算処理 |
実装例で理解する
module ExampleModule
def helper_method
"helper"
end
end
# include: インスタンスメソッドとして追加
class WithInclude
include ExampleModule
end
WithInclude.new.helper_method # ✅ 動く
# extend: クラスメソッドとして追加
class WithExtend
extend ExampleModule
end
WithExtend.helper_method # ✅ 動く
# module_function: モジュール名で直接呼び出し
module WithModuleFunction
def utility_method
"utility"
end
module_function :utility_method
end
WithModuleFunction.utility_method # ✅ 動く
記憶しやすい覚え方
- include = "中に含める" = インスタンスメソッド
- extend = "拡張する" = クラスメソッド
- module_function = "モジュールの関数" = 直接呼び出し
module_functionのベストプラクティス
新旧パターンの比較
module_functionには2つの使用方法がありますが、現在は明示的指定が推奨されています。
# ❌ 古い書き方(Ruby 1.8時代から可能だが非推奨)
module OldStyle
module_function # 全メソッドに適用(危険)
def public_method; end
def private_helper; end # 意図せず公開される
end
# ✅ 新しい書き方(Ruby 2.1+推奨)
module NewStyle
def public_method; end
def private_helper; end
module_function :public_method # 明示的指定
private :private_helper
end
なぜ明示的指定が推奨されるのか
- セキュリティ: 意図しないメソッドの公開を防ぐ
- 保守性: どのメソッドが外部利用可能か明確
- 可読性: コードの意図が分かりやすい
実際、Ruby Style GuideやRuboCopでもmodule_functionの明示的な使用が推奨されています。
実践的な使用例
module CalculationHelper
def tax_amount(price, rate = 0.1)
(price * rate).round
end
def discount_price(original, discount_rate)
original * (1 - discount_rate)
end
# 内部でのみ使用するヘルパーメソッド
def validate_price(price)
raise ArgumentError, "Price must be positive" if price <= 0
end
# 公開したいメソッドのみ明示的に指定
module_function :tax_amount, :discount_price
private :validate_price
end
# 使用例
CalculationHelper.tax_amount(1000) # => 100
CalculationHelper.discount_price(1000, 0.2) # => 800
ActiveSupport::Concernとの使い分け
Rails開発では、ActiveSupport::Concernもよく使用されます。使い分けの基準を明確にしておきましょう。
ActiveSupport::Concernとは
ActiveSupport::Concernは、Railsが提供するモジュール拡張機能で、従来のRubyモジュールでは複雑だった処理を直感的に書けるようにしてくれます。
従来のRubyモジュールの問題点
Rails開発でモデルに共通機能を追加しようとすると、以下のような複雑な書き方が必要でした
# ❌ 従来の方法:複雑で冗長
module Trackable
# includeされた時の処理を定義(理解が困難)
def self.included(base)
base.extend(ClassMethods)
base.class_eval do
has_many :tracking_events
scope :tracked, -> { where(tracked: true) }
end
end
# クラスメソッドを別モジュールで定義する必要がある
module ClassMethods
def track_all!
update_all(tracked: true)
end
end
# インスタンスメソッド
def track!
update(tracked: true)
end
end
この書き方の問題:
-
複雑:
self.includedフックの理解が必要 -
冗長:
ClassMethodsモジュールを別途定義 - 読みにくい: 何をしているのか分かりにくい
ActiveSupport::Concernによる解決
# ✅ Concern使用:シンプルで直感的
module Trackable
extend ActiveSupport::Concern
# インスタンスメソッド(普通に書くだけ)
def track!
update(tracked: true)
end
# includeされた時に実行される処理(直感的)
included do
has_many :tracking_events
scope :tracked, -> { where(tracked: true) }
end
# クラスメソッド(直感的に書ける)
class_methods do
def track_all!
update_all(tracked: true)
end
end
end
実際の使用例で理解する
Rails開発でよくある認証機能のConcernを作ってみましょう
# app/models/concerns/authenticatable.rb
module Authenticatable
extend ActiveSupport::Concern
# インスタンスメソッド
def generate_auth_token
SecureRandom.hex(20)
end
def token_expired?
auth_token_expires_at < Time.current
end
# includeされた時に実行される
included do
has_secure_password
validates :email, presence: true, uniqueness: true
before_create :set_auth_token
end
# クラスメソッド
class_methods do
def authenticate_with_token(token)
find_by(auth_token: token)&.tap do |user|
return nil if user.token_expired?
end
end
def valid_email?(email)
email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
end
end
private
def set_auth_token
self.auth_token = generate_auth_token
self.auth_token_expires_at = 30.days.from_now
end
end
# app/models/user.rb
class User < ApplicationRecord
include Authenticatable # これだけで全機能が使える!
end
# 使用例
user = User.create(email: "test@example.com", password: "password")
user.generate_auth_token # インスタンスメソッド
user.token_expired? # インスタンスメソッド
User.authenticate_with_token("abc123") # クラスメソッド
User.valid_email?("test@example.com") # クラスメソッド
ActiveSupport::Concernの3つの主要機能
-
includedブロック: モジュールがincludeされた時に実行される処理 -
class_methodsブロック: クラスメソッドを直感的に定義 - 依存関係の自動解決: モジュール間の依存順序を自動管理
使い分けの比較表
| 特徴 | module_function |
ActiveSupport::Concern |
|---|---|---|
| 用途 | ユーティリティ関数 | Rails モデル/クラス拡張 |
| 呼び出し方 | Module.method |
obj.method, Class.method
|
| 依存性 | Ruby標準 | Rails必須 |
| クラスメソッド | ❌ 直接追加不可 | ✅ 簡単に追加 |
| フック処理 | ❌ 難しい | ✅ included ブロック |
実践的な使い分けガイド
module_functionを選ぶべき場面
- ✅ 純粋な計算・変換処理
- ✅ 状態を持たないユーティリティ
- ✅ Rails に依存しない汎用処理
- ✅ 設定値の管理・取得
具体例:
module DateHelper
def business_days_between(start_date, end_date)
# 営業日計算ロジック
end
def format_japanese_date(date)
# 日本語日付フォーマット
end
module_function :business_days_between, :format_japanese_date
end
ActiveSupport::Concernを選ぶべき場面
- ✅ Rails モデルの機能拡張
- ✅ 複数のクラスで共通の振る舞い
- ✅ クラスメソッドとインスタンスメソッド両方必要
- ✅ ActiveRecord のコールバック・バリデーション
- ✅ 関連付け(association)の追加
具体例:
module Authenticatable
extend ActiveSupport::Concern
included do
has_secure_password
validates :email, presence: true, uniqueness: true
end
def authenticate_with_token(token)
# 認証ロジック
end
class_methods do
def find_by_credentials(email, password)
user = find_by(email: email)
user&.authenticate(password) ? user : nil
end
end
end
判断フローチャート
Railsアプリ?
├─ No → module_function (汎用ユーティリティ)
└─ Yes → モデル拡張?
├─ No → module_function (計算処理)
└─ Yes → ActiveSupport::Concern (Rails統合)
命名規則とコードの品質
Rails Concerns の命名規則
Railsプロジェクトでのモジュール命名では、以下のパターンが推奨されます
# ✅ 推奨パターン
module Productable # 形容詞形(能力・行動を表す)
module PaymentMethod # 単数形(機能を表す)
module Searchable # 形容詞形(能力を表す)
# ❌ 避けるべきパターン
module ProductableMethods # 冗長
module PaymentMethods # 複数形は避ける(例外もあり)
エラーを避けるチェックポイント
-
module_function指定したメソッドは外部公開される- 内部実装は別途
private指定する
- 内部実装は別途
-
全メソッド一括指定は避ける
- Ruby 2.1+では明示的指定を推奨
-
依存関係を明確にする
- Rails依存の機能は
ActiveSupport::Concernを検討
- Rails依存の機能は
まとめ
今回のリファクタリング経験を通じて、以下のことを学びました
使い分けの基本原則
-
stateless な処理 →
module_function -
Rails モデルの機能拡張 →
ActiveSupport::Concern -
インスタンスに振る舞いを追加 →
include -
クラスにメソッドを追加 →
extend
品質向上のポイント
-
明示的な
module_function指定でセキュリティと可読性を向上 - 適切な命名規則でコードの意図を明確化
- 使い分けの基準を持つことで、一貫性のある設計を実現
「何となく」使っていたモジュールも、明確な基準を持つことで、より適切で保守性の高いコードを書けるようになります。同じような疑問を持つ方の参考になれば幸いです。
皆さんも、自分のコードを見直してみてください。「このクラス、本当にクラスである必要があるかな?」と考えることで、より良い設計に気づくかもしれません。
参考資料
Discussion