単一責任の原則(Single responsibility principle)

クラスを変更する理由は1つ以上存在してはならない。

アジャイルソフトウェア開発の奥義 第2版 p.122

モジュールはたったひとつのアクターに対して責務を負うべきである。

Clean Architecture 第7章 SRP: 単一責任の原則

以下のようなメソッドを考える。

def export_data(format = nil)
  data = export # データを集計して配列にしているとする
  return data if format.nil? # formatが未指定なら配列を返す

  # formatがCSVならCSV化して返す
  if format == :csv
    return convert_csv(data)
  end
  # ...
end

このメソッドが明らかに2つの使われ方が想定されている。1つはexport_data(nil)という呼び出しのケースで、戻り値を利用してさらなる集計処理などを行うことが予想される。もう1つはexport_data(:csv)という呼び出しのケースで、この場合はファイルに出力するなりユーザーにレスポンスとして返すなりすることが予想できる。

つまり、プログラム内部のデータとして戻り値を使いたい人(アクター)と、外部出力のデータとして戻り値を使いたい人(アクター)の2つのユーザーが想定されるわけである。

これは単に以下のように分けることで1つのユーザーに対して責務を担うだけでよくなる。

# 外部出力のデータとして戻り値を使いたい人(アクター)向け
def export_csv
  convert_csv(export_data) # エクスポートしたデータをCSV化させる処理
end

# プログラム内部のデータとして戻り値を使いたい人(アクター)向け
def export_data
  export # エクスポート処理
end

export_dataexport_csvから呼ばれているから、結局2つのユーザーに対して責務を担っているように思えるが、export_csvは「『プログラム内部のデータとしてexport_dataの戻り値を使って』外部出力のためのデータを作成している」ので、export_csvは「プログラム内部のデータとして戻り値を使いたい人(アクター)」ということになる。

もちろん、すべてのif分岐を別のメソッドに分けよと言っているわけではない。次のような例を考えれば明らかである。

# フォーマットが指定可能
def export_file(format = :csv)
  return convert_csv(export_data) if format == :csv
  return convert_tsv(export_data) if format == :tsv
  return convert_xls(export_data) if format == :xls
  # ...
end

このexport_fileメソッドは「具体的な中身は気にしないが指定したフォーマットで外部出力用のデータが欲しい人(アクター)」に対して責務を担っている。

メソッドでの例は以上にしてクラスの例に入る。

管理者とユーザーがいるブログホスティングサービスを考える。ユーザーは自分のブログの管理ができるし、管理者は全てのブログに対して強制非公開の操作ができる。

Blogクラスを作って以下のようにすることもできる。

class Blog
  # テーマを更新する
  def update_thema(thema_name)
    # ...
  end

  # ブログを強制非公開にする
  def freeze_blog
    # ...
  end
end

ActiveRecordパターン(PofEAA)を実装しているフレームワークであれば確実に発生することであるが、ユーザーのためのメソッドであるupdate_themaと管理者のためのメソッドであるfreeze_blogが同じクラスに存在してしまっている。

つまり、Blogクラスは管理者とユーザーというふたつの人(アクター)に対して責務を担ってしまっている。

この時点では問題はないように思えるが、次のような場合に名前の衝突が発生する。

  • 管理者として要注意のブログを一覧して監視するためマークする機能を追加したい。メソッド名はmarkにしよう。
  • ユーザーとしてコメントなどがあったときに通知が欲しいブログにマークする機能を追加したい。メソッド名はmarkにしよう。

この例だともっと適切なメソッド名を見つけられるかもしれないが、どちらも譲れないほど適切なメソッド名になる場合もある。そういう場合、無理に名前を分けるため、違和感のある名前になったり名前からどちらのメソッドなのか読み取れない状況になりやすい。

さらにタチの悪いケースは、共通する処理のようだったのに実際には偶然の一致でしかなかったというケースである。

ブログ側の画面では当然、ブログ名が表示される。管理側の画面ではブログ一覧という形でブログ名が表示されるとする。自然な例ではないが、ブログ名を取得するのに複雑な処理が必要でメソッドが用意されているとしよう。

class Blog
  # ブログのヘッダからも呼び出されているし、管理画面のブログ一覧からも呼び出されている
  def title
    # 複雑な処理を用いてブログ名を作っている
  end
end

このメソッドを実装して数ヶ月たったあと、「管理画面のブログ一覧で、ブログ名は20文字以内で表示して欲しい」という要求がでてきたとする。このとき、新米のプログラマが管理画面だけ見てBlog#titleを修正すれば良いと判断してしまったらブログ側の画面を壊すことになる。

こういった問題を回避するには、そもそも、用途(アクター)ごとにクラスを分けてしまえば良い。

# 管理者としてブログを操作する場合に使う
module Admin
  class Blog
    # ブログを強制非公開にする
    def freeze_blog
      # ...
    end

    # 要注意のブログをマークする
    def mark
      # ...
    end

    # 管理画面のブログ一覧に表示するブログ名
    def title
      # ...
    end
  end
end

# ユーザーとしてブログを操作する場合に使う
module User
  class Blog
    # テーマを更新する
    def update_thema(thema_name)
      # ...
    end

    # 通知が欲しいブログをマークする
    def mark
      # ...
    end

    # ブログのヘッダに表示するブログ名
    def title
      # ...
    end
  end
end

titleの構築処理が共通なら、親クラスを用意するかtitle構築用のクラスを用意して委譲する方法をとれば良い。

上記の例だと、管理者としてブログを管理するためにAdmin::Blogクラスを利用するが、管理者としてブログデータをダンプするならAdmin::Dump::Blogのように別のクラスを用意するべきかもしれない。単にシステム上のユーザーロールでクラスを分ければ単一責任の原則が達成できるわけではないということは注意しなければならない。

まとめ

単一責任の原則に則るためにはどういった使われ方を想定しているのか意識することが必要である。

ActiveRecordパターン(PofEAA)のフレームワークを利用しているのであれば、確実に単一責任の原則に違反したコードとなる。Railsのようにモジュールで分割が可能なら、用途(アクター)ごとに分ける方法も取れる。

# app/models/blog.rb
class Blog < ApplicationRecord
end

# app/models/admin_module/blog.rb
# 後ろにModuleをつけるのはActiveRecordのクラスと名前の衝突しないようにするため
module AdminModule
  class Blog < ::Blog
  end
end

# app/models/user_module/blog.rb
module UserModule
  class Blog < ::Blog
  end
end