🎉

『何となく』から卒業!Rubyモジュールの使い分けを体系的に理解する

に公開

はじめに

「このクラス、本当にクラスである必要があるのだろうか?」

業務中にコードレビューでこんな指摘を受けたことはありませんか?私は最近、まさにこの問題に直面しました。状態を持たない純粋な計算処理をクラスで実装していたところ、「これはモジュールの方が適切では?」というフィードバックを受けたのです。

この経験をきっかけに、Rubyのモジュールについて改めて深く学び直しました。includeextendmodule_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

なぜ明示的指定が推奨されるのか

  1. セキュリティ: 意図しないメソッドの公開を防ぐ
  2. 保守性: どのメソッドが外部利用可能か明確
  3. 可読性: コードの意図が分かりやすい

実際、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つの主要機能

  1. includedブロック: モジュールがincludeされた時に実行される処理
  2. class_methodsブロック: クラスメソッドを直感的に定義
  3. 依存関係の自動解決: モジュール間の依存順序を自動管理

使い分けの比較表

特徴 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      # 複数形は避ける(例外もあり)

エラーを避けるチェックポイント

  1. module_function指定したメソッドは外部公開される
    • 内部実装は別途private指定する
  2. 全メソッド一括指定は避ける
    • Ruby 2.1+では明示的指定を推奨
  3. 依存関係を明確にする
    • Rails依存の機能はActiveSupport::Concernを検討

まとめ

今回のリファクタリング経験を通じて、以下のことを学びました

使い分けの基本原則

  1. stateless な処理module_function
  2. Rails モデルの機能拡張ActiveSupport::Concern
  3. インスタンスに振る舞いを追加include
  4. クラスにメソッドを追加extend

品質向上のポイント

  • 明示的なmodule_function指定でセキュリティと可読性を向上
  • 適切な命名規則でコードの意図を明確化
  • 使い分けの基準を持つことで、一貫性のある設計を実現

「何となく」使っていたモジュールも、明確な基準を持つことで、より適切で保守性の高いコードを書けるようになります。同じような疑問を持つ方の参考になれば幸いです。

皆さんも、自分のコードを見直してみてください。「このクラス、本当にクラスである必要があるかな?」と考えることで、より良い設計に気づくかもしれません。

参考資料

https://rubystyle.guide/#module-function
https://docs.rubocop.org/rubocop/cops_style.html#stylemodulefunction
https://api.rubyonrails.org/classes/ActiveSupport/Concern.html

合同会社春秋テックブログ

Discussion