#11 デザインパターン入門
Template Method——アルゴリズムの骨格を定義する
問題: 「手順は同じだが、ステップの中身が違う」
CSVとExcelのインポート機能を作るとき、処理の大まかな流れは同じです。
- ファイルを開く
- データを行ごとにパース
- バリデーション
- DBへ保存
- ファイルを閉じる
しかしCSVとExcelではファイルの「開き方」「パース方法」が異なります。コードを全部コピーすると、手順5の「ファイルを閉じる」処理を修正したとき両方の修正が必要になります。
// 問題のあるコード: 処理の骨格が重複している
class CsvImporter
{
public function import(string $path): void
{
$file = fopen($path, 'r'); // ①開く
while ($row = fgetcsv($file)) { // ②パース(CSV固有)
$this->validate($row); // ③バリデーション
$this->save($row); // ④保存
}
fclose($file); // ⑤閉じる
}
}
class ExcelImporter
{
public function import(string $path): void
{
$reader = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx();
$sheet = $reader->load($path)->getActiveSheet(); // ①開く(Excel固有)
foreach ($sheet->getRowIterator() as $row) { // ②パース(Excel固有)
$cells = iterator_to_array($row->getCellIterator());
$data = array_map(fn($c) => $c->getValue(), $cells);
$this->validate($data); // ③バリデーション
$this->save($data); // ④保存
}
// ⑤閉じる(Excelはガベージコレクションに任せる)
}
}
パターン: Template Method
Template Methodパターンは、アルゴリズムの骨格を抽象クラスで定義し、可変部分のみを子クラスにオーバーライドさせるパターンです。「型(テンプレート)」と「中身」を分離します。
+----------------------------------+
| DataImporter (abstract) |
+----------------------------------+
| + import(path: string): void | ← テンプレートメソッド(final推奨)
| - open(path) |
| - parseRows(): array |
| - validate(row): void |
| - save(row): void |
| - close(): void |
+----------------------------------+
| # open(path): void (abstract) | ← フック/抽象メソッド
| # parseRows(): array (abstract) |
| # close(): void (デフォルト実装)|
+----------------------------------+
△
_________|___________
| |
CsvImporter ExcelImporter
(open, parseRowsを実装)
PHP 8.x での実装
<?php
// 抽象基底クラス(テンプレートを定義)
abstract class DataImporter
{
private int $processedCount = 0;
private int $errorCount = 0;
// テンプレートメソッド: アルゴリズムの骨格を定義。変更させない
final public function import(string $filePath): array
{
$this->open($filePath);
try {
$rows = $this->parseRows();
foreach ($rows as $row) {
try {
$this->validate($row);
$this->save($row);
$this->processedCount++;
} catch (\InvalidArgumentException $e) {
$this->errorCount++;
$this->handleRowError($row, $e);
}
}
} finally {
$this->close();
}
return [
'processed' => $this->processedCount,
'errors' => $this->errorCount,
];
}
// サブクラスが実装しなければならない抽象メソッド
abstract protected function open(string $filePath): void;
abstract protected function parseRows(): array;
// デフォルト実装を持つフックメソッド(オーバーライド任意)
protected function validate(array $row): void
{
if (empty($row)) {
throw new \InvalidArgumentException('空の行はスキップします');
}
}
protected function close(): void
{
// デフォルトは何もしない(サブクラスで必要に応じてオーバーライド)
}
protected function handleRowError(array $row, \Throwable $e): void
{
// デフォルトはログ出力のみ(オーバーライドで通知等を追加できる)
error_log("行処理エラー: " . $e->getMessage());
}
// サブクラスがオーバーライドする: 抽象として強制しない
protected function save(array $row): void
{
// デフォルトの保存処理(DBへ)
echo "保存: " . implode(', ', $row) . "\n";
}
}
// CSVインポーター
class CsvImporter extends DataImporter
{
private mixed $fileHandle = null;
private array $rows = [];
protected function open(string $filePath): void
{
$this->fileHandle = fopen($filePath, 'r');
if ($this->fileHandle === false) {
throw new \RuntimeException("ファイルを開けません: {$filePath}");
}
}
protected function parseRows(): array
{
$rows = [];
// ヘッダー行をスキップ
fgetcsv($this->fileHandle);
while (($row = fgetcsv($this->fileHandle)) !== false) {
$rows[] = $row;
}
return $rows;
}
protected function close(): void
{
if ($this->fileHandle !== null) {
fclose($this->fileHandle);
}
}
}
// Excelインポーター
class ExcelImporter extends DataImporter
{
private ?\PhpOffice\PhpSpreadsheet\Spreadsheet $spreadsheet = null;
protected function open(string $filePath): void
{
$reader = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx();
$this->spreadsheet = $reader->load($filePath);
}
protected function parseRows(): array
{
$sheet = $this->spreadsheet->getActiveSheet();
$rows = [];
foreach ($sheet->getRowIterator(2) as $row) { // 2行目からスタート(ヘッダースキップ)
$cells = iterator_to_array($row->getCellIterator());
$rows[] = array_map(fn($c) => $c->getValue(), $cells);
}
return $rows;
}
// ExcelはGCに任せるのでcloseは不要(オーバーライドしない)
}
使う側のコードはシンプルです。
// CSVインポート
$csvImporter = new CsvImporter();
$result = $csvImporter->import('/path/to/users.csv');
echo "処理済み: {$result['processed']}, エラー: {$result['errors']}\n";
// Excelインポート(同じテンプレートで動く)
$excelImporter = new ExcelImporter();
$result = $excelImporter->import('/path/to/users.xlsx');
フレームワークのコア設計
LaravelのコントローラーやJobクラスもTemplate Methodパターンを使っています。
// Laravelのコントローラーの仕組み(簡略化)
abstract class Controller
{
// フレームワークが呼び出すテンプレートメソッド
public function callAction(string $method, array $parameters): mixed
{
$this->beforeAction(); // フック(子クラスでオーバーライド可)
$result = $this->$method(...$parameters); // 子クラスのアクション
$this->afterAction(); // フック
return $result;
}
protected function beforeAction(): void {} // デフォルトは何もしない
protected function afterAction(): void {}
}
// 子クラスはアクションだけ実装する
class UserController extends Controller
{
public function index(): Response
{
return response()->json(User::all());
}
}
LaravelのMailableクラスも同様です。
// app/Mail/WelcomeMail.php
use Illuminate\Mail\Mailable;
class WelcomeMail extends Mailable
{
public function __construct(private readonly User $user) {}
// build() がテンプレートメソッドの「実装部分」に相当
public function build(): self
{
return $this->subject('ようこそ!')
->view('emails.welcome')
->with(['user' => $this->user]);
}
}
StrategyパターンとTemplate Methodの使い分け
よく比較される両パターンの違いをまとめます。
| 比較項目 | Template Method | Strategy |
|---|---|---|
| 実現手段 | 継承(抽象クラス) | 委譲(インターフェース) |
| 変更タイミング | コンパイル時(クラスを選ぶ) | 実行時(オブジェクトを差し替え) |
| 骨格の場所 | 親クラス | コンテキストクラス |
| 結合度 | 高め(継承依存) | 低め(インターフェース依存) |
| 向いている場面 | 「一部だけ違う処理」を統一したい | アルゴリズム全体を実行時に切り替えたい |
使い分けの目安:
- 「CSVとExcelで処理の骨格は同じ」→ Template Method(継承)
- 「支払い方法を実行時にカートの状態で切り替えたい」→ Strategy(委譲)
一般に、Template MethodよりStrategyの方が柔軟ですが、Template Methodは骨格の共有と強制力(final メソッド)が明確に書けます。
まとめ
- Template Methodは「アルゴリズムの骨格」を抽象クラスで固め、可変部分を子クラスに委ねる
finalメソッドで骨格の改変を禁止し、abstractで実装を強制する- フックメソッド(デフォルト実装あり)でオプションのカスタマイズも可能
- Strategyは実行時の切り替えに向き、Template Methodはコンパイル時(クラス設計時)の統一に向く
次回はシリーズ最終回、まとめ——パターンを活かした設計の考え方を解説します。