はじめに — なぜクラス設計を学ぶのか
PHPでクラスを書けることと、変更に強いクラスを設計できることは、別の技術です。
文法どおりに書かれたクラスでも、機能を追加するたびにあちこちの処理を読み返す必要が生じたり、1つの修正が思わぬ箇所の挙動を変えてしまったりすることがあります。 この連載は、その差を埋めるために書きます。
1つのクラスに責務を詰め込むとどうなるか
次のOrderクラスを見てください。 ECサイトでよく見かける、注文を表すクラスです。
privateやpublic functionのような文法の細部は、まだ気にしなくてかまいません。
これらは第2章で1つずつ解説します。
ここでは、メソッド(クラスの中に定義された、処理のまとまり)がいくつあり、それぞれ何をしているかという全体像だけを見てください。
Database::queryやDatabase::insertも、説明のために用意した架空のクラスです。
<?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)を扱います。