#04 デザインパターン入門
Builder——複雑なオブジェクトを段階的に組み立てる
問題: コンストラクタが複雑になりすぎる
オプションの多いオブジェクトを生成するとき、コンストラクタに引数を詰め込むとすぐに読みにくくなります。
// 問題のあるコード: コンストラクタ引数が多すぎる
$query = new SqlQuery(
'users', // テーブル
['id', 'name'], // カラム
['age > 18'], // WHERE条件
'name', // ORDER BY
'ASC', // 方向
10, // LIMIT
0, // OFFSET
['orders'], // JOIN
false // DISTINCT
);
引数の順序を覚えなければならず、何番目が何を意味するかが一目でわかりません。また、オプション項目を省略したい場合に null を大量に渡す必要があります。
パターン: Builder
Builderパターンは、複雑なオブジェクトの構築プロセスを専用の「ビルダークラス」に分離するパターンです。各オプションをメソッドで順番に設定し、最後に build() や get() で完成品を取り出します。
+--------------------------------+
| QueryBuilder |
+--------------------------------+
| - table: string |
| - columns: array |
| - conditions: array |
| - orderBy: string|null |
| - limitValue: int|null |
+--------------------------------+
| + from(table): static |
| + select(columns): static |
| + where(condition): static |
| + orderBy(column): static |
| + limit(n): static |
| + build(): SqlQuery |
+--------------------------------+
PHP 8.x での実装
<?php
// 完成品クラス
final class SqlQuery
{
public function __construct(
public readonly string $table,
public readonly array $columns,
public readonly array $conditions,
public readonly ?string $orderColumn,
public readonly string $orderDirection,
public readonly ?int $limitValue,
public readonly int $offsetValue,
) {}
public function toSql(): string
{
$cols = empty($this->columns) ? '*' : implode(', ', $this->columns);
$sql = "SELECT {$cols} FROM {$this->table}";
if (!empty($this->conditions)) {
$sql .= ' WHERE ' . implode(' AND ', $this->conditions);
}
if ($this->orderColumn !== null) {
$sql .= " ORDER BY {$this->orderColumn} {$this->orderDirection}";
}
if ($this->limitValue !== null) {
$sql .= " LIMIT {$this->limitValue}";
}
if ($this->offsetValue > 0) {
$sql .= " OFFSET {$this->offsetValue}";
}
return $sql;
}
}
// ビルダークラス
class QueryBuilder
{
private string $table = '';
private array $columns = [];
private array $conditions = [];
private ?string $orderColumn = null;
private string $orderDirection = 'ASC';
private ?int $limitValue = null;
private int $offsetValue = 0;
public function from(string $table): static
{
$this->table = $table;
return $this; // メソッドチェーンのためにselfを返す
}
public function select(string ...$columns): static
{
$this->columns = $columns;
return $this;
}
public function where(string $condition): static
{
$this->conditions[] = $condition;
return $this;
}
public function orderBy(string $column, string $direction = 'ASC'): static
{
$this->orderColumn = $column;
$this->orderDirection = strtoupper($direction);
return $this;
}
public function limit(int $limit): static
{
$this->limitValue = $limit;
return $this;
}
public function offset(int $offset): static
{
$this->offsetValue = $offset;
return $this;
}
public function build(): SqlQuery
{
if (empty($this->table)) {
throw new \RuntimeException('テーブル名は必須です');
}
return new SqlQuery(
table: $this->table,
columns: $this->columns,
conditions: $this->conditions,
orderColumn: $this->orderColumn,
orderDirection: $this->orderDirection,
limitValue: $this->limitValue,
offsetValue: $this->offsetValue,
);
}
}
使う側のコードは格段に読みやすくなります。
$query = (new QueryBuilder())
->from('users')
->select('id', 'name', 'email')
->where('age > 18')
->where('active = 1')
->orderBy('name')
->limit(10)
->offset(20)
->build();
echo $query->toSql();
// SELECT id, name, email FROM users WHERE age > 18 AND active = 1 ORDER BY name ASC LIMIT 10 OFFSET 20
メソッドチェーン(Fluent Interface)
return $this; を返すことで実現するメソッドチェーンを Fluent Interface(流暢なインターフェース) と呼びます。英語で読むように左から右に流れるコードが書けるため、可読性が大幅に向上します。
// Fluent Interface の例: 条件付きで組み立てられる
function buildUserQuery(bool $onlyActive, ?string $keyword): SqlQuery
{
$builder = (new QueryBuilder())->from('users')->select('id', 'name');
if ($onlyActive) {
$builder->where('active = 1');
}
if ($keyword !== null) {
$builder->where("name LIKE '%{$keyword}%'");
}
return $builder->orderBy('created_at', 'DESC')->limit(20)->build();
}
LaravelのEloquentビルダーとの関係
LaravelのEloquentクエリビルダーはBuilderパターンの典型例です。
// Eloquentビルダー: まさにBuilderパターン
$users = User::query()
->select('id', 'name', 'email')
->where('active', true)
->where('age', '>', 18)
->orderBy('name')
->limit(10)
->offset(20)
->get();
// 条件付きビルド: when() を使ったFluent Interface
$users = User::query()
->when($request->has('keyword'), function ($query) use ($request) {
$query->where('name', 'like', "%{$request->keyword}%");
})
->when($request->has('role'), function ($query) use ($request) {
$query->where('role', $request->role);
})
->get();
Eloquentの query() が返す Builder オブジェクトは、最終的に get() や first() を呼ぶまでSQLを実行しません。これを遅延実行(Lazy Evaluation)と呼び、Builderパターンの大きなメリットのひとつです。
メールビルダーへの応用
Builderパターンはメール送信など他の場面でも活躍します。
// メール送信用ビルダー
class MailBuilder
{
private string $to = '';
private string $subject = '';
private string $body = '';
private array $attachments = [];
private bool $htmlEnabled = false;
public function to(string $email): static
{
$this->to = $email;
return $this;
}
public function subject(string $subject): static
{
$this->subject = $subject;
return $this;
}
public function body(string $body, bool $html = false): static
{
$this->body = $body;
$this->htmlEnabled = $html;
return $this;
}
public function attach(string $filePath): static
{
$this->attachments[] = $filePath;
return $this;
}
public function send(): void
{
// メール送信の実装
echo "Sending mail to {$this->to}: {$this->subject}\n";
}
}
// 使用例
(new MailBuilder())
->to('user@example.com')
->subject('登録完了のお知らせ')
->body('<h1>ようこそ!</h1>', html: true)
->attach('/path/to/guide.pdf')
->send();
まとめ
- Builderパターンは複雑なオブジェクトの構築をビルダークラスに分離する
return $this;によるメソッドチェーン(Fluent Interface)で読みやすいコードになる- 必須パラメータと任意パラメータを明確に分けられる
- LaravelのEloquentクエリビルダーはBuilderパターンの代表例
次回はStrategyパターンで、アルゴリズムを差し替え可能にする方法を解説します。