【ソフトウェア設計】「マネージャー」は単一責任の原則を破るのか?――フローを管理するクラスの「責任」を考える
「変更理由が1つ」という言葉の難しさ
ソフトウェア設計を学んでいると、必ずと言っていいほど「単一責任の原則(Single Responsibility Principle: SRP)」という言葉に出会います。
「クラスを変更する理由は1つでなければならない」という教えですが、実際の開発現場でこれを厳密に当てはめようとすると、ふと手が止まってしまうことがあります。
たとえば、「データを作る」「データを整形する」「データを納品する」という一連の処理があるとします。
「作るクラス」「整形するクラス」「納品するクラス」と分けるところまでは、単一責任としてイメージしやすいかもしれません。
しかし、それらの部品を組み合わせて「順番に実行を依頼する役(パイプ役)」が必要になります。
誰かが「作って、整形して、納品してね」と指示を出さなければ、システムは動きません。
では、この「色々な人にお願いするフローを管理するクラス」の責任とは何なのでしょうか。
作る人が変わったり、整形ルールが変わったり、納品先が変わったりするたびに、このクラスは影響を受けるのでしょうか。だとしたら、それは単一責任とは呼べないのではないか。結果として「Manager」や「Coordinator」といった曖昧な名前の、何でも屋になってしまうのではないか。
今回は、この多くの開発者が抱く「フロー管理の責任」に対する違和感について、少し違う角度から考えてみたいと思います。
「お願いするだけ」のクラスは、誰の変更に影響されるのか
この問題を考えるために、少しだけコードを書いてみましょう。
以下は、レポートの作成から納品までを管理する、架空のC#のクラスです。
public class ReportManager
{
private readonly ICreator _creator;
private readonly IFormatter _formatter;
private readonly IDeliverer _deliverer;
public ReportManager(ICreator creator, IFormatter formatter, IDeliverer deliverer)
{
_creator = creator;
_formatter = formatter;
_deliverer = deliverer;
}
public void ProcessReport()
{
// 1. 作る
var rawData = _creator.Create();
// 2. 整形する
var formattedData = _formatter.Format(rawData);
// 3. 納品する
_deliverer.Deliver(formattedData);
}
}
この ReportManager のコードを見ると、非常にシンプルで、ただ他のクラスに処理を委譲(お願い)しているだけであることがわかります。
ここで疑問となるのが、「このクラスの変更理由は本当に1つなのか?」という点です。
一見すると、以下のような複数の変更理由を抱えているように見えます。
データの作り方が変わったとき
整形のフォーマットが変わったとき
納品先がメールからチャットに変わったとき
これらがすべて ReportManager の変更理由になってしまうのだとしたら、確かに単一責任の原則は破綻しているように感じられます。
「フローを知っていること」自体を一つの責任と捉えてみる
このジレンマを解きほぐす一つの考え方は、「何が変更されたら、このクラスを書き換える必要があるのか」を具体的に想像してみることです。
先ほどのコードでは、インターフェース(ICreator など)を使って依存関係を注入しています。
この構造のおかげで、実は「納品先がメールからチャットに変わった」としても、ReportManager のコードを書き換える必要はありません。外部から渡す IDeliverer の中身(部品)を入れ替えるだけで済むからです。
つまり、「作る」「整形する」「納品する」というそれぞれの具体的な作業内容が変わっても、このクラス自体は変更されないという状態を作ることができます。
では、このクラスが変更されるのはどんな時でしょうか。
それは、「納品する前に、上司の承認チェック工程を入れたい」といった**「フロー(手順)そのものが変わった時」**です。
そう考えると、このクラスの本当の責任が見えてきます。
それは「データの作成から納品までの手順(ワークフロー)を知り、進行すること」です。
「色々な人にお願いする一連の流れ」それ自体を、立派な一つの責任(ドメイン)として捉えることができるのではないか、と私は考えています。
「作業をする責任」と「調整する責任」は全く別物かもしれない
この視点をさらに深掘りしてみると、ソフトウェアにおける「責任」には、大きく分けて2つの種類があることに気づきます。
作業者(Worker)の責任:実際に計算をしたり、文字列を加工したり、DBに保存したりする実作業の責任。
調整役(Coordinator/Orchestrator)の責任:作業者たちを適切な順番で呼び出し、全体の流れを進行する責任。
単一責任の原則を考えるとき、この2つを混ぜてしまうと設計が混乱しがちです。
いわゆる「Manager」クラスが忌み嫌われがちなのは、名前が曖昧だからというだけでなく、「進行役のはずなのに、自分でもデータの加工(実作業)を始めてしまう」ことが多いからかもしれません。
調整役は、徹底して「誰かに指示を出すだけ」に徹することで、その純粋性を保つことができます。
そして、もし「Manager」という名前が曖昧で気持ち悪いと感じるなら、それは設計を見直す良いサインかもしれません。
たとえば、特定の業務フローを表現しているなら ReportGenerationUseCase(レポート生成ユースケース)や ReportPublishingWorkflow(レポート発行ワークフロー)といった名前に変えてみると、「このクラスの責任は、この一連のフローを成立させることである」という輪郭が、よりくっきりと見えてくるのではないでしょうか。
責任とは「誰の要望でコードを書き換えるか」という視点
単一責任の原則で言われる「変更理由は一つであるべき」というのは、「クラスを極限まで小さく分割しなさい」というルールではなく、「変更の波及をコントロールするための知恵」なのだと思います。
「フローを管理する」という役割は、システムを作る上で絶対に欠かせません。
それを無理に無くそうとするのではなく、「フローを管理するコード」と「実際の作業をするコード」を分け、それぞれが自分の関心事(変更理由)にだけ集中できるようにする。
「営業部門からフローの変更依頼が来たときはここを直す」「デザイン部門からフォーマットの変更依頼が来たときはあそこを直す」というように、誰からのどんな要望でそのコードに手を入れるのかを想像してみると、責任の境界線が少し見えやすくなるかもしれません。
これはあくまで一つの考え方ですが、クラスの分割や名付けに迷った際、皆さんのプロジェクトの設計を考える上での小さなヒントになれば幸いです。
この記事が少しでも参考になった方へ
関連書籍として以下もあわせて紹介します。
Kindle Unlimitedの対象になっている場合は、追加費用なしでそのまま読むことができます。
関連書籍
SOLID原則 実践ソフトウェア設計
SOLID原則の「知っている」と「できる」の間にあるギャップを埋めるための実践的な設計ガイドです。
コード例はC#ですが、言語に依存しない「設計の考え方」に焦点を当てています。
SOLID原則 C言語ソフトウェア設計
制約の少ないC言語を題材に、ソフトウェア設計の本質(責任・契約・依存)を体系的に学べる一冊です。
関数ポインタや構造体などを活用し、オブジェクト指向的な設計を自ら構築しながらSOLID原則を理解できます。
