🤔

単一責任の原則の例示は「多目的ユーティリティクラスを役割ごとに分ける」で良いのか

2024/09/03に公開

はじめに

社内の勉強会の中で単一責任の原則(以下、SRP)の説明があり

Utility.class
- ファイルの取得/削除
- 日付フォーマット
- ログ出力

というユーティリティクラスを

DateFormat.class
File.class
Log.class

とそれぞれの役割ごとに分離したことがSRPの適用だと言う説明がありました。

簡単な例示であり理解しやすくはありますが、私が以前に見た提唱者の例示はこれよりも理解し難い点がいくつかあるようなものでした。本記事は提唱者の例示が表していたものは何だったのか、そしてSRPの適切な例示は何なのかを改めて考えることが趣旨となります。世の達人プログラマーはもう既に通った道だと思いますが、私が理解しようとしたプロセスを書き綴ったものとご理解ください。

提唱者の例示

SRPの提唱者であるRobert C. Martin氏(Uncle Bob)の例示は以下の様になります。

public class Employee {
  public Money calculatePay();
  public void save();
  public String reportHours();
}

彼はこのクラスを変更する理由が3つあるためそれぞれのクラスに分けるべきだと主張しています。(変更理由:給与計算のルール、データベーススキーマ、勤務時間を報告する文字列の形式)

この例示に関する当時の私の疑問は以下のとおりです。

  • 従業員という単位で見れば高凝集になっており、オブジェクト指向としてもまとめる方が良いのではないか
  • reportHoursが勤務時間を報告する文字列の形式を変更理由とするのであれば、勤務内容を報告する処理もまた別クラスにしなくてはならずクラス数が膨大になるのではないか
  • saveは永続化処理を伴うため複数の条件によって変更理由がドメインロジックも合わせて2つになるのではないか

迷ったら公式ということでMartin氏のサイトを確認してみます。

http://www.butunclebob.com/
https://blog.cleancoder.com/

変更する理由とは

This principle is about people.

as you think about this principle, remember that the reasons for change are people.

Martin氏は上記のとおり、SRPが「人」に関するものだと記載しています。ここでの「人」は1人の人物、または厳密に定義された1つのビジネス機能を代表する1つの密接なグループとしており、各モジュールは1つのビジネス機能のニーズのみを担当すると主張しています。

私の最初の疑問である「従業員」は複数のニーズを持っています。Martin氏の説明を借りればcalculatePayは財務責任者であるCFO、reportHoursは監査責任者のCOO、saveはデータベースの破壊によりCOOの責任(少し大袈裟な例な気がしますが)にそれぞれ関与しており、誰に、あるいはどのビジネスグループに価値を提供しているかは同じ「従業員」でも異なります。財務責任者に対しての変更が監査責任者のロジックに影響を与えるべきではありません。

変更する理由が人に対してなのであれば、2つ目の疑問であるreportHoursに勤務内容を報告するreportWorkが加わったとしても必ずしも別クラスに分ける必要がないことが分かります。報告する文字列の形式が変更の本質的な理由ではなく、報告対象が同一の業務、報告先であれば2つのメソッドは同じクラスに配置しても変更理由は1つであると考えられます。

SRPは「実装」

最後の疑問はほぼ同じ回答がMartin氏の旧サイトにありました。

http://www.butunclebob.com/ArticleS.DavidChelimsky.MattersOfPrinciple.SrpIsAboutImplementation

"the Person is responsible for its own behaviour AND its persistence, therefore it has two responsibilities." I've finally put my finger on why I don't agree with this and I'm wondering what the rest of you have to say about this. To me, SRP is about implementation, not interface.

SRPは「インタフェース」ではなく「実装」であるとのことです。さらに続けて

In other words, if a Person object has a reference to some persister and delegates the save() message to it, then the Person is not implementing persistence, and therefore does not implement that responsibility. Person would only change if its behaviour changed. Changes to the persistence layer might affect the Person, but not its clients. Loose coupling is preserved.

Person(Employee)クラスは永続化の処理を委任していれば、自分自身は実装していないため責任はあくまでPersonのもつビジネスロジックのみに責任を持っています。永続化レイヤが変更された場合はPersonも変更はされますが、Personのクライアント、つまりPersonのビジネスロジック自体には影響がありません。このこともあくまでPersonが責任を果たすべき対象のみにフォーカスしていることが分かります。

例示は簡略化して良いのか?

Martin氏の思想は最初のユーティリティクラスの例示からは読み取ることが出来ません。初見だと理解しづらいところがありますが、大切なエッセンスが含まれています。理解しやすい様に簡単な例示にしてくださったのだとは思いますが、私はMartin氏の例示をそのまま使って説明するのが一番良いと思います。

おわりに

改めてMartin氏のブログを確認しながらSRPに向き合うことが出来て良い知見になりました。理解しやすさと本来の意図を考えながら例示の出し方を考えつつ、元となる思想の理解に努めていこうと思います。本記事において異なる解釈がある場合などございましたらご指摘いただけると幸いです。

Discussion