単一責任の原則 とは?

「単一責任の原則」(Single Responsibility Principle: SRP) とは、SOLID原則の一つであり、「1つのクラスは1つの責任のみを持つべきである」という原則です。

ここでいう「責任」とは、「変更する理由が1つだけある」という意味です。つまり、あるクラスに対する変更が複数の理由から発生する場合、そのクラスは複数の責任を持っていることになります。SRPに従えば、1つのクラスは1つの変更理由だけを持つようになります。

なぜ 単一責任の原則 が重要なのか?

単一責任の原則を守らない場合、以下のような問題が生じます。

  • 複数の機能が絡み合っているため、変更時に予期しない副作用が発生しやすくなる。
  • コードが複雑化し、可読性・保守性が低下する。
  • 再利用が難しく、重複コードが増える。

一方、単一責任の原則を守れば、以下のようなメリットがあります。

  • コードがシンプルになり、変更や修正が容易になる。
  • クラスが明確な役割を持つため、理解しやすくなる。
  • テストが容易になり、品質が向上する。

解説

では具体的な例を通じて、この原則を学びましょう。

店舗の売上レポートを作成して画面に表示し、さらにファイルに保存するクラスを考えます。

修正前のコード

class SalesReport
  def initialize(data)
    @data = data
  end

  def generate_report
    report = "売上レポート\n"
    @data.each do |item|
      report += "#{item[:name]}: #{item[:sales]}円\n"
    end
    report
  end

  def display
    puts generate_report
  end

  def save_to_file(filename)
    File.write(filename, generate_report)
  end
end

問題点

  • 上記のクラスは3つの責任を持っています:
    1. レポートの生成
    2. コンソールへの表示
    3. ファイルへの保存
  • 例えば、レポートのフォーマットを変更するだけでこのクラスを修正する必要があり、それが表示や保存にも影響を与える可能性があります。

修正後のコード

# レポート生成の責任のみを持つ
class SalesReport
  def initialize(data)
    @data = data
  end

  def generate_report
    report = "売上レポート\n"
    @data.each do |item|
      report += "#{item[:name]}: #{item[:sales]}円\n"
    end
    report
  end
end

# レポート表示の責任を持つ
class ReportPrinter
  def self.print(report)
    puts report
  end
end

# レポート保存の責任を持つ
class ReportSaver
  def self.save(report, filename)
    File.write(filename, report)
  end
end

解決された問題

  • 各クラスが明確に1つの責任を持つようになり、変更時の影響範囲が明確になりました。
  • 表示方法や保存方法が変更されても、レポートの生成部分に影響が及ばなくなりました。
  • 各クラスを個別にテスト可能なため、品質も向上します。

まとめ

単一責任の原則を守ることで、各クラスがシンプルでわかりやすくなり、変更や修正が容易になります。保守性や拡張性が向上し、結果的にコードの品質が向上します。

練習問題 (1)

次のコードは、ユーザー認証とユーザー情報のメール送信を行うクラスです。

修正前のコード

class UserManager
  def initialize(user)
    @user = user
  end

  def authenticate(password)
    @user.password == password
  end

  def send_user_info_email
    email_body = "ユーザー名: #{@user.name}\nメールアドレス: #{@user.email}"
    EmailService.send(@user.email, "ユーザー情報", email_body)
  end
end

修正後のコード

class UserAuthenticator
  def initialize(user)
    @user = user
  end

  def authenticate(password)
    @user.password == password
  end
end

class UserInfoMailer
  def initialize(user)
    @user = user
  end

  def send_user_info_email
    email_body = "ユーザー名: #{@user.name}\nメールアドレス: #{@user.email}"
    EmailService.send(@user.email, "ユーザー情報", email_body)
  end
end

解説

元のコードでは、ユーザーの認証とメール送信の2つの異なる責任が同じクラスに含まれていました。そのため、例えばメールの内容や送信方法を変更する場合にも、認証機能を持つクラスを修正する必要があり、不要な影響を与えてしまう可能性がありました。

修正後のコードでは、それぞれの責任を専用のクラスに分割しています。「UserAuthenticator」は認証処理だけを担当し、「UserInfoMailer」はメール送信だけを担当しています。これにより、認証の仕組みを変更してもメール送信機能には影響がなくなり、その逆も同様です。

練習問題 (2)

次のコードは、注文の処理と請求書の作成を1つのクラスにまとめています。

修正前のコード

class Order
  def initialize(items)
    @items = items
  end

  def process_order
    # 注文処理のロジック
    puts "注文が処理されました"
  end

  def generate_invoice
    invoice = "請求書\n"
    total = 0
    @items.each do |item|
      invoice += "#{item[:name]}: #{item[:price]}円\n"
      total += item[:price]
    end
    invoice += "合計: #{total}円"
    invoice
  end
end

修正後のコード

class Order
  def initialize(items)
    @items = items
  end

  def process_order
    # 注文処理のロジック
    puts "注文が処理されました"
  end
end

class InvoiceGenerator
  def initialize(items)
    @items = items
  end

  def generate_invoice
    invoice = "請求書\n"
    total = 0
    @items.each do |item|
      invoice += "#{item[:name]}: #{item[:price]}円\n"
      total += item[:price]
    end
    invoice += "合計: #{total}円"
    invoice
  end
end

解説

元のコードでは、注文の処理と請求書の生成という2つの別々の責任が1つのクラスにまとめられていました。そのため、注文処理のロジックを変更すると、請求書の生成部分に予期せぬ影響が及ぶ可能性がありました。

修正後は、「Order」クラスが注文処理のみに責任を持ち、「InvoiceGenerator」クラスが請求書作成のみに責任を持つように分割しました。これにより、注文処理と請求書生成が独立し、それぞれの変更が他方に影響を与えなくなりました。保守性・拡張性が向上し、今後の変更が容易になりました。