Railsで実践するオープン・クローズド原則 〜 拡張に強く、変更に強い設計を目指して

# Railsで学ぶオープン・クローズド原則 〜 拡張に強く、修正に優しい設計を目指して

## はじめに

Railsアプリを開発していて、こんなことに悩んだことはありませんか？

- 「既存の処理に条件を追加したら、別の場所が壊れた」
- 「仕様変更が入るたびにコードを直接書き換える必要がある」
- 「if文がどんどん増えて可読性が落ちている」

これらは、設計原則「OCP（オープン・クローズド原則）」を意識することで改善できます。

本記事では、Railsでこの原則をどのように実践するかを、具象クラスと抽象化、Strategyパターンやモジュールの活用といった手法を交えて解説します。

---

## OCP（オープン・クローズド原則）とは

OCP（Open/Closed Principle）は、SOLID原則のひとつで、次のように定義されています：

> **「ソフトウェアの構成要素は拡張に対して開かれており、修正に対して閉じていなければならない」**

これは、既存のコードを変更せずに、**新しい振る舞いを追加できるように設計する**という考え方です。

---

## OCP違反の例：条件分岐の増殖

### ❌ Before

以下のように、配送方法によって送料を計算するロジックがあるとします。

```ruby
class ShippingFeeCalculator
  def initialize(order)
    @order = order
  end

  def calculate
    case @order.shipping_method
    when 'standard'
      500
    when 'express'
      1000
    when 'pickup'
      0
    else
      raise "Unknown shipping method"
    end
  end
end
```

- 条件が増えるたびにこのクラスを修正する必要がある
- 他の場所で同じ判定が必要になったら、ロジックを重複させるか共通化する必要がある

---

## OCPを意識した改善

### ✅ After：Strategyパターンの導入

```ruby
class ShippingFeeCalculator
  def initialize(order)
    @strategy = strategy_for(order.shipping_method)
  end

  def calculate
    @strategy.calculate
  end

  private

  def strategy_for(method)
    case method
    when 'standard' then StandardShipping.new
    when 'express'  then ExpressShipping.new
    when 'pickup'   then PickupShipping.new
    else
      raise "Unknown shipping method"
    end
  end
end

class StandardShipping
  def calculate
    500
  end
end

class ExpressShipping
  def calculate
    1000
  end
end

class PickupShipping
  def calculate
    0
  end
end
```

- 拡張（新しい配送方法の追加）は新しいクラスを追加するだけで済む
- `ShippingFeeCalculator` 自体は一切変更する必要がない

---

## RailsでOCPを実践するテクニック

### 1. モジュールを使って振る舞いを切り替える（Concernによる拡張の分離）

OCPにおける「変更に強くする」一つの方法として、モデルに直接書かず、関心ごとをモジュールに分離する手法があります。

たとえば、複数のモデルが「承認機能」を持つ場合、`approve!` の実装を各モデルに持たせると変更のたびに全モデルを修正する必要があります。

#### ❌ Before：それぞれのモデルに重複コード

```ruby
class Comment < ApplicationRecord
  def approve!
    update!(approved: true, approved_at: Time.current)
  end
end

class Article < ApplicationRecord
  def approve!
    update!(approved: true, approved_at: Time.current)
  end
end
```

#### ✅ After：Concernで共通の振る舞いに切り出す

```ruby
# app/models/concerns/approvable.rb
module Approvable
  extend ActiveSupport::Concern

  def approve!
    update!(approved: true, approved_at: Time.current)
  end
end

class Comment < ApplicationRecord
  include Approvable
end

class Article < ApplicationRecord
  include Approvable
end
```

- `approve!` の仕様が変わった場合でも、**Concern内を修正するだけで済む**
- 各モデルは拡張に開かれており、**修正はConcern内で完結するためOCPを守れる**

---

## OCPを守るメリット

| 観点               | OCPを守らない場合                            | OCPを守った場合                     |
|--------------------|-----------------------------------------------|--------------------------------------|
| 拡張性             | 条件を追加するたびに既存クラスを修正する     | 新しいクラスを追加するだけで済む     |
| テストのしやすさ   | 巨大なクラスをテストする必要がある           | クラスごとにテストが分離できる       |
| バグの発生率       | 修正によって他の処理を壊すリスクが高まる     | 既存コードを触らないので壊れにくい   |
| チーム開発         | 同じクラスを複数人で編集 → コンフリクト発生  | 新規クラス追加で並行開発しやすい     |

---

## まとめ

- OCPは「既存のコードを変更せずに拡張する」ための設計原則
- RailsでもサービスオブジェクトやStrategyパターン、モジュールを使えば実現できる
- 特に「今後拡張が予想される処理」にはOCPを意識すると、保守性・再利用性が大きく向上する

ぜひあなたのRailsアプリでも、OCPを意識した設計に取り組んでみてください。