オープン・クローズドの原則 とは?

オープン・クローズドの原則(Open-Closed Principle, OCP)は、ソフトウェア設計におけるSOLID原則のひとつであり、「ソフトウェアのコンポーネントやクラスは、拡張に対しては開いていて(Open)、修正に対しては閉じている(Closed)べきである」という考え方です。

言い換えると、既存のコードを変更せずに、新しい機能や振る舞いを追加できるように設計すべきだという原則です。

なぜ オープン・クローズドの原則 が重要なのか?

オープン・クローズドの原則が重要なのは、以下のような理由からです。

  • コード変更によるバグの導入を防ぎ、システムの安定性を保つ
  • 既存コードを変更することなく機能を追加できるため、メンテナンス性が向上する
  • コードの再利用性が高まり、開発速度が向上する
  • シンプルで柔軟な設計を促し、変更に強いシステムを作ることができる

解説

「注文金額に対して割引を適用する」という機能を例に、オープン・クローズドの原則を実際にRubyコードで説明しましょう。

修正前のコード

class Order
  attr_reader :amount, :customer_type

  def initialize(amount, customer_type)
    @amount = amount
    @customer_type = customer_type
  end

  def discount
    case customer_type
    when :regular
      amount * 0.1
    when :premium
      amount * 0.2
    else
      0
    end
  end
end

order = Order.new(1000, :regular)
puts order.discount #=> 100.0

問題点

  • 新しい顧客タイプが追加された場合、discountメソッドを毎回変更する必要があります。
  • これはオープン・クローズドの原則に反します(修正に対して閉じていない)。

修正後のコード

class Order
  attr_reader :amount, :discount_strategy

  def initialize(amount, discount_strategy)
    @amount = amount
    @discount_strategy = discount_strategy
  end

  def discount
    discount_strategy.calculate(amount)
  end
end

# 割引戦略インターフェース
class DiscountStrategy
  def calculate(amount)
    raise NotImplementedError, 'サブクラスで実装してください'
  end
end

class RegularDiscount < DiscountStrategy
  def calculate(amount)
    amount * 0.1
  end
end

class PremiumDiscount < DiscountStrategy
  def calculate(amount)
    amount * 0.2
  end
end

regular_order = Order.new(1000, RegularDiscount.new)
puts regular_order.discount #=> 100.0

premium_order = Order.new(1000, PremiumDiscount.new)
puts premium_order.discount #=> 200.0

解決された問題

  • 新しい割引タイプを追加する場合でも、discountメソッドを変更する必要がありません。
  • 代わりに新しい割引クラスを作成するだけで拡張可能になり、既存コードに影響を与えません。
  • これにより、オープン・クローズドの原則が満たされ、システムはより柔軟で保守しやすくなりました。

まとめ

オープン・クローズドの原則を守ることで、コードの変更による影響を最小限に抑え、保守性・拡張性を高めることができます。既存のコードを修正することなく、戦略パターンなどを利用して機能を追加できる仕組みを構築することが、この原則の本質です。

以下の練習問題でさらに理解を深めましょう。

練習問題 (1)

以下のコードは通知を送信するクラスです。オープン・クローズドの原則に反しているため、改善してください。

修正前のコード

class Notification
  def send(type, message)
    case type
    when :email
      puts "Email notification: #{message}"
    when :sms
      puts "SMS notification: #{message}"
    else
      raise "Unknown notification type"
    end
  end
end

修正後のコード

class Notification
  def initialize(sender)
    @sender = sender
  end

  def send(message)
    @sender.send(message)
  end
end

class Sender
  def send(message)
    raise NotImplementedError, 'サブクラスで実装してください'
  end
end

class EmailSender < Sender
  def send(message)
    puts "Email notification: #{message}"
  end
end

class SmsSender < Sender
  def send(message)
    puts "SMS notification: #{message}"
  end
end

email_notification = Notification.new(EmailSender.new)
email_notification.send("Hello Email")

sms_notification = Notification.new(SmsSender.new)
sms_notification.send("Hello SMS")

解説

修正前のコードは、通知タイプが追加されるたびにsendメソッドの修正が必要であり、OCPに反していました。修正後はSenderという抽象クラスを作成し、各通知タイプをサブクラスとして定義することで、新しい通知タイプを追加する際に既存コードを変更する必要がなくなりました。

練習問題 (2)

以下のコードは支払い方法を処理するクラスです。オープン・クローズドの原則に従って改善してください。

修正前のコード

class Payment
  def process(type, amount)
    case type
    when :credit_card
      puts "Processing credit card payment of #{amount}"
    when :paypal
      puts "Processing PayPal payment of #{amount}"
    else
      raise "Unsupported payment type"
    end
  end
end

修正後のコード

class Payment
  def initialize(processor)
    @processor = processor
  end

  def process(amount)
    @processor.process(amount)
  end
end

class PaymentProcessor
  def process(amount)
    raise NotImplementedError, 'サブクラスで実装してください'
  end
end

class CreditCardProcessor < PaymentProcessor
  def process(amount)
    puts "Processing credit card payment of #{amount}"
  end
end

class PaypalProcessor < PaymentProcessor
  def process(amount)
    puts "Processing PayPal payment of #{amount}"
  end
end

credit_payment = Payment.new(CreditCardProcessor.new)
credit_payment.process(500)

paypal_payment = Payment.new(PaypalProcessor.new)
paypal_payment.process(800)

解説

修正前は新しい支払い方法が増えるたびにprocessメソッドを修正する必要があり、OCP違反でした。修正後は各支払い方法をサブクラス化して抽象化することで、新たな支払い方法を簡単に追加でき、既存のコードを変更する必要がなくなりました。これにより、拡張性が向上しました。