メインコンテンツまでスキップ

単一責任の原則

第 1 章で見た Order クラスは、注文明細の管理、合計金額の計算、在庫確認、確認メールの送信、データベースへの保存、請求書 PDF の生成という 6 つの役割を持つ god class でした。 この章では、実際に Order クラスを分割していきます。

god classの兆候​

分割の前に、god class をどう見分けるかを整理します。 次のような特徴があれば、god class を疑ってよい兆候です。

  • コンストラクタやメソッドの引数が多い
  • クラス名が「Manager」「Helper」のような汎用的な言葉になっている、あるいは複数の名詞を含んでいる
  • クラスの中で扱っている関心事の種類が多い(データの永続化、通知、金額計算、外部フォーマット生成など)
  • そのクラスを変更する理由を、一言で説明できない

第 1 章の Order クラスは、最後の項目に当てはまっていました。 「注文明細が変わったから」「メール文面が変わったから」「在庫テーブルの構造が変わったから」「請求書のフォーマットが変わったから」と、変更理由が 4 つも 5 つも考えられる状態だったのです。

単一責任の原則とは​

クラスが変更される理由は 1 つだけであるべきだ、という考え方を単一責任の原則(Single Responsibility Principle、SRP)と呼びます1。 Robert C. Martin が提唱した原則です。

ここで言う「理由」とは、「誰が、どんな都合でそのクラスを変更するか」という視点です。 Order クラスの場合、次のように変更理由を担当領域ごとに分けられます。

  • 注文明細の持たせ方が変わる: 注文そのものの仕様変更
  • メール文面が変わる: マーケティングやカスタマーサポートの都合
  • 在庫テーブルの構造が変わる: 在庫管理システムの都合
  • 請求書のフォーマットが変わる: 会計・経理の都合

これらは互いに無関係な理由です。 1 つのクラスに同居していると、ある理由での変更が、無関係な他の処理にまで影響するリスクを持ち続けます。

Orderクラスを分割する​

第 1 章の予告どおり、責務ごとにクラスを分割します。

✅ Good: 責務ごとに分割したOrderクラス
class Order
{
private array $items = [];

public function __construct(
private readonly string $customerEmail,
) {
}

public function addItem(string $name, int $price, int $quantity): void
{
$this->items[] = [
'name' => $name,
'price' => $price,
'quantity' => $quantity,
];
}

public function calculateTotal(): int
{
$total = 0;
foreach ($this->items as $item) {
$total += $item['price'] * $item['quantity'];
}
return $total;
}

public function getItems(): array
{
return $this->items;
}

public function getCustomerEmail(): string
{
return $this->customerEmail;
}
}

class StockChecker
{
public function hasEnoughStock(Order $order): bool
{
foreach ($order->getItems() as $item) {
$stock = Database::query(
'SELECT quantity FROM stocks WHERE name = ?',
[$item['name']]
);
if ($stock < $item['quantity']) {
return false;
}
}
return true;
}
}

class OrderMailer
{
public function sendConfirmation(Order $order): void
{
$body = "ご注文ありがとうございます。合計金額は{$order->calculateTotal()}円です。";
mail($order->getCustomerEmail(), '注文確認', $body);
}
}

class InvoicePdfGenerator
{
public function generate(Order $order): string
{
$total = $order->calculateTotal();
// PDF生成処理(省略、$totalを請求書の合計欄に反映)
return "invoice-{$order->getCustomerEmail()}.pdf";
}
}

Order クラスには、注文明細の保持と合計金額の計算という、注文そのものの責務だけが残りました。 在庫確認・メール送信・PDF 生成は、それぞれ別のクラスに移っています。 データベースへの保存はこの章では扱いません。 永続化の設計は、Laravel の実践編である第 2 部で改めて取り上げます。

分割してわかること​

1 つ、正直に確認しておきたいことがあります。 この分割によって、calculateTotal への依存箇所そのものが減ったわけではありません。 OrderMailer も InvoicePdfGenerator も、それぞれ$order->calculateTotal()を呼んでいます。 送料計算のロジックを変更すれば、この 2 箇所を確認する必要があります。 分割によってクラスは整理されましたが、calculateTotal への依存という結合そのものは残ったままです。

分割によって変わったのは、それぞれのクラスが担う関心事が 1 つに絞られたことです。 在庫確認のロジックを変えたいときは、StockChecker だけを読めば済みます。 メール文面を変えたいときは、OrderMailer だけを見ればよく、在庫確認や PDF 生成のコードを読み飛ばす必要がありません。 StockChecker を他の画面でも使い回したいときも、Order クラス全体を持ち出さずに、StockChecker 単体を呼び出せます。

calculateTotal のような、複数のクラスから参照される値そのものへの依存を減らす設計は、この先の章で扱います。

まとめ​

  • god class は、コンストラクタ・メソッドの引数の多さや、変更理由の説明しにくさから見分けられる
  • 単一責任の原則は、クラスが変更される理由を 1 つに絞るという考え方
  • 責務ごとにクラスを分割すると、各クラスが担う関心事が 1 つに絞られ、変更時に読むべき範囲が狭まる
  • 分割は依存関係そのものを減らすわけではない。値への依存を減らす設計は別の技法が必要

次に読む​

次章では、継承とコンポジションを扱います。 is-a・has-a の関係と、なぜ継承よりコンポジションが優先されるかを見ていきます。

練習問題​

次のReportGeneratorクラスが抱えている、変更理由を3つ挙げてください
class ReportGenerator
{
public function generate(array $salesData): void
{
// 集計処理
$total = array_sum(array_column($salesData, 'amount'));

// CSV形式に変換
$csv = "date,amount\n";
foreach ($salesData as $row) {
$csv .= "{$row['date']},{$row['amount']}\n";
}

// ファイルに保存
file_put_contents('/tmp/report.csv', $csv);

// Slackに通知
$this->postToSlack("レポートを生成しました。合計: {$total}円");
}

private function postToSlack(string $message): void
{
// Slack API呼び出し(省略)
}
}

解答例

少なくとも、次の 3 つの変更理由が考えられます。

  1. 集計ロジックが変わる(合計以外の指標を追加したいなど、集計担当の都合)
  2. 出力フォーマットが変わる(CSV から JSON に変えたいなど、データ連携先の都合)
  3. 通知方法が変わる(Slack から Email に変えたいなど、通知インフラの都合)

これらは、集計・フォーマット変換・保存・通知という 4 つの処理が 1 つのメソッドに同居している状態です。 Order クラスと同じように、それぞれ別のクラスに分割できます。


Footnotes​

  1. 出典: Single-responsibility principle(Wikipedia)。Robert C. Martin が 2003 年の著書『Agile Software Development, Principles, Patterns, and Practices』で広めた原則で、「クラスが変更される理由は 1 つだけであるべきだ」と定義されています。 ↩