RET

Reflection English Technology

<SOLID原則>単一責任の原則について

単一責任原則とは

オブジェクト指向のSOLID原則のうちの一つ。色々な説明のされ方がある

  • モジュールを変更する理由はたった一つだけであるべきある。
  • モジュールはたったひとつのアクターに対して責務を負うべきである
  • 1つのモジュール(クラス)は1つだけの責任を持たなければならない

単一責任はクラスの責務の話として言及されることが多いが、メソッドレベルでも単一責任を意識する必要がある。

単一責任と凝集度、結合度

単一責任を実現するクラスは、責務に沿ったデータとロジックをまとめた凝集性の高いものである必要がある。

逆にいうと、密結合低凝集なクラスを作ると単一責任を阻害することになる。

https://blog.cleancoder.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html

Robert C. Martinは上の記事で、以下のように単一責任に関して言及している。

Another wording for the Single Responsibility Principle is:

Gather together the things that change for the same reasons. Separate those things that change for different reasons.

If you think about this you’ll realize that this is just another way to define cohesion and coupling.

つまりは単一責任原則を実現することは、高凝集低結合を実現することと同義であるといえる。

単一責任にする重要性

複数の責任を持つモジュールは再利用しにくい。

このようなモジュールを再利用しようとすると、一つの修正によって予期せぬところでバグが発生する可能性を作り出してしまう。

単一責任を阻害する「共通化

単一責任原則の説明の中でよく一緒に語られるのが、過度な共通化だ。

過度な共通化によって、一つの変更が予期せぬバグを生みうる状況が作られてしまう。

例えばリマインドメールを送るクラスと、メルマガを送るクラスがあるとする。

その2つのクラス内で、現在の日付がメール配信日かどうかを判定するバリデーションロジックが存在していて、内容が全く同じである。

class RemindMail
  def send_mail
    send_remind_mail if delivery_date?
  end

  private

  def delivery_date?
    今日が配信日かどうか調べて、真偽値を返す
  end
end

class MailMagazine
  def send_mail
    send_mail_magazine if delivery_date?
  end

  private

  def delivery_date?
    RemindMail内のdelivery_dateと全く同じ内容
  end
end

これがDRY原則に反すると考え、バリデーションロジックを別のクラスに移動させた。

class DateValidator
  def initialize(date)
    @date = date
  end

  def perform
    今日が配信日かどうか調べて、真偽値を返す
  end
end

そして以下のように2つのクラスでDateValidatorを呼び出す

class RemindMail
  def send_mail
    send_remind_mail if DateValidator.new(Date.today).perform
  end
end

class MailMagazine
  def send_mail
    send_mail_magazine if DateValidator.new(Date.today).perform
  end
end

さて、このような状況の時に、リマインドメール側で配信日かどうかを調べるロジックを変更する必要が出てきた。そこでDateValidatorのperformメソッドをいじくることにした

class DateValidator
  def initialize(date)
    @date = date
  end

  def perform
    何かしらの変更を施す
  end
end

こうなったときに、変更の必要性がないメールマガジンの配信日チェックロジックにも影響が出てしまう。最悪の場合、気付かれることなくバグが放置され業務に大きな損害をもたらす。

このようにMailMagazineクラスはMailMagazineクラス自身の変更の必要性だけでなく、RemindMailクラスの変更の必要性にも影響を受けた。

つまり、「あるモジュール(クラス)が変更される理由は必ず一つでなければならない」とする単一責任原則に反していると言える。

DRYとは何か

同じコードを重複して書いてはいけないとする原則がDRY原則である。

しかし、DRYが「ソースコードのコピー&ペーストをしてはいけない」とする原則であると単純解釈するのは危険だ。

厳密にはDRYというのは「知識」や「意図」の二重化についての原則なのである。

コードが全く同じでも、そのコードが表現している知識や意図が異なっているのであれば、それらを共通化するべきではない。

「知識」とは何か

ここで言う「知識」の中の一つが「ビジネス知識」であるとミノ駆動本には書いてある。ビジネス知識はビジネスのなかで使われる概念で、先程のコードの例だと「リマインドメール」「メールマガジン」がビジネス概念と言える。

リマインドメールとメールマガジンという異なった知識に関するコードを共通化するべきではなかったのだ。

通化するタイミング(設計を決定するタイミング)

早い段階で設計を決定する必要はない。

  • 今設計を決定するメリット
  • 将来の変更によってかかってくるコスト

この2つをうまく比較衡量する。

ある機能を共通化するという設計判断に関して考えてみよう。もし、今共通化しなくても将来の変更による負担があまり大きくないのであれば、共通化の決定を遅らせてもいい。

Sustainable Web Development with Ruby on Rails」では同じ記述が3回出てきたら重複したコードを共通化すると書かれており、このルールを「rule of three」と呼んでいる。

同じコードが2箇所現れても共通化する判断を遅らせているいい例なのではないだろうか。

あるクラスやメソッドが単一責任かどうか確認するには?

  • 一文でクラスやメソッドについて説明してみる
    • and的なワードが入っていればそれは単一責任ではない
  • クラス内のパブリックメソッドが、クラスの責務を果たすためのものになっているか確認する
    • パブリックメソッドがそのクラスの責務を表現する

責任が単一かどうかの判断は難しい、、

どうしても単一責任かどうか迷うようであれば、先述の通り設計の判断を遅らせるのもあり。

ここで「アジャイルソフトウェア開発の奥義」からの抜粋を載せたい。

ここには必然的な法則がある。「変更の理由が変更の理由たるのは、実際に変更の理由が生じた場合だけである」という法則だ。変更の兆候もないのに単一責任の原則(SPR)を適用するのは賢明ではない。他の原則についても同様だ

これはとても重要な法則であるように思える。「単一責任にしないといけない」という原則に固執して、無理して単一責任を適用した結果、コードに無駄な複雑になるのであれば意味がない。

「最初の弾丸は甘んじて食らう」という方針のもと、最初は変更が起きないことを前提としてコードを書き、もしも変更が起きてしまったらその時に責務分割に着手するというのでもいいのかもしれない。

責任を考える上で役立ちそうな視点「アクター」

コードに対する変更を引き起こしているのは何なのだろうか?

それは「人」であるとRobert C. Martinと断言する。変更を要求する人がいるから、コードを書き換えないといけない。

However, as you think about this principle, remember that the reasons for change are people. It is people who request changes.

そして単一責任の原則は「人」に関する原則であると言う。

And this gets to the crux of the Single Responsibility Principle. This principle is about people.

https://blog.cleancoder.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html

この「人」を彼の著書Clean Architectureでは「アクター」と呼んでいる。

ここでは、変更を望む人たちをひとまとめにしたグループとして扱いたい。このグループのことをアクターと呼ぶことにしよう

(一人のユーザーやステークホルダにとどまらず、)特定の意図や変更要求を持ったユーザーやステークホルダの集合体がアクターと言える。

なおステークホルダとは「人」だけでなく、

システムが呼び出す外部Webシステム 機器組み込み系のプログラムにおける各種センサー バッチシステムにおけるスケジューラ

なども考えられるらしい。

https://www.ogis-ri.co.jp/otc/hiroba/others/OOcolumn/single-responsibility-principle.html

彼は単一責任の原則を「モジュールを変更する理由はたった一つだけであるべき」という内容だとしつつ、さらにその「変更される理由」は何かについて踏み込んだ。

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

つまりは、あるモジュール(クラス)に対して変更要求してくるアクターはたったひとつだけであるべきであるということである。

When you write a software module, you want to make sure that when changes are requested, those changes can only originate from a single person, or rather, a single tightly coupled group of people representing a single narrowly defined business function.

責務を見出すために、モジュールの機能を利用するアクターが誰(何)なのかを考えるというのは視野を広げるという意味で結構有益なのではないかと思う。

先程のリマインドメールと自動メルマガの例で再び考えてみると

  • リマインドメールを利用するのは、特定のイベントに参加申し込みをしていたユーザー
  • 自動メルマガを利用するのは、自動メルマガを受け取る設定をしているユーザー

といった感じになる。

リマインドメールと自動メルマガでアクターが異なるので、無理な共通化はやめておくべきと判断できそうだ。

このクラスに対応するアクターは何か?という問いは責務を考える上でいつか役立つかもしれない。

その他参考資料