サイトアイコンmaita tomoya dev io

#44 「1ファイルで済む」vs「正しい設計」- Railsアーキテクチャ原則が教える本当の価値

「1ファイルで済む」vs「正しい設計」- Railsアーキテクチャ原則が教える本当の価値

開発現場でよく聞く言葉があります。

「この修正、1ファイル変更するだけで済むから簡単でしょ?」

確かに、短期的には修正量が少なく見えます。しかし、本当に「簡単」なのでしょうか?

この記事では、実際のWeb・アプリ共通の通知機能実装を例に、「修正量を小さく納める方法」と「Railsのアーキテクチャ原則に従った方法」を比較し、なぜ後者の方が長期的に価値があるのかを詳しく解説します。

この記事で学べること(TL;DR)

  • 「1ファイル修正」のアプローチがなぜ長期的に問題となるか
  • Railsアーキテクチャ原則(Single Source of Truth、Separation of Concerns等)の実践的価値
  • 設計投資の定量的な効果(6ヶ月で14時間削減の実例)
  • チーム開発でのメンテナンス性向上とバグ削減手法

前提:実装すべき機能

WEB版とスマートフォンアプリでメンテナンス通知を統一表示する機能を実装する必要があります。

要件

  • WEB側: Railsビューでメンテナンスバナーを表示
  • APP側: React Native経由でAPIから取得して表示
  • 表示条件: 同じフラグで制御
  • 表示内容: 同じテキストを使用
  • 運用性: 設定変更を容易に

この要件に対して、2つのアプローチが考えられます。

アプローチ1: 「1ファイル修正」で済ます方法

実装方法

# app/controllers/api/v1/app_config_controller.rb(既存ファイル)
class Api::V1::AppConfigController < Api::V1::BaseController
  def show
    render json: { 
      appVersion: "1.0.0",
      showMaintenanceBanner: true,  # ← 直接書く
      maintenanceBannerText: "システムメンテナンスのため、2月15日の深夜2時〜4時にサービスを停止いたします。"  # ← 直接書く
    }
  end
end
<!-- app/views/shared/_maintenance_banner.html.haml(既存ファイル) -->
.maintenance-banner
  システムメンテナンスのため、2月15日の深夜2時〜4時にサービスを停止いたします。

メリット

  • 修正箇所が少ない: 2ファイルのみの変更
  • 即座に実装完了: 設定ファイルやクラス作成が不要
  • 理解しやすい: 値がどこに書かれているか一目瞭然

デメリット(後から判明)

しかし、運用が始まると問題が見えてきます:

1. 同期の困難さ

# APIコントローラー
showMaintenanceBanner: true,
maintenanceBannerText: "システムメンテナンスのため..."

# ビューファイル  
.maintenance-banner
  システムメンテナンスのため... <!-- 微妙に文言が違う! -->

2. 変更時の手間

# テキスト変更時に必要な作業
# 1. APIコントローラーを修正
# 2. ビューファイルを修正  
# 3. 両方が同じ内容か確認
# 4. テスト実行
# 5. デプロイ

3. 緊急時の対応困難

# 緊急でON/OFF切り替えたい場合
# → コード修正 → デプロイが必要
# → 数時間〜半日かかる

アプローチ2: Railsアーキテクチャ原則に従う方法

設計思想

Railsのアーキテクチャ原則に従い、以下を重視します:

  1. Single Source of Truth(単一情報源の原則)
  2. Separation of Concerns(関心の分離)
  3. Convention over Configuration(設定より規約)
  4. Fat Model, Skinny Controller

実装ステップ

ステップ1: 通知設定の専用クラス作成

# app/lib/banner_config.rb(新規作成)
class BannerConfig
  # メンテナンス通知
  module Maintenance
    ENABLED = true
    TEXT = 'システムメンテナンスのため、2月15日の深夜2時〜4時にサービスを停止いたします。'
  end

  # 将来的な拡張用
  module Emergency
    ENABLED = false
    TEXT = '緊急メンテナンスのため、一時的にサービスを停止しています。'
  end

  module Campaign
    ENABLED = false
    TEXT = '新機能リリース記念キャンペーン実施中!'
  end
end

なぜ app/lib/ なのか?

ディレクトリ用途今回の設定に適用?
app/models/ActiveRecordモデル(DB関連)❌ DBテーブルではない
app/controllers/リクエスト処理❌ HTTP処理ではない
app/lib/アプリ固有のライブラリ最適
lib/汎用ライブラリ△ アプリ固有なので微妙

Rails自動読み込み: Rails 6以降、app/lib/config.autoload_pathsに含まれるため、追加設定なしでクラスが自動読み込みされます。

ステップ2: APIコントローラーの修正

# app/controllers/api/v1/app_config_controller.rb
class Api::V1::AppConfigController < Api::V1::BaseController
  def show
    render json: { 
      appVersion: "1.0.0",
      showMaintenanceBanner: BannerConfig::Maintenance::ENABLED,
      maintenanceBannerText: BannerConfig::Maintenance::TEXT
    }
  end
end

ステップ3: WEB側の適切な実装

❌ 良くない実装例

<!-- ビューでビジネスロジックを持つのは適切ではない -->
- if NotificationConfig::ShippingDelay::ENABLED
  .shipping-notice
    = NotificationConfig::ShippingDelay::TEXT

✅ 適切な実装例

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  # サイト全体で必要な場合はbefore_actionが有効
  before_action :set_maintenance_banner
  
  private
  
  def set_maintenance_banner
    @maintenance_banner_enabled = BannerConfig::Maintenance::ENABLED
    @maintenance_banner_text = BannerConfig::Maintenance::TEXT
  end
  
  # 特定のページでのみ使用する場合の代替方法
  # helper_method :maintenance_banner_enabled?, :maintenance_banner_text
  
  # def maintenance_banner_enabled?
  #   BannerConfig::Maintenance::ENABLED
  # end
  
  # def maintenance_banner_text
  #   BannerConfig::Maintenance::TEXT
  # end
end
<!-- app/views/shared/_maintenance_banner.html.haml -->
- if @maintenance_banner_enabled
  .maintenance-banner
    = @maintenance_banner_text

ステップ4: テストの更新

# spec/requests/api/v1/app_config_controller_spec.rb
RSpec.describe Api::V1::AppConfigController do
  describe 'GET #show' do
    context 'メンテナンスバナーが有効な場合' do
      before do
        stub_const('BannerConfig::Maintenance::ENABLED', true)
        stub_const('BannerConfig::Maintenance::TEXT', 'テスト用メッセージ')
      end

      it '適切なレスポンスを返す' do
        get api_v1_app_config_path
        json = JSON.parse(response.body)
        expect(json['showMaintenanceBanner']).to be_truthy
        expect(json['maintenanceBannerText']).to eq('テスト用メッセージ')
      end
    end
  end
end

修正が必要なファイル数

新規作成: 1ファイル
├── app/lib/banner_config.rb

修正: 4ファイル  
├── app/controllers/api/v1/app_config_controller.rb
├── app/controllers/application_controller.rb
├── app/views/shared/_maintenance_banner.html.haml
└── spec/requests/api/v1/app_config_controller_spec.rb

合計: 5ファイル

2つのアプローチの詳細比較

開発時の工数

項目1ファイル修正アーキテクチャ原則
実装時間30分2時間
修正ファイル数2個5個
テスト作成簡単やや複雑
レビュー時間短い長い

一見すると、1ファイル修正の方が効率的に見えます。

運用時の違い

しかし、運用フェーズに入ると状況は一変します:

シナリオ1: テキスト変更

1ファイル修正の場合

# 作業手順(40分)
1. APIコントローラー修正(5分)
2. ビューファイル修正(5分)
3. 内容の同期確認(10分)
4. テスト実行(10分)
5. デプロイ(10分)

アーキテクチャ原則の場合

# 作業手順(15分)
1. NotificationConfig修正(5分)
2. テスト実行(5分)  
3. デプロイ(5分)

シナリオ2: 緊急でON/OFF切り替え

1ファイル修正の場合

# 各ファイルを個別に修正が必要
# APIコントローラー
showMaintenanceBanner: false,  # ← 手動変更

# ビューファイル
- if false  # ← 手動変更(しかもこれだと見た目が悪い)

アーキテクチャ原則の場合

# 1箇所変更するだけ
module Maintenance
  ENABLED = false  # ← この1行だけ
  TEXT = '...'
end

シナリオ3: 新しい通知追加

1ファイル修正の場合

# 各ファイルにバラバラに追加
# APIコントローラー
showEmergencyBanner: true,
emergencyBannerText: "...",

# ビューファイル(別ファイルに)
- if true
  .emergency-banner
    緒急メンテナンス中...

アーキテクチャ原則の場合

# 設定クラスに追加するだけ
module Emergency
  ENABLED = true
  TEXT = '緒急メンテナンス中...'
end

# 使用箇所では統一的に参照
BannerConfig::Emergency::ENABLED
BannerConfig::Emergency::TEXT

長期的なコスト分析

6ヶ月間の運用コスト(仮定)

1ファイル修正の場合

初期実装: 0.5時間
テキスト変更(月2回): 1.3時間 × 6ヶ月 = 7.8時間
ON/OFF切替(月1回): 0.7時間 × 6ヶ月 = 4.2時間  
新通知追加(2回): 2時間 × 2回 = 4時間
バグ修正(同期ミス等): 3時間
------
合計: 約20時間

アーキテクチャ原則の場合

初期実装: 2時間
テキスト変更(月2回): 0.25時間 × 6ヶ月 = 1.5時間
ON/OFF切替(月1回): 0.1時間 × 6ヶ月 = 0.6時間
新通知追加(2回): 0.5時間 × 2回 = 1時間
バグ修正: 0.5時間
------
合計: 約6時間

結果: 6ヶ月で14時間(約2日分)の差が生まれます。

注意: この数値は仮定に基づく例であり、実際の数値はプロジェクトの変更頻度やチーム規模に依存します。しかし、数値化しにくい定性的な価値(開発者のストレス軽減、バグ修正の迅速化、新規メンバーのオンボーディング効率化など)も含めると、実際の価値はさらに大きくなります。

Railsアーキテクチャ原則の具体的価値

1. Single Source of Truth(単一情報源の原則)

問題: 同じ情報が複数箇所に散らばっている

# ❌ 情報が分散
# APIコントローラー
maintenanceBannerText: "システムメンテナンスのため..."

# ビューファイル  
.banner システムメンテナンスのため...

# 設定ファイル
MAINTENANCE_BANNER = "システムメンテナンスのため..."

解決: 情報源を1箇所に集約

# ✅ 単一情報源
class BannerConfig
  module Maintenance
    TEXT = 'システムメンテナンスのため...'  # ← ここだけ
  end
end

# どこからでもこの値を参照
BannerConfig::Maintenance::TEXT

2. Separation of Concerns(関心の分離)

各コンポーネントが適切な責務を持つように設計します:

コンポーネント責務担当しないこと
BannerConfigバナー設定の管理HTTP処理、DB操作
Controllerリクエスト処理、データ準備設定値の決定、ビューロジック
View表示データ取得、ビジネスロジック

❌ 関心が混在した例

<!-- ビューでビジネスロジックを持っている -->
- if Rails.env.production? && Time.current.hour.between?(9, 17)
  - if some_complex_condition && user_has_permission?
    .notice 現在、多くの発送申請を...

✅ 適切に分離した例

# NotificationConfig: 設定管理
module ShippingDelay
  ENABLED = true
end

# Controller: データ準備
@shipping_notice_enabled = NotificationConfig::ShippingDelay::ENABLED

# View: 表示のみ
- if @shipping_notice_enabled
  .notice= @shipping_notice_text

3. Convention over Configuration(設定より規約)

Railsの命名規約に従うことで、設定ファイルを最小化:

# ディレクトリ構造がRailsの規約に従う
app/
├── lib/                    # アプリ固有ライブラリ
│   └── notification_config.rb
├── controllers/
│   └── application_controller.rb
└── views/
    └── shared/
        └── _shipping_notice.html.haml

# クラス名も規約に従う  
NotificationConfig::ShippingDelay::ENABLED

4. Fat Model, Skinny Controller

ビジネスロジックをモデル層に集約する原則です。ここでいう「Model」とは、データベースとやりとりするActiveRecordモデルだけでなく、アプリケーションのビジネスロジックを担当するapp/lib内のクラスなども含めた広い意味で捉えています:

❌ コントローラーが太い例

class MetadataController < ApplicationController
  def show
    # コントローラーでビジネスロジックを持っている
    show_notice = if Rails.env.production?
                    Time.current.between?(business_hours_start, business_hours_end)
                  else
                    true
                  end
    
    notice_text = if special_campaign_active?
                    "特別キャンペーン中!配送遅延のお知らせ"
                  else
                    "現在、多くの発送申請を..."
                  end
    
    render json: {
      showShippingNotice: show_notice,
      shippingNoticeText: notice_text
    }
  end
end

✅ 適切に分離した例

# app/lib/notification_config.rb(ロジック集約)
class NotificationConfig
  module ShippingDelay
    def self.enabled?
      return true unless Rails.env.production?
      Time.current.between?(business_hours_start, business_hours_end)
    end
    
    def self.text
      if special_campaign_active?
        "特別キャンペーン中!配送遅延のお知らせ"
      else
        "現在、多くの発送申請を..."
      end
    end
  end
end

# app/controllers/metadata_controller.rb(薄い)
class MetadataController < ApplicationController
  def show
    render json: {
      showShippingNotice: NotificationConfig::ShippingDelay.enabled?,
      shippingNoticeText: NotificationConfig::ShippingDelay.text
    }
  end
end

実装の進化過程

フェーズ1: 最小実装

# シンプルな定数から開始
module ShippingDelay
  ENABLED = true
  TEXT = '現在、多くの発送申請を...'
end

フェーズ2: 柔軟性の追加

# メソッド化で動的な制御を実現
# 注意:モジュール内でクラスメソッドを定義する際は def self.method_name を使用
module ShippingDelay
  def self.enabled?
    Rails.env.production? ? ENV['SHOW_SHIPPING_NOTICE'] == 'true' : true
  end
  
  def self.text
    ENV.fetch('SHIPPING_NOTICE_TEXT', default_text)
  end
  
  private
  
  def self.default_text
    '現在、多くの発送申請をいただいており、お届けまでにお時間を要しております。'
  end
end

フェーズ3: 高度な制御

# 条件による切り替えロジック
module ShippingDelay
  def self.enabled?
    return false unless base_enabled?
    return false if maintenance_mode?
    return false if business_hours_only? && !business_hours?
    true
  end
  
  def self.text
    case current_situation
    when :high_volume then high_volume_text
    when :maintenance then maintenance_text  
    else default_text
    end
  end
end

テストの書きやすさ

1ファイル修正の場合

# テストが複雑になりがち
RSpec.describe MetadataController do
  it '適切なレスポンスを返す' do
    # 複数の箇所をモックする必要
    allow(Rails).to receive(:env).and_return('production')
    allow(Time).to receive(:current).and_return(business_time)
    allow(ENV).to receive(:[]).with('SHOW_SHIPPING_NOTICE').and_return('true')
    
    get :show
    # テストの本質的な部分
  end
end

アーキテクチャ原則の場合

# テストがシンプル
RSpec.describe MetadataController do
  it '適切なレスポンスを返す' do
    # 設定クラスだけをモック
    allow(NotificationConfig::ShippingDelay).to receive(:enabled?).and_return(true)
    allow(NotificationConfig::ShippingDelay).to receive(:text).and_return('テスト用')
    
    get :show
    # テストの本質的な部分
  end
end

# 設定ロジックは別途独立してテスト
RSpec.describe NotificationConfig::ShippingDelay do
  describe '.enabled?' do
    context '本番環境で営業時間内の場合' do
      it 'trueを返す' do
        # NotificationConfigのロジックのみをテスト
      end
    end
  end
end

チーム開発での違い

コードレビューの観点

1ファイル修正の場合

# レビュー時の確認ポイント
# ❌ 複数ファイルの整合性確認が必要
# ❌ ハードコードされた値の妥当性判断が困難
# ❌ 将来的な拡張性が見えない

アーキテクチャ原則の場合

# レビュー時の確認ポイント  
# ✅ 設定クラスの設計が適切か
# ✅ 責務分離ができているか
# ✅ テストケースが網羅されているか

新規メンバーのオンボーディング

1ファイル修正の場合

# 新規メンバーが理解すべきこと
# - APIコントローラーのどこに設定があるか
# - ビューファイルのどこに対応する表示があるか  
# - 両者の同期を保つ方法
# - テストでの確認方法

アーキテクチャ原則の場合

# 新規メンバーが理解すべきこと
# - NotificationConfigクラスの役割
# - Railsのディレクトリ構造規約
# → 規約に従っているので理解しやすい

パフォーマンスへの影響

メモリ使用量

1ファイル修正の場合

# 各所で文字列を重複して持つ
"現在、多くの発送申請を..."  # APIコントローラー内
"現在、多くの発送申請を..."  # ビューファイル内
"現在、多くの発送申請を..."  # テストファイル内

アーキテクチャ原則の場合

# 文字列は1箇所で定義、参照は定数
NotificationConfig::ShippingDelay::TEXT  # 定数参照

実行速度

両者に大きな違いはありませんが、設定クラスを使う場合:

# 初回アクセス時にクラスがロード(数ms)
# 以降はメモリ上の定数を参照(0.001ms未満)
NotificationConfig::ShippingDelay::ENABLED

他の設計パターンとの比較

データベース設定パターン

# app/models/notification_setting.rb
class NotificationSetting < ApplicationRecord
  # メリット: 管理画面から変更可能
  # デメリット: DB負荷、複雑さ増加
end

環境変数パターン

# config/application.rb
config.show_shipping_notice = ENV.fetch('SHOW_SHIPPING_NOTICE', 'true') == 'true'

# メリット: デプロイなしで変更可能
# デメリット: 環境変数管理の複雑さ

設定ファイルパターン

# config/notifications.yml
shipping_delay:
  enabled: true
  text: "現在、多くの発送申請を..."

# メリット: YAML形式で管理しやすい  
# デメリット: 動的な制御が困難

今回のケースでは、設定の複雑さ変更頻度を考慮してapp/lib/のクラス定数が最適でした。

まとめ:本当の「簡単さ」とは

短期的視点 vs 長期的視点

観点1ファイル修正アーキテクチャ原則
初期実装簡単やや複雑
1回目の変更簡単簡単
5回目の変更面倒簡単
新機能追加困難簡単
バグ発生率高い低い
メンテナンス困難簡単

真の「簡単さ」の定義

1ファイル修正の「簡単さ」

簡単さ = 修正ファイル数の少なさ

アーキテクチャ原則の「簡単さ」

簡単さ = 理解しやすさ × 変更しやすさ × バグの少なさ

投資対効果の考え方

# 初期コスト
アーキテクチャ原則 = 1ファイル修正 + 1.5時間

# しかし、運用コストで逆転
# 6ヶ月後: アーキテクチャ原則の方が14時間削減
# 1年後: さらに28時間削減
# 2年後: さらに56時間削減

結論: 初期の1.5時間の投資で、年間56時間(約1週間)を節約できます。

実践的なアドバイス

いつアーキテクチャ原則を適用すべきか

適用を推奨するケース

  • ✅ 複数の場所で同じ情報を使用する
  • ✅ 将来的に設定項目が増える可能性がある
  • ✅ チーム開発で多くの人が触る
  • ✅ 長期運用が予定されている

1ファイル修正でも良いケース

  • ✅ 完全に1回限りの実装
  • ✅ プロトタイプや検証目的
  • ✅ 非常に小さなチーム(1-2人)
  • ✅ 短期プロジェクト(1-2ヶ月)

段階的な改善アプローチ

プロジェクトの状況によっては、段階的に改善していく方法も有効です:

# フェーズ1: まずは動かす(1ファイル修正)
showShippingNotice: true

# フェーズ2: 設定を外出し  
showShippingNotice: SHIPPING_NOTICE_ENABLED

# フェーズ3: 設定クラス作成
showShippingNotice: NotificationConfig::ShippingDelay::ENABLED

# フェーズ4: 動的制御追加
showShippingNotice: NotificationConfig::ShippingDelay.enabled?

最後に

「修正量を小さく納める」という考え方は、短期的には魅力的に見えます。しかし、ソフトウェアの価値は長期的な運用で決まります

Railsのアーキテクチャ原則は、数多くの開発者の経験から生まれた知恵の結晶です。これらの原則に従うことで、以下を実現できます:

  1. 予測可能なコード: 規約に従うことで、コードの在り処が予想できる
  2. 変更に強い設計: 要件変更に対して柔軟に対応できる
  3. チーム開発の効率化: 共通の理解基盤でコミュニケーションコストを削減
  4. バグの削減: 適切な責務分離でバグの発生と影響を局所化

目先の「簡単さ」に惑わされず、長期的な価値を見据えた設計判断をすることが、真のエンジニアリング力だと言えるでしょう。

次回プロジェクトで「1ファイル修正で済む」という提案があった際には、ぜひこの記事の内容を思い出してください。そして、本当にそれが「簡単」なのかを検討してみてください。

きっと、より良い設計の選択ができるはずです。

参考リンク