オブジェクトの広場はオージス総研グループのエンジニアによる技術発表サイトです

オブジェクト指向

単一責任の原則(Single responsibility principle)について、もう一度考える

オージス総研 オブジェクトの広場
菅野 洋史
2021年5月26日

簡単なようで一番難しい、オブジェクト指向設計の基礎にして真髄ともいえる、SOLID原則の筆頭「単一責任の法則」について深堀りしていきます。

はじめに

オブジェクトの広場をご覧の皆様ならば、「SOLID原則」という言葉を聞いたことがあるかもしれません。

SOLIDとは、以下の5つのソフトウェア設計原則を並べたバクロニムです。

  1. Single Responsibility Principle:単一責任の原則
  2. Open/closed principle:オープン/クロースドの原則
  3. Liskov substitution principle:リスコフの置換原則
  4. Interface segregation principle:インターフェース分離の原則
  5. Dependency inversion principle:依存性逆転の原則

ソフトウェアエンジニアが知っておくべき設計原則のセットとして、Clean Architecture や アジャイルソフトウェア開発の奥義等の書作で有名な、Robert C. Martin氏(通称:ボブおじさん)によって書かれた論文が初出です。

今回はSOLIDの筆頭、単一責任の原則について考えてみたいと思います。

Web上では、SOLID原則解説記事(日本語)を月数本程度はコンスタントに発見できますが、単一責任については結構さらっと流されることが多いです。実は、これが簡単なようで一番難しい。そしてオブジェクト指向設計の真髄ともいえる部分なのです。今回は単一責任の法則についてオブジェクト指向おじさんとして深堀りしていきます。

単一責任の原則 ( Single Responsibility Principle ) とは

wikipediaによると以下のような解説が記述されています。

1つのクラスは1つだけの責任を持たなければならない。

すなわち、ソフトウェアの仕様の一部分を変更したときには、それにより影響を受ける仕様は、そのクラスの仕様でなければならない。

クラスには責務があるし、それは明確に一つであるべき。確かにそうですねと読み飛ばしてしまいそうです。あまりにも当たり前だし、SOLIDのSを埋めるために無理くり持ってきたのではないのか、と。しかし文章の後半の意味をじっくり考えると悩ましいのです。素直に解釈すると「ソフトウェアの仕様の一部分を変更したときに、特定の1つのクラスの仕様が変更される」ということですよね。

でも、このようにソフトウェア仕様の一部とクラスが一対一にマッピングされている状況が想像できません。例えば仕様の変更により、画面の表示項目が変われば、画面ロジック、永続化ロジック、ドメインのコアロジック、その他変換層、いろいろな箇所のクラスが変更されるのが普通ですよね。

そもそも「ソフトウェア仕様の一部ってどういう単位よ? そんなの仕様のまとめ方や状況によって変わってくるだろ!!」ってなりませんか?

ボブ自身が書いたクリーンアーキテクチャの記述を読んでみると、以下のような補足解説があります。

モジュールを変更する理由はたったひとつだけであるべきである。

この文章も素直に解釈すると、変更理由というものがあったとして、あるクラスは一つの変更理由しか持てないと読めてしまいます。これも理解しづらいです。

そもそも変更理由という言葉に曖昧なニュアンスを感じませんか。モジュールを変更する理由なんて無限にありそうです。もし変更理由が一つのIssueを指すならクラスは一回しか変更できないことになってしまいます。まあこれはあまりに無茶な解釈でしょう。

おそらく、ソフトウェア仕様の一部やモジュールの変更理由という言葉の定義が重要で、それをきちんと理解することによって単一責任の意味が見えてくると思います。

さらにクリーンアーキテクチャの当該箇所を読み進めていくと以下のような記述があります。

モジュールはたったひとりのユーザーやステークホルダーに対して責務を負うべきである。

モジュール/クラスは個人用ってこと? これもちょっとわからない。もう一声!

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

ここまで読んで、ようやくボブおじさんの意図が見えてきました。

つまり、こういうことでしょうか。

  • アクターがモジュールの変更理由になる
  • だから、一つのクラスが責務として対応すべきアクターは1つであるべき → 単一責任

今度はアクターが何か?ということを追求する必要がありますね。

アクターとは

アクターは色々な意味で使われますが、UMLでの定義は以下の通り。(ボブおじさんはUMLCaseツールの開発もしてた人なので当然UMLにおける用法を意識していると思います)

https://www.ogis-ri.co.jp/otc/swec/process/am-res/am/artifacts/useCaseDiagram.html

アクターとはシステムとの相互作用においてある役割を果たす人や組織や外部システムで、線で描いた人型で表されます

典型的なアクターは以下のようなものです。

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

上はもちろんシステムの範囲の捉え方にもよります。上記の例でもスケジューラをシステム内の存在と捉えればアクターではなくなります。その場合は、そのバッチ処理にデータを供給する外部の連携先等がアクターになるのかもしれません。

ユースケースとは

アクターとシステムの相互作用、つまり利用者(アクター)が画面上で一連の情報を入力したりシステムが外部API(アクター)を呼び出すといった一連の手順は、ユースケースやストーリーという名前で呼ばれることがあります。先程のリンクでは、「ユースケースとはアクターにとって重要な価値を持つアクションのシーケンス」と説明されていますね。

典型的なユースケースは以下のようなものです。

  • ユーザがシステムにログインする
  • 申請者(アクター)が申請書類を申請する
  • 管理ユーザが日時レポートを出力する

クラスの責任

設計原則について説明しているのに、いつの間にか要求に関する話題になっています。アクターやユースケースは計計やコーディングのレベルでは関係無いように思えます。しかしこれが単一責任の法則という設計実装レベルの判断には必要だというのがボブの主張です。

プログラムのすべての部分はアクターとシステムの相互作用(=ユースケース)の中で呼び出されてなんらかの役目を果たします。一見、ユーザや外部システムから直接呼び出されていないクラスでも、呼び出し階層を遡っていけば最終的にはシステム外部のアクターとの相互作用に必ずたどり着くはずです。どんなクラスにもそれに対するアクターが存在し、そのクラスの責務はそのアクターの要求から生み出されています。

単一責任原則とは、そのクラスに対するアクターの数を絞る事を示しています。では、なぜアクターの数を絞る必要があるのでしょうか。以下のような複数のアクターに利用されるクラスを考えてみましょう。

  1. 特定のアクターAのためにそのクラスのメソッドαを変更する際に、インスタンス変数への書き込みが発生する
  2. 別のアクターBのために呼び出されるメソッドβで、そのインスタンス変数を参照している

アクターAに対する変更要求があって、それに対応するメソッドαを修正したが、その際にメソッドβも参照するインスタンス変数を書き換えるロジックを記述したとします。クラスレベルの単体テストでは問題なかったとしても、それが影響してアクターBに対する振る舞いが変わり、意図しないバグになるかもしれません。つまり、このクラスを変更する場合は関係する全てのアクターに対する影響を考慮する必要があります。

単一責任の法則とは、クラス(コードの一行一行)に対応するアクターを一種類に絞ることによってそのような面倒を引き起こすのを防ぐものです。上記の例ではクラスをメソッドαとメソッドβを持つそれぞれのクラスに分割すべきというわけです。

つまり「あるモジュールを変更する理由(=アクターからの変更要求)はたったひとつだけであるべきである」ということになります。こうすればシステムを要求変更の影響を特定の部分に封じ込めて他に影響を与えないロバストな構造にすることが出来ます。

クラスに対してアクターを一つ割り当てるのは難しいのでは

しかし、すべてのクラスに対して、アクターの割り当てを一つだけに制限するのは一見不可能に思えます。普通に設計していれば複数のユースケースで共通で利用されるドメインモデルやロジッククラスが出てきます。先ほどの例ならば、メソッドαとβで共通利用されていた変数はどこに置けば良いのかという話です。

ここで重要なのは、クラスに対して特定のユーザや特定のシステムが紐づくのではなくアクターが紐づくという視点です。共通のデータや共通ロジックに対応するアクターが存在すればよいのです。アクターを分類して共通化することによって、クラスもそれに対応した形に分割できます。

UMLではアクターを汎化させることによって、上記の分類を表現できます。

アクターの分類

分類されたアクターに対応するクラスを整理するとこんな感じです。

分類したアクターに対応するクラス

この例では、アクター「共通ユーザ」とシステム間の振る舞いになんらかの変更があれば、それに対応するクラス「共通ユーザ向けロジック」を変更し、アクター「管理者ユーザ」からの変更要求があれば、それに対応したクラスの変更を行うことになります。

アクターの汎化、なんとなくUMLの概念上の絵空事だと思われがちです。しかし実際にはアクターを分類し共通性を整理する基本的な作業です。UMLを利用していなくてもユーザなり外部システムに対応する共通部分と差異がある部分を整理する必要はあるでしょう。

ボブの例

「プログラマが知るべき97のこと」における、アンクルボブの例では従業員クラスを三つのクラスに分割する例が載っています。

public class Employee {
    public Money calculatePay() ...
}
public class EmployReporter {
    public String reportHours(Employee e) ...
}
public class EmployeeRepository {
    public void save(Employee e) ...
}

本文から引用すると

  • calculatePayメソッドには、 給与計算に関わるビジネスルールの変更を加える必要があります。
  • reportHoursメソッドには、レポートのフォーマットが変わる度に、変更を加える必要があります。
  • saveメソッドには、DBAがデータベーススキーマを変更する度に、変更を加える必要があります。

と説明されています。

それぞれのメソッドでは別のユースケースとアクターが存在し、そこから変更要求が生まれるため一つのクラスでは対応せず複数クラスへ分割する必要があるということです。急にDBAが出てきたり、給与計算とレポートフォーマットの変更タイミングは結局一心同体ではないか?といった多少の疑問はありますが。

ここまでのアクターに注目した説明に合わせるならば、

  • calculatePayメソッドは、システム共通の利用者アクター(=給与計算を必要とする人たち)が利用する
  • reportHoursメソッドは、レポート出力サービスが利用する
  • saveメソッドは、データベースシステムと相互作用する

といった感じでしょう。

さらにこのエッセイでは、この分割だけでは依存関係の問題が発生すると述べていて、依存関係逆転の原則(Dependency Inversion Principle)を適用する必要を示唆していますが、それはまた別のお話です。

ドメインモデルにおける単一責任原則

システムの中心的な関心事項となるデータ構造とそれに関係するロジック、いわゆるドメインクラスやエンティティクラスは、単一責任原則の範疇でしょうか。

ほとんどのドメインクラス・エンティティクラスはシステム共通のデータ構造や概念であり、アクターもシステム共通アクターになります。単一責任を意識してすでに存在するクラスをさらに分割する機会は少ないように思えます。

しかしドメインモデル内でもアクターが明確に異なることがあれば、単一責任原則を意識することによって、変更に強いモデルにすることができます。

モデル

上記モデルでは、同じシステム内の売上明細と商品カタログで同名の商品クラスが出現していますが、これらは本当に同じものでしょうか。

モデリングに慣れた人ならば、以下のような観点で別々のクラスにするでしょう。(おそらく共通の商品クラスも必要になり三つのクラスになりそうです)

  • 売上明細に含まれる商品の情報は、売上時点でのスナップショット
  • 商品カタログに含まれる商品の情報は、商品のマスタ情報

逆にこれらを一つの商品クラスに統合してしまうと、ドメインモデルもそれ以外の周辺部も複雑化して変更に弱いシステムになってしまいます。

しかし、このような判断はモデリングのコツといった感もあり簡単に根拠を説明するのは難しかったりします。

ここでアクターと単一責任原則を意識することによって、上記のモデリングに根拠を与えることができます。

つまり売上明細の商品と商品カタログの商品は、アクターが違うので分割すべきという言い方ができます。

  • 売上明細に含まれる商品を利用するのは、受発注機能を利用するユーザ・外部システムなど
  • 商品カタログを利用するのは、商品カタログをみるユーザ・外部システムなど

アクター「カタログユーザ」起点での変更要求、例えば「カタログの商品紹介になんらかの追加情報が欲しい」があった場合、カタログの商品クラスやその周辺部を変更しても、売上や売上明細を利用するアクターには変更の波及が及びません。

要求仕様と設計について

システムの変更要求はアクターが起点になることがほとんどでしょう。だから単一責任の法則では、クラスへの責務割り当てや分割の指針としてアクターやユースケースの分析が有用なことを示しています。

逆にアクターやユースケースといった要求仕様を意識せず、設計や実装時の判断だけで責務を定義するなら、各モジュールの責務分割に根拠がなくなります。不必要なレベルまで柔軟性を確保する過剰設計をしたり、目的もなくデザインパターンを乱用したりとプログラムを読みづらく脆弱にする原因になります。

ソースコードの一行一行は必ずなんらかのアクターに結びついています。クラスやモジュールの分割は設計時やコーディング時のミクロな判断で行うものではなく、要求分析と一心同体だったのです。これはユースケース・ユーザーストーリー駆動開発の数多くの利点の一つにもなりそうです。

結論

長々考えてきたのですが、結論としては以下のとおりです。

  • 単一責任の法則は、「クラスには一つのアクター」という原則である
  • アクターはシステムに対する変更要求の出発点である
  • アクターとクラスの分轄を連動させることによって変更箇所を局在化して仕様変更に強いシステムにできる
  • 要求分析と設計は一心同体

というわけでSOLID原則の中でも一番地味な単一責任の原則について深堀りをしてしてみました。