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

はじめに: なぜクラス設計を学ぶのか

PHP でクラスを書けることと、変更に強いクラスを設計できることは、別の技術です。

文法どおりに書かれたクラスでも、機能を追加するたびにあちこちの処理を読み返す必要が生じたり、1 つの修正が思わぬ箇所の挙動を変えてしまったりすることがあります。 この連載は、その差を埋めるために書きます。

1つのクラスに責務を詰め込むとどうなるか​

次の Order クラスを見てください。 EC サイトでよく見かける、注文を表すクラスです。

クラスの文法は第2章で扱います

privateやpublic functionのような文法の細部は、まだ気にしなくてかまいません。 これらは第 2 章で 1 つずつ解説します。 ここでは、メソッド(クラスの中に定義された、処理のまとまり)がいくつあり、それぞれ何をしているかという全体像だけを見てください。 Database::queryやDatabase::insertも、説明のために用意した架空のクラスです。

❌ Bad: 複数の責務を持つOrderクラス
<?php

class Order
{
private array $items = [];
private string $customerEmail;

public function __construct(string $customerEmail)
{
$this->customerEmail = $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 hasEnoughStock(): bool
{
foreach ($this->items as $item) {
$stock = Database::query(
'SELECT quantity FROM stocks WHERE name = ?',
[$item['name']]
);
if ($stock < $item['quantity']) {
return false;
}
}
return true;
}

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

public function save(): void
{
Database::insert('orders', [
'customer_email' => $this->customerEmail,
'total' => $this->calculateTotal(),
]);
}

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

このクラスは、注文明細の管理、合計金額の計算、在庫確認、確認メールの送信、データベースへの保存、請求書 PDF の生成という 6 つの役割を一手に引き受けています。

1 つのクラスが本来の役割を超えて多くの責務を抱え込んだ状態は、god class(神クラス)と呼ばれます1。 god class 自体は、PHP の文法エラーにはなりません。 動きますし、テストデータを流せば正しい合計金額も返ってきます。 問題は動くかどうかではなく、この先で変更を重ねたときに表面化します。

変更が一箇所で済まない​

Order クラスに、次の変更要求が来たとします。

離島への配送では、送料を追加してほしい。

この要求を実装するには、まずどこに手を入れるべきかを考える必要があります。 calculateTotal の中に送料を足し込むのか、送料だけを返す別メソッドを用意するのか。 前者を選んだ場合、calculateTotal の戻り値を参照している箇所をすべて洗い出さなければなりません。 sendConfirmationEmail は合計金額をメール本文に埋め込んでいますし、save はその値をデータベースに保存し、generateInvoicePdf も請求書に反映します。

calculateTotal の戻り値を、メール送信・保存・PDF 生成という性質の異なる 3 つの処理が共有しているため、計算ロジックを変えると、この 3 箇所すべてで意図した金額になっているかを確認する必要があります。 呼び出し元の数が増えるほど、この確認作業も増えていきます。

hasEnoughStock にも同様の問題があります。 Database クラスに直接クエリを投げているため、在庫テーブルの構造が変わったときや、在庫確認のロジックを他の画面でも使い回したいときに、Order クラスの外からこのメソッドを呼び出すか、同じクエリをどこかにコピーするかを迫られます。

クラス設計を学ぶと何が変わるか​

同じ注文処理でも、責務ごとにクラスを分けると見通しが変わります。 注文明細と金額計算は Order クラスに残し、在庫確認は StockChecker、メール送信は OrderMailer、PDF 生成は InvoicePdfGenerator のように分割し、それぞれが 1 つの役割だけを持つようにします。

送料のルールを変えたいときは、送料の計算を担うクラスだけを見ればよくなります。 在庫確認のロジックを他の画面で使い回したいときも、StockChecker を呼び出すだけで済みます。

この分割は、闇雲にクラスを増やせばよいという話ではありません。 分け方を誤ると、今度はクラス同士のやり取りが複雑になり、god class とは別の種類の読みにくさが生まれます。 どこで分け、どこは分けないままにするかを判断する力を、本連載では次章以降で一歩ずつ身につけていきます。

この連載で扱うこと​

本連載は 2 部構成です。 第 1 部(第 1〜7 章)ではフレームワークに依存しない PHP のクラス設計原則を扱い、第 2 部(第 8〜14 章)ではその原則を Laravel のモデル設計・サービスクラス設計に応用します。

クラス設計の経験は問いません。 PHP の基本文法(変数・関数・制御構文)が身についていれば、第 1 部から読み進められます。 連載を読み終えると、Fat Controller や Fat Model のような設計の崩れを見たときに、何が問題で、どう直すかを自分の言葉で判断できる状態を目指します。 連載全体の章立てと対象読者の詳細は、このガイドの目次にまとめています。

練習問題​

Orderクラスに送料計算を追加する場合、影響範囲を考えてください

calculateTotal に送料計算を追加するとします。 正しく動作しているか確認すべきメソッドを、本文中の Order クラスからすべて挙げてください。

解答例

calculateTotal の戻り値を参照している、次の 3 つのメソッドを確認する必要があります。

  • sendConfirmationEmail: メール本文の合計金額に送料が反映されているか
  • save: データベースに保存する合計金額に送料が反映されているか
  • generateInvoicePdf: 請求書の合計金額に送料が反映されているか

3 つのメソッドが calculateTotal の戻り値をそのまま使っているため、計算ロジックを変えると、この 3 箇所すべてに影響します。 この依存の減らし方は、次章以降で扱います。

まとめ​

  • 1 つのクラスに関係の薄い責務を詰め込むと、god class と呼ばれる状態になる
  • god class 自体は動くが、1 つの変更が複数の呼び出し元に波及しやすく、変更のたびに影響範囲の洗い出しが必要になる
  • 責務ごとにクラスを分けることで、変更の影響範囲を狭められる
  • どこで分割するかの判断力を、本連載では次章以降で段階的に養う

次に読む​

次章では、PHPにおけるクラスの基本文法(プロパティ・メソッド・コンストラクタ・可視性修飾子・readonly・コンストラクタプロモーション・enum)を扱います。


Footnotes​

  1. 出典: What Is a God Class and Why Should We Avoid It?。単一のクラスが過剰な責務を抱え、システムの大部分を把握・制御してしまう状態を指すアンチパターンです。単一責任の原則(第 4 章で扱います)に反する典型例として知られています。 ↩