0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RailsのClass継承を理解する

Posted at

はじめに

RailsアプリケーションでClass継承を使いこなすことで、DRY(Don't Repeat Yourself)な設計が実現できます。本記事では、Railsにおける継承の基本から実践的な使い方まで、具体例を交えて解説します。

基本的な継承の書き方

Rubyでは<演算子を使ってクラスを継承します。

class 子クラス < 親クラス
  # 子クラスの実装
end

Railsの主要な継承パターン

1. Model(モデル)の継承

基本的なモデル継承

# app/models/user.rb
class User < ApplicationRecord
  # ApplicationRecordを継承することで
  # Active Recordの全機能が使える
  validates :email, presence: true
end

ApplicationRecordの構造

# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true  # 直接インスタンス化されないクラス
  
  # アプリ全体で共通のメソッドを定義
  def formatted_created_at
    created_at.strftime("%Y年%m月%d日")
  end
end

2. Controller(コントローラー)の継承

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  before_action :set_locale
  
  private
  
  def set_locale
    I18n.locale = params[:locale] || I18n.default_locale
  end
end

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  # ApplicationControllerの機能を全て継承
  before_action :authenticate_user!  # 追加の認証
  
  def index
    @users = User.all
  end
end

3. 単一テーブル継承(STI: Single Table Inheritance)

一つのテーブルで複数のモデルクラスを管理する強力な機能です。

マイグレーション

class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string :type  # STIで必須のカラム
      t.string :name
      t.string :email
      t.integer :permission_level  # Admin用
      t.string :company_name       # Customer用
      
      t.timestamps
    end
  end
end

モデルの実装

# app/models/user.rb
class User < ApplicationRecord
  validates :name, presence: true
  validates :email, presence: true, uniqueness: true
  
  def greeting
    "こんにちは、#{name}さん"
  end
end

# app/models/admin.rb
class Admin < User
  validates :permission_level, presence: true
  
  def greeting
    "管理者の#{name}です"
  end
  
  def can_delete_users?
    permission_level > 3
  end
end

# app/models/customer.rb
class Customer < User
  validates :company_name, presence: true
  
  def greeting
    "#{company_name}#{name}です"
  end
  
  def calculate_discount
    # 顧客特有の割引計算
  end
end

STIの使用例

# 新規作成
admin = Admin.create(name: "田中", email: "admin@example.com", permission_level: 5)
customer = Customer.create(name: "佐藤", email: "customer@example.com", company_name: "ABC商事")

# 検索
User.all           # AdminとCustomerも含む全ユーザー
Admin.all          # Adminのみ
Customer.all       # Customerのみ

# typeカラムは自動で設定される
admin.type         # => "Admin"
customer.type      # => "Customer"

継承で引き継がれる要素

引き継がれるもの

class Parent < ApplicationRecord
  # 1. バリデーション
  validates :name, presence: true
  
  # 2. アソシエーション
  has_many :posts
  belongs_to :company
  
  # 3. スコープ
  scope :active, -> { where(active: true) }
  
  # 4. コールバック
  before_save :normalize_data
  
  # 5. クラスメソッド
  def self.featured
    where(featured: true)
  end
  
  # 6. インスタンスメソッド
  def display_name
    name.upcase
  end
  
  private
  
  def normalize_data
    self.name = name.strip if name
  end
end

class Child < Parent
  # 上記すべてが使える!
  # 追加のバリデーション
  validates :age, numericality: { greater_than: 0 }
end

メソッドのオーバーライドとsuper

class Vehicle < ApplicationRecord
  def start_engine
    update(engine_status: 'running')
    "エンジンを始動しました"
  end
  
  def stop_engine
    update(engine_status: 'stopped')
    "エンジンを停止しました"
  end
end

class ElectricCar < Vehicle
  # 完全にオーバーライド
  def start_engine
    update(battery_status: 'active', engine_status: 'running')
    "モーターを起動しました"
  end
  
  # superで親メソッドを呼び出し
  def stop_engine
    result = super  # 親クラスのstop_engineを実行
    update(battery_status: 'standby')
    result + "(バッテリーをスタンバイモードに)"
  end
end

Concernsを使った機能の共有

継承の代わりにモジュールを使う方法です。

# app/models/concerns/timestampable.rb
module Timestampable
  extend ActiveSupport::Concern
  
  included do
    scope :recent, -> { where('created_at > ?', 1.week.ago) }
    scope :old, -> { where('created_at < ?', 1.year.ago) }
  end
  
  def age_in_days
    ((Time.current - created_at) / 1.day).to_i
  end
  
  def recently_updated?
    updated_at > 1.hour.ago
  end
end

# 複数のモデルで利用
class Article < ApplicationRecord
  include Timestampable
end

class Comment < ApplicationRecord
  include Timestampable
end

実践的なユースケース

ECサイトの商品モデル

# 基底クラス
class Product < ApplicationRecord
  validates :name, presence: true
  validates :price, numericality: { greater_than_or_equal_to: 0 }
  
  has_many :reviews
  has_many :cart_items
  
  scope :available, -> { where('stock > 0') }
  
  def discount_price(rate)
    (price * (1 - rate)).round
  end
end

# 物理商品
class PhysicalProduct < Product
  validates :weight, presence: true
  validates :dimensions, presence: true
  
  def shipping_cost
    base_cost = 500
    weight_cost = weight * 100
    base_cost + weight_cost
  end
end

# デジタル商品
class DigitalProduct < Product
  validates :file_size, presence: true
  validates :download_url, presence: true
  
  def shipping_cost
    0  # デジタル商品は送料無料
  end
  
  def generate_download_link(user)
    # ダウンロードリンクの生成ロジック
  end
end

# サブスクリプション商品
class SubscriptionProduct < Product
  validates :billing_cycle, inclusion: { in: %w[monthly yearly] }
  
  def next_billing_date(from_date = Date.current)
    case billing_cycle
    when 'monthly'
      from_date + 1.month
    when 'yearly'
      from_date + 1.year
    end
  end
end

アンチパターンと注意点

1. 深すぎる継承階層

# ❌ 悪い例:深すぎる継承
class ApplicationRecord < ActiveRecord::Base
  class User < ApplicationRecord
    class Customer < User
      class PremiumCustomer < Customer
        class VIPCustomer < PremiumCustomer  # 5階層は深すぎる!
        end
      end
    end
  end
end

# ✅ 良い例:Concernsで機能を分離
module Premium
  extend ActiveSupport::Concern
  # プレミアム機能
end

module VIP
  extend ActiveSupport::Concern
  # VIP機能
end

class Customer < User
  include Premium  # 必要に応じてinclude
  include VIP
end

2. 不適切なSTIの使用

# ❌ 悪い例:カラムが大きく異なる場合
class Animal < ApplicationRecord
  # type, name, age, fly_speed, swim_depth, run_speed...
  # 鳥、魚、哺乳類で必要なカラムが全然違う
end

# ✅ 良い例:別テーブルにする
class Bird < ApplicationRecord
  # birds テーブル: name, age, fly_speed, wing_span
end

class Fish < ApplicationRecord
  # fishes テーブル: name, age, swim_depth, fin_type
end

ベストプラクティス

1. abstract_classを適切に使う

class BaseModel < ApplicationRecord
  self.abstract_class = true  # このクラスはテーブルを持たない
  
  # 共通のメソッドやバリデーション
  def self.recent(days = 7)
    where('created_at > ?', days.days.ago)
  end
end

class User < BaseModel
  # usersテーブルを使用
end

class Post < BaseModel
  # postsテーブルを使用
end

2. 継承とCompositionの使い分け

# 継承が適切:is-a関係
class Admin < User  # AdminはUserである
end

# Compositionが適切:has-a関係
class User < ApplicationRecord
  has_one :profile  # UserはProfileを持つ(継承ではない)
  
  delegate :bio, :avatar, to: :profile
end

まとめ

Railsのクラス継承は、適切に使えば強力なツールです。以下のポイントを押さえておきましょう:

  1. 基本的な継承:Model、Controller、Viewヘルパーなど、Railsのあらゆる場所で使える
  2. STI:同じテーブル構造を持つ関連クラスに最適
  3. メソッドオーバーライドsuperを活用して親クラスの機能を拡張
  4. Concerns:横断的な機能は継承よりもモジュールで
  5. 階層は浅く:3階層程度までに留める

継承を使いこなして、メンテナンスしやすいRailsアプリケーションを作りましょう!

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?