0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

第25章|今さら学ぶ「設計・リファクタリング」

0
Last updated at Posted at 2026-02-28

第25章|今さら学ぶ「設計・リファクタリング」

📚 シリーズ目次はこちら → 「今さら学ぶ」シリーズ — はじめに
🗺️ KnowledgeNoteの設計を確認 → 設計マップ

この章でわかること

  • Fat Controller / Fat Model — 一人に仕事を押し付けない
  • デザインパターン(Service / Form / Query / Decorator)— 仕事を分担する専門チーム
  • Concern — 複数モデル/コントローラで共通の処理をモジュールにまとめる
  • 命名の重要性 — コードは「未来の自分への手紙」
  • リファクタリングの進め方 — テストがあるから安心して変えられる

🏠 たとえ話で掴む「設計」

Railsの設計問題は 会社の組織 にたとえるとわかりやすいです。

スタートアップの初期は、社長が営業・経理・人事・開発を全部やっています。でも会社が大きくなると、1人で全部やるのは無理。 部署を作って仕事を分担 しないと回りません。

Railsアプリも同じです。最初はコントローラに全てのロジックを詰め込んでも動きます。でもアプリが大きくなると、 責務を分割しないとコードが読めなくなります

会社の問題 Railsの問題
社長が全部やる(ワンオペ) Fat Controller
経理部長に全部任せる Fat Model
部署を作って分担する Service / Form / Query 等
マニュアルを共有する Concern(共通処理)
名刺の肩書きが的確 命名が適切

🏗️ 設計とは何か — 技術的な定義

ソフトウェア設計とは、 コードの責務をどう分割し、どう組み合わせるかを決めること です。

MVCフレームワークであるRailsは、すでに「Model・View・Controller」という責務分割を提供しています(→ 第9章)。しかしアプリが成長すると、MVC の3つだけでは責務が収まらなくなります。

たとえば「記事を公開する」という処理には、記事の保存、タグの紐付け、フォロワーへの通知、メール送信が含まれます。これをすべてコントローラに書くと Fat Controller になり、モデルに書くと Fat Model になります。

設計の原則として最も重要なのは 単一責任の原則(SRP: Single Responsibility Principle) です。「1つのクラスは、1つの理由でしか変更されるべきでない」という考え方で、これを意識すると自然に責務の分割が進みます。

Railsで使われる主な責務分割パターンは以下のとおりです。

パターン 責務 MVCだけだと何に入るか
Service Object 業務ロジック Controller or Model
Form Object 複数モデルにまたがるフォーム Controller
Query Object 複雑な検索条件 Model(scope)
Decorator 表示用のロジック View helper
Concern 複数モデル共通の振る舞い 各Modelに重複コピー

🏋️ Fat Controller — コントローラの肥大化

# ❌ Fat Controller — コントローラにビジネスロジックを詰め込んでいる
class ArticlesController < ApplicationController
  def create
    @article = current_user.articles.build(article_params)
    @article.status = :published if params[:publish]

    # タグの処理(ここにあるべきじゃない)
    tag_names = params[:tag_names].split(",").map(&:strip)
    tag_names.each do |name|
      tag = Tag.find_or_create_by(name: name)
      @article.tags << tag unless @article.tags.include?(tag)
    end

    if @article.save
      # 通知の処理(ここにあるべきじゃない)
      current_user.followers.each do |follower|
        Notification.create(user: follower, notifiable: @article, action_type: "published")
      end
      # メール送信(ここにあるべきじゃない)
      NotificationMailer.new_article(current_user, @article).deliver_later

      redirect_to @article, notice: "記事を投稿しました"
    else
      render :new, status: :unprocessable_entity
    end
  end
end

このコントローラは「タグの処理」「通知の作成」「メール送信」までやっています。50行、100行と膨らんでいく典型的なFat Controllerです。


🛠️ Service Object — 専門チームに仕事を切り出す

Service Object は、特定の業務ロジックを1つのクラスに切り出すパターンです。

# app/services/article_publish_service.rb
class ArticlePublishService
  def initialize(article:, tag_names: "", user:)
    @article = article
    @tag_names = tag_names
    @user = user
  end

  def call
    ActiveRecord::Base.transaction do
      save_article!
      attach_tags!
      notify_followers! if @article.published?
    end
    true
  rescue ActiveRecord::RecordInvalid
    false
  end

  private

  def save_article!
    @article.save!
  end

  def attach_tags!
    names = @tag_names.split(",").map(&:strip).reject(&:blank?)
    names.each do |name|
      tag = Tag.find_or_create_by!(name: name)
      @article.tags << tag unless @article.tags.include?(tag)
    end
  end

  def notify_followers!
    @user.followers.find_each do |follower|
      Notification.create!(user: follower, notifiable: @article, action_type: "published")
    end
    NotificationMailer.new_article(@user, @article).deliver_later
    # deliver_laterによる非同期メール送信は(→ [第29章](https://qiita.com/harapeco-mgn/items/420a7d80595504b0444d)で詳しく扱います)
  end
end
# ✅ スッキリしたコントローラ
class ArticlesController < ApplicationController
  def create
    @article = current_user.articles.build(article_params)

    service = ArticlePublishService.new(
      article: @article,
      tag_names: params[:tag_names],
      user: current_user
    )

    if service.call
      redirect_to @article, notice: "記事を投稿しました"
    else
      render :new, status: :unprocessable_entity
    end
  end
end

コントローラは「窓口」に戻り、ビジネスロジックはServiceに移りました。


📋 その他のデザインパターン

Form Object — 複数モデルにまたがるフォーム

# app/forms/user_registration_form.rb
class UserRegistrationForm
  include ActiveModel::Model

  attr_accessor :name, :email_address, :password, :password_confirmation

  validates :name, presence: true
  validates :email_address, presence: true
  validates :password, presence: true, confirmation: true

  def save
    return false unless valid?

    user = User.create!(
      name: name,
      email_address: email_address,
      password: password,
      password_confirmation: password_confirmation
    )
    # デフォルトロールの付与
    user.roles << Role.find_by(name: "member")
    user
  rescue ActiveRecord::RecordInvalid
    false
  end
end

Query Object — 複雑な検索条件をまとめる

# app/queries/article_search_query.rb
class ArticleSearchQuery
  def initialize(scope = Article.all)
    @scope = scope
  end

  def call(params)
    @scope = filter_by_status(@scope, params[:status])
    @scope = filter_by_tag(@scope, params[:tag])
    @scope = filter_by_keyword(@scope, params[:keyword])
    @scope = sort_by(@scope, params[:sort])
    @scope
  end

  private

  def filter_by_status(scope, status)
    status.present? ? scope.where(status: status) : scope.published
  end

  def filter_by_tag(scope, tag_name)
    tag_name.present? ? scope.joins(:tags).where(tags: { name: tag_name }) : scope
  end

  def filter_by_keyword(scope, keyword)
    return scope unless keyword.present?

    scope.where("title ILIKE ?", "%#{sanitize_sql_like(keyword)}%")
  end

  def sort_by(scope, sort)
    case sort
    when "oldest" then scope.order(created_at: :asc)
    else scope.order(created_at: :desc)
    end
  end

  def sanitize_sql_like(string)
    # LIKE のワイルドカード文字をエスケープ
    string.gsub(/[%_\\]/) { |m| "\\#{m}" }
  end
end

💡 PostgreSQLでは LIKE は大文字小文字を区別するため、ILIKE(大文字小文字を無視)を使うのが一般的です。また、ユーザー入力をLIKE句に渡す場合は %_ をエスケープしないと意図しない検索結果になります。

パターンの使い分け

パターン 使いどころ ファイルの置き場
Service 「記事を公開する」等の業務ロジック app/services/
Form 複数モデルにまたがるフォーム app/forms/
Query 複雑な検索・フィルター条件 app/queries/
Decorator 表示用のロジック(ヘルパーの代替) app/decorators/

🔗 Concern — 共通処理をモジュールにまとめる

Concern は、複数のモデルやコントローラで共通の処理を モジュール として切り出す仕組みです。

# app/models/concerns/likeable.rb
module Likeable
  extend ActiveSupport::Concern

  included do
    has_many :likes, as: :likeable, dependent: :destroy
  end

  def liked_by?(user)
    likes.exists?(user: user)
  end

  def likes_count
    likes.count
  end
end
# app/models/article.rb
class Article < ApplicationRecord
  include Likeable   # ← これだけで liked_by? と likes_count が使える
end

# app/models/comment.rb
class Comment < ApplicationRecord
  include Likeable   # ← 同じ機能をコメントにも
end

Concern の使いすぎ注意

Concernは便利ですが、 「モデルの行数を減らすためだけに」使うと、処理の全体像が見えにくくなります

✅ Concernが適切な場面
  → 複数モデルで共通の振る舞い(Likeable、Publishable 等)

❌ Concernが不適切な場面
  → 1つのモデルでしか使わない処理を「行数削減」のために切り出す
  → 切り出す先が増えすぎて、全体の処理を追えなくなる

📛 命名の重要性

コードは「未来の自分への手紙」です。半年後の自分が読んで理解できるかどうかは、命名にかかっています。

# ❌ 何をしているかわからない命名
def proc(d)
  d.map { |x| x.v > 100 ? x : nil }.compact
end

# ✅ 読めば意味がわかる命名
def filter_high_value_articles(articles)
  articles.select { |article| article.likes_count > 100 }
end
対象 良い命名 悪い命名
メソッド published_articles get_data
変数 current_user u
クラス ArticlePublishService ArticleHelper2
boolean published? / following? flag / check

🔄 リファクタリングの進め方

リファクタリング とは、外部から見た振る舞い(機能)を変えずに、コードの内部構造を改善することです。

「振る舞いを変えない」ことを保証するのが テスト です(→ 第22章)。テストがない状態でリファクタリングすると、意図せず機能を壊すリスクがあります。

リファクタリングの手順

① テストを書く(まだなければ追加する)
    ↓
② テストが通ることを確認(Green)
    ↓
③ コードを小さく変更する(1ステップずつ)
    ↓
④ テストが通ることを確認(Green のまま)
    ↓
⑤ ③〜④を繰り返す

重要なのは 小さく変更する ことです。一気に大きく書き換えると、テストが失敗したときにどの変更が原因かわからなくなります。

具体例:Fat Controller → Service Object

# ① まず現在のFat Controllerに対するテストを書く(or 確認する)
RSpec.describe "POST /articles", type: :request do
  it "記事が作成され、タグが紐付き、通知が送られる" do
    user = create(:user)
    follower = create(:user)
    follower.follow(user)
    sign_in(user)

    expect {
      post articles_path, params: {
        article: { title: "テスト記事", body: "本文" },
        tag_names: "Ruby, Rails"
      }
    }.to change(Article, :count).by(1)
      .and change(Notification, :count).by(1)

    article = Article.last
    expect(article.tags.pluck(:name)).to contain_exactly("Ruby", "Rails")
  end
end

# ② テストがGreenであることを確認
# ③ ArticlePublishService を作成し、コントローラから呼ぶ形に書き換え
# ④ テストがGreenのままであることを確認
# → 外から見た振る舞いは変わっていない。内部構造だけ改善された

💼 面接で聞かれたら?

Q:Fat Controllerとは何ですか?どう対処しますか?

「Fat Controllerとは、コントローラにビジネスロジックを詰め込みすぎた状態です。本来コントローラは「リクエストを受けてレスポンスを返す」という窓口業務に専念すべきですが、タグ処理・通知作成・メール送信などの処理まで書いてしまうと肥大化します。対処としては、Service Objectに業務ロジックを切り出し、コントローラはServiceを呼ぶだけにします。単一責任の原則に従い、1クラス1責務に整理することが重要です。」

深掘りされたら:

  • 「Concernとは?」→ 複数モデルで共通する処理をモジュールとして切り出す仕組み。extend ActiveSupport::Concern で定義し、include で使う。ただし使いすぎると処理の全体像が見えにくくなる。
  • 「Service / Form / Query の使い分けは?」→ Serviceは業務ロジック、Formは複数モデルにまたがるフォーム、Queryは複雑な検索条件のカプセル化に使う。
  • 「リファクタリングの進め方は?」→ まずテストを書き、テストがGreenであることを確認してから、小さく構造を変更する。テストが常にGreenのまま進めることで、機能を壊さずに内部構造を改善できる。

🔗 もっと深く知りたい人へ(1次情報リンク)


まとめ

  • ✅ Fat Controllerは「社長のワンオペ」。ビジネスロジックをServiceに切り出す
  • ✅ 設計の基本は単一責任の原則。1クラス1責務に整理する
  • ✅ Service(業務ロジック)/ Form(複合フォーム)/ Query(検索条件)で責務を分離
  • ✅ Concernは複数モデルの共通処理に使う。1箇所でしか使わない処理には不向き
  • ✅ 命名は「未来の自分への手紙」。意図が伝わる名前をつける
  • ✅ リファクタリングはテストがあって初めて安心してできる。小さく変更し、常にGreenを保つ

📚 シリーズ目次:「今さら学ぶ」シリーズ — はじめに

0
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?