PHP: Builderパターンの実装手順 #4【Builder経由での生成を強制する】

このBuilderパターン実装シリーズではPHPでどのようにBuilderパターンを実装するのかを解説する。前回は、ImmutableなBuilderを実装する方法を解説した。

前回: [PHP: Builderパターンの実装手順 #3【Immutable Builder実装】 - Qiita](https://qiita.com/suin/items/7594e8984efc96594cee)

今回は、生成対象のオブジェクトをBuilder経由でのみ生成できるようにする方法を紹介する。

本稿で紹介する方法は、PHPの言語仕様の限界にチャレンジする側面があるので、実装としてはトリッキーで必ずしも美しくないかもしれないが、オブジェクトをBuilder経由でのみ生成させたいときには「こういった方法もあるよ」ということを示したいと思う。

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

## なぜBuilder経由でのみ生成できるようにしたいか？

前回までに作ってきたEmailクラスは`new Email`することで`EmailBuilder`を使わなくても生成することが可能だった:

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

このコードは動作するし、問題なさそうだ。では、なぜ`new Email`をさせたくないのか？ そのモチベーションはいくつか考えられる。

* コードの一貫性: すべての生成を`EmailBuilder`経由にすることで、実装のゆれをなくしたい。ここは`new`で、ここはビルダーで、となっていると読み手は混乱するし、ひとつのことを複数の方法でできるようになっているのは美しくない。
* バグりにくさ: `new Email`は引数が多く、注意して書かないとバグを生みやすい。そうならないよういするためにビルダーを作っている。
* ビルダーへの誘導: `EmailBuilder`という便利なクラスがあることに気づかなければ、`new Email`を頑張って書いてしまうだろう。コード上で`new Email`できないようになっていれば、`EmailBuilder`を自然と使ってくれるだろう。
* 保守性の観点: `EmailBuilder`を通じて生成されるよう一貫したコードになっていれば、`EmailBuilder`をメンテするだけで生成の処理をアップデートしていける。もし、`new`が併用されていたら、どちらもメンテしていかなければならない。

## `new Email`を封じる

ビルダー経由でのみ`Email`オブジェクトを作れるようにするための第一歩は、`new Email`を実行できないようにすることだ。そのためには、`Email`クラスのコンストラクタをプライベートメソッドにする。`public function __construct()`となっていた部分を`private function __construct()`に変更しよう。そうすれば、`Email`クラス以外から`new Email`は呼び出せなくなる:

```php
final class Email
{
    // ...略...
    private 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`だけは`new Email`できるようにする

このままだと、`EmailBuilder`までも`new Email`できない。それだと、意味のないコードになってしまうので、`EmailBuilder`だけは、`new Email`できるようにする。

PHPの言語仕様上、そういったことをできるようにする都合のいい構文はない[^1]ので、トリッキーな方法だがプライベートコンストラクタを`Closure`を使って呼び出す小技を使うことにする。

[^1]: 他の言語だと、例えばJavaのinner classやScalaのスコープを指定したprivate修飾子があったりするが、PHPにはそれに該当する機能がない。

まずは、`Email`クラスに`EmailBuilder`を返す静的メソッドを生やす:


```php
final class Email
{
    // ...略...
    public static function builder(): EmailBuilder
    {
        return new EmailBuilder();
    }

    // ...略...
}
```

`EmailBuilder`が間接的に`new Email`できるように、`Email`クラスのコンストラクタを`EmailBuilder`に渡すようにする:

```php
    public static function builder(): EmailBuilder
    {
        $constructor = function (
            array $from,
            array $to,
            array $cc,
            string $subject,
            string $body
        ) {
            return new Email($from, $to, $cc, $subject, $body);
        };
        return new EmailBuilder($constructor);
    }
```

この変更はクライアントコードにも影響がある。これまでは、`new EmailBuilder()`でビルダーを作っていたが、これからは`Email::builder()`でビルダーを作る必要がある。

```php
// 旧コード
$email = (new EmailBuilder())
    ->setFrom('alice@example.com')
    ->setTo('bob@example.com')
    ->setCc('carol@example.com')
    ->setSubject('Hello')
    ->setBody('Hello, Bob.')
    ->build();

// 新コード
$email = Email::builder()
    ->setFrom('alice@example.com')
    ->setTo('bob@example.com')
    ->setCc('carol@example.com')
    ->setSubject('Hello')
    ->setBody('Hello, Bob.')
    ->build();
```

## `EmailBuilder`で`Email`のコンストラクタを受け取れるようにする

次に、`EmailBuilder`を変更して、`Email`のコンストラクタを受け取れるようにする。`EmailBuilder`クラスに`callable`型の引数を受け取るコンストラクタ追加し、それを`$emailConstructor`に代入するようにしよう:

```php
final class EmailBuilder
{
    /**
     * @var callable
     */
    private $emailConstructor;

    // ...略...

    public function __construct(callable $emailConstructor)
    {
        $this->emailConstructor = $emailConstructor;
    }
    // ...略...
}
```

`new Email`は直接呼び出せなくなったので、`EmailBuilder::build`は受け取ったコンストラクタを使うように変更する:

```php
    public function build(): Email
    {
        $this->assertAtLeastOneFromAddress();
        $this->assertAtLeastOneRecipientAddress();
        $this->assertSubjectIsProvided();
        $this->assertThatBodyIsSpecified();
        return ($this->emailConstructor)( // new Emailしていた箇所
            $this->from,
            $this->to,
            $this->cc,
            $this->subject,
            $this->body
        );
    }
```

以上で、`EmailBuilder`の対応は完了だ。

## テストコードを変更する

前回までに作ったテストコードでは`new EmailBuilder()`と書いていたが、今回の変更で`Email::builder()`に書き換える必要が出てきたのでその対応をする。対応箇所は数が少なくないのが、エディタの一括置換で直せば良さそうだ。

```php
use PHPUnit\Framework\TestCase;

final class EmailTest extends TestCase
{
    /**
     * @test
     */
    public function email_builder_usage(): void
    {
        $email = Email::builder() // これまでは (new EmailBuilder()) となっていた
            ->setFrom('alice@example.com')
            ->setTo('bob@example.com')
            ->setCc('carol@example.com')
            ->setSubject('Hello')
            ->setBody('Hello, Bob.')
            ->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());
    }
    // ...略...
}
```

すべて変更したら、テストが通るかチェックしよう。

## リファクタリングする

テストがすべて通ることを確認したら、テストを回しながら`Email::builder()`をリファクタリングしていこう。下記のコードはもっとシンプルにすることができるからだ:

```php
    public static function builder(): EmailBuilder
    {
        $constructor = function (
            array $from,
            array $to,
            array $cc,
            string $subject,
            string $body
        ) {
            return new Email($from, $to, $cc, $subject, $body);
        };
        return new EmailBuilder($constructor);
    }
```

まず、一時変数を無くす。

```php
    public static function builder(): EmailBuilder
    {
        return new EmailBuilder(
            function (
                array $from,
                array $to,
                array $cc,
                string $subject,
                string $body
            ) {
                return new Email($from, $to, $cc, $subject, $body);
            }
        );
    }
```

次に、`Email`は`self`に置き換えられるのでそれも直す:

```php
    public static function builder(): EmailBuilder
    {
        return new EmailBuilder(
            function (
                array $from,
                array $to,
                array $cc,
                string $subject,
                string $body
            ) {
                return new self($from, $to, $cc, $subject, $body);
            }
        );
    }
```

`new self`への引数はクロージャの引数をそのまま渡せばいいので、可変長引数を使うように変える:

```php
    public static function builder(): EmailBuilder
    {
        return new EmailBuilder(
            function () {
                return new self(...func_get_args());
            }
        );
    }
```

これで最もシンプルな記述になった。

最後に、`new EmailBuilder()`に渡す変数が何なのか読み手に伝わりやすくするために、クロージャーを作る処理を適切な名前をつけたプライベートメソッドに切り出す。ここでは切り出したメソッドは`getEmailConstructor`と名付ける。

```php
final class Email
{
    // ...略...

    public static function builder(): EmailBuilder
    {
        return new EmailBuilder(self::getEmailConstructor());
    }

    // ...略...

    private static function getEmailConstructor(): callable
    {
        return function () {
            return new self(...func_get_args());
        };
    }
}
```

クロージャー構文を直接EmailBuilderのコンストラクタに書くよりも説明的になったのではないだろうか。

これで、リファクタリングは完了となる。

## 今回のパターンの問題点

今回紹介したパターンは、確かにビルダー経由での生成を強制することに成功したが、問題点がまったくないわけではない。

1. まず、トリッキーである。クロージャを使ってprivateコンストラクタにアクセスしているので初見殺し感はいなめない。
2. 次に、EmailとEmailBuilderが密結合になっている。そして循環依存になっている。本当ならEmailがEmailBuilderを知らないほうが設計としては優れている。

これらの問題があるが、PHPにおいてはこれはトレードオフだと思うので、問題点を理解した上で使う分には有りだと思う。


## まとめ

* Builderパターンの実装において、生成対象のオブジェクトをBuilder経由でのみ生成できるようにする方法を紹介した。
* それに当たってのテストコードの変更方法を説明した。
* 紹介した方法にある問題点と、それにはトレードオフがあることを説明した。

## 関連

* [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) `この投稿`