PHP: Builderパターンの実装手順 #1【基礎実装】

Builderパターンとは、生成過程を抽象化したり、コンストラクタの引数が多かったり、コンストラクタの引数を作るのが複雑なときに使うデザインパターンのひとつだ。本稿では、引数が多すぎるコンストラクタの解消法のひとつとしてPHPでのBuilderパターンの実装方法を紹介する。

なお、本稿で実装したコードの完全版は[GitHub](https://github.com/suin/php-playground/tree/master/BuilderPattern/No1Basic)で公開している。

## お題のモデル

Builderパターンを実装するに当たってのお題として、Eメールを扱う。Eメールは、To, CC, From, Subject, Bodyの5つの情報を持っていることにする。これを素直にモデルにするとこうなる:

![image.png](https://qiita-image-store.s3.amazonaws.com/0/889/ab0d6d6d-1a78-87b0-2271-7c70aa6f96ae.png)

コンストラクタには5つ引数がある。この実装では、`Email`クラスを利用したいクライアントコードは次のようになる:

```php
new Email(
    ['alice@example.com'],
    ['bob@example.com'],
    [],
    'Hello',
    'Hello, there.'
);
```

これは可読性が高いコードとは言えないだろう。一見するとどれが送信者のアドレスでどれが受信者のものか見分けがつきにくい。引数の順番を間違えれば、送信者と受信者が真逆になるかもしれない。また、CCはオプショナルなのだが、いちいち空の配列を渡さないとならない。しかしながら、PHPのコンストラクタではこれ以上の表現は難しいだろう。[^1]

[^1]: 他の言語のように名前付きパラメータが使えればいいのですが……。
  
  ```php
  new Email(
      to = ['alice@example.com'],
      from = ['bob@example.com'],
      subject = 'Hello',
      body = 'Hello, there.'
  );
  ```
  
  PHPでこれと似たようなことをするには、連想配列を渡すことになりますが、IDEのリファクタリング支援が受けにくくなったり、型の安全性を損なうのでおすすめしません。
  
  ```php
  $config = ['to' => 'alice@example.com', 'from' => ...];
  new Email($config);
  ```


Builderパターンはこうした問題を解決する。Emailを作るクラスとして`EmailBuilder`を用意しよう。`EmailBuilder`は`Email`のコンストラクタよりも明示的な表現ができるAPIを備える。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/889/daf97684-4000-689b-e8ce-178ce347617b.png)


この`EmailBuilder`を使うと、クライアントコードの可読性が高まる。

```php
$emailBuilder = new EmailBuilder();
$emailBuilder->setFrom('alice@example.com');
$emailBuilder->setTo('bob@example.com');
$emailBuilder->setSubject('Hello');
$emailBuilder->setBody('Hello, there.');
$email = $emailBuilder->build();
```

## `Email`クラスの実装

以上は利用者視点で見たBuilderパターンだったが、ここからはBuilderパターンの実装者として各クラスを見ていこう。

`Email`クラスはシンプルに`Email`の属性とコンストラクタを実装する:

```php
final class Email
{
    private $from;
    private $to;
    private $cc;
    private $subject;
    private $body;

    /**
     * @param string[] $from
     * @param string[] $to
     * @param string[] $cc
     */
    public function __construct(
        array $from,
        array $to,
        array $cc,
        string $subject,
        string $body
    ) {
        $this->from = $from;
        $this->to = $to;
        $this->cc = $cc;
        $this->subject = $subject;
        $this->body = $body;
    }
}
```

あとは振る舞いを持たせるなり、ゲッターを生やすなりする。

## `EmailBuilder`クラスの実装

次に`EmailBuilder`クラスを実装していこう。

まずは`Email`クラスのコンストラクタ引数と同じ数だけのプロパティを`EmailBuilder`に持たせる。デフォルト値がセットできるものはせっとしておこう。ここでは`array`型のプロパティである`$from`, `$to`, `$cc`は空の配列で初期化しておいたほうがいい。`$body`などは0バイトの文字列もOKという仕様にしたいので未設定は`""`ではなく`null`で表現する。

```php
final class EmailBuilder
{
    private $from = [];
    private $to = [];
    private $cc = [];

    /**
     * @var string|null
     */
    private $subject;

    /**
     * @var string|null
     */
    private $body;
}
```

次にセッターを用意する。このへんはPhpStormのコード生成([Generate Code])でサクッと作ってしまおう。ちなみに、引数に使っている`...`については[ジェネリクスがないPHPでも配列中身のタイプヒントを可能にする「Splat Operator」](https://qiita.com/suin/items/781c35edc32aaafac01e)を参照。

[Generate Code]: https://qiita.com/suin/items/bda682201fed0936cf95#generate-code

```php
final class EmailBuilder
{
    // ...プロパティ略...

    public function setFrom(string ...$from): void
    {
        $this->from = $from;
    }

    public function setTo(string ...$to): void
    {
        $this->to = $to;
    }

    public function setCc(string ...$cc): void
    {
        $this->cc = $cc;
    }

    public function setSubject(string $subject): void
    {
        $this->subject = $subject;
    }

    public function setBody(string $body): void
    {
        $this->body = $body;
    }
}
```

最後に`Email`オブジェクトを生成する`build`メソッドを生やす。

```php
final class EmailBuilder
{
    // ...プロパティ略...

    // ...セッター略...

    public function build(): Email
    {
        return new Email(
            $this->from,
            $this->to,
            $this->cc,
            $this->subject,
            $this->body
        );
    }
}
```

ここまでやれば`EmailBuilder`クラスはビルダーとして機能するようになる。

## `EmailBuilder`クラスにバリデーションをしかける

このままだと、`EmailBuilder`は受信者や件名、本文が指定されてないメールが作れてしまうバグがある。なので、`build()`メソッドを直しておかしな`Email`オブジェクトが作られないようにしよう。

`build`メソッドで`new Email`する前に、`Email`オブジェクトが満たすべき不変条件を定義する。難しく言ったが平たく言えばバリデーションするということだ。

```php
    public function build(): Email
    {
        $this->assertAtLeastOneFromAddress();
        $this->assertAtLeastOneRecipientAddress();
        $this->assertSubjectIsProvided();
        $this->assertThatBodyIsSpecified();
        return new Email(
            $this->from,
            $this->to,
            $this->cc,
            $this->subject,
            $this->body
        );
    }
```

`build`メソッドにおいては、どんな不変条件があるかは抽象的に書いたほうが良い。なぜなら、「何がチェックされるか」と「どうチェックされるか」を分けたほうが可読性のみならず保守性も向上するからだ。`assert`のメソッド名が「何が」を表現し、各assertメソッドの実装が「どう」を表現する。「何がチェックされるか」は変更されにくいが、「どうチェックするか」はリファクタリングやチューニングのために変更されやすい。

チェック方法がアップデートされるたびに`build`メソッドに手が入るのは好ましくない。`build`メソッドを書き換えるのは、`Email`オブジェクトの作成方法を変えるときだ。できるだけそうなるように設計する。

各`assert`メソッドは`EmailBuilder`でしか使わないので`private`メソッドとして実装する。

```php
final class EmailBuilder
{
    // ...プロパティ略...
    // ...セッター略...
    // ...buildメソッド略...

   private function assertAtLeastOneFromAddress(): void
    {
        if (empty($this->from)) {
            throw new \LogicException(
                'At least one from-address must be provided'
            );
        }
    }

    private function assertAtLeastOneRecipientAddress(): void
    {
        if (empty($this->to) && empty($this->cc)) {
            throw new \LogicException(
                'At least one recipient address (To or CC) must be provided'
            );
        }
    }

    private function assertSubjectIsProvided(): void
    {
        if ($this->subject === null) {
            throw new \LogicException('Email subject must be provided');
        }
    }

    private function assertThatBodyIsSpecified(): void
    {
        if ($this->body === null) {
            throw new \LogicException('Email body must be provided');
        }
    }
}
```

## テストを書く

最後にPHPUnitでテストを書く。

Builderパターンのテストを書くコツは、ひとつひとつのメソッドに対してテスト(`setTo()`のテストなど)を書くより、`build()`までやって期待したオブジェクトが生成できるかどうかまで通貫したテストを書くことだ。そうしたほうが、Builderの利用者にとってサンプルコードになるし、パラメータの数だけテストケースを用意しないといけない地獄を味わわなくて済む。

```php
use PHPUnit\Framework\TestCase;

final class EmailTest extends TestCase
{
    /**
     * @test
     */
    public function email_builder_usage(): void
    {
        // 1. create EmailBuilder object.
        $emailBuilder = new EmailBuilder();

        // 2. provide email properties.
        $emailBuilder->setFrom('alice@example.com');
        $emailBuilder->setTo('bob@example.com');
        $emailBuilder->setCc('carol@example.com');
        $emailBuilder->setSubject('Hello');
        $emailBuilder->setBody('Hello, Bob.');

        // 3. get Email object with calling build() method.
        $email = $emailBuilder->build();

        // The Email object will be like the following:
        self::assertSame(['alice@example.com'], $email->getFrom());
        self::assertSame(['bob@example.com'], $email->getTo());
        self::assertSame(['carol@example.com'], $email->getCc());
        self::assertSame('Hello', $email->getSubject());
        self::assertSame('Hello, Bob.', $email->getBody());
    }

    /**
     * @test
     * @testdox buildするには少なくともToかCCどちらかにメールアドレスがあれば良い
     */
    public function build_email_without_to(): void
    {
        $emailBuilder = new EmailBuilder();
        $emailBuilder->setFrom('dummy@sender');
        $emailBuilder->setCc('bob@example.com');
        $emailBuilder->setSubject('dummy_subject');
        $emailBuilder->setBody('dummy_body');
        $email = $emailBuilder->build();
        self::assertSame([], $email->getTo());
        self::assertSame(['bob@example.com'], $email->getCc());
    }
}
```

あとは、不変条件違反のテストケースを用意する。これで単体テストを書く作業が完了となる。

```php
final class EmailTest extends TestCase
{
    // ...

    public function test_missing_sender_email_address(): void
    {
        $this->expectException(\LogicException::class);
        $this->expectExceptionMessage(
            'At least one from-address must be provided'
        );
        $emailBuilder = new EmailBuilder();
        $emailBuilder->setCc('dummy@address');
        $emailBuilder->setSubject('dummy_subject');
        $emailBuilder->setBody('dummy_body');
        $emailBuilder->build();
    }
}
```

## 今後のお題

今回は基本的なBuilderパターンの実装手順を紹介したかったので、紹介できなかったテーマがいくつかある。

* EmailをEmailBuilder経由で作ることを強制する方法
* Fluent Builder
* イミュータブルなBuilder
* Builderを使ったEmail生成方法の抽象化

## 関連

* [PHP: Builderパターンの実装手順 #1【基礎実装】](https://qiita.com/suin/items/d8d4dc019d3beb428249) `この投稿`
* [PHP: Builderパターンの実装手順 #2【Fluent Interface実装】](https://qiita.com/suin/items/888f59851f940ee97c3a) 
* [PHP: Builderパターンの実装手順 #3【Immutable Builder実装】](https://qiita.com/suin/items/7594e8984efc96594cee)
* [PHP: Builderパターンの実装手順 #4【Builder経由での生成を強制する】](https://qiita.com/suin/items/feafae06c644f871f7b8)