単一責任の原則 とは?
「単一責任の原則」(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つの責任を持っています:
- レポートの生成
- コンソールへの表示
- ファイルへの保存
- 例えば、レポートのフォーマットを変更するだけでこのクラスを修正する必要があり、それが表示や保存にも影響を与える可能性があります。
修正後のコード
# レポート生成の責任のみを持つ
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」クラスが請求書作成のみに責任を持つように分割しました。これにより、注文処理と請求書生成が独立し、それぞれの変更が他方に影響を与えなくなりました。保守性・拡張性が向上し、今後の変更が容易になりました。