PHP: Builderパターンの実装手順 #3【Immutable Builder実装】

前回は、Builderパターンにfluent interfaceを取り入れると、Builderを使う側のコード(クライアントコード)の可読性が高まることを説明した。

前回: [PHP: Builderパターンの実装手順 #2【Fluent Interface実装】 - Qiita](https://qiita.com/suin/items/888f59851f940ee97c3a)


今回は、それを更に発展させた形としてイミュータブル(immutable)なBuilderを実装する手順を説明する。なお、本稿で実装したコードの完全版は[GitHub](https://github.com/suin/php-playground/tree/master/BuilderPattern/No3ImmutableBuilder)で公開している。

## イミュータブルとは？

イミュータブルとは日本語では不変と訳される。不変とは読んで字のごとく、変わらないことである。オブジェクトにおいては、オブジェクトが生成されてから消滅されるまで、そのプロパティが一切変更されない性質[^少なくとも外見上は]を持つオブジェクトはイミュータブルなオブジェクトということになる。PHPでは[`DateTimeImmutable`](http://php.net/manual/ja/class.datetimeimmutable.php)や`Exception`はimmutableなオブジェクトである。

[^少なくとも外見上は]: 完全にプロパティの変更がないものに限らず、少なくともクライアントコードからはイミュータブルに見えればイミュータブルな性質を持つオブジェクトとみなせる。

## PHPでオブジェクトをイミュータブルにする方法は？

セッターを持つオブジェクトをイミュータブルにする方法は、下記の投稿で詳しく説明した:

* [PHP: イミュータブルなオブジェクトの実装方法 - Qiita](https://qiita.com/suin/items/56859f5b5f6f962e2744)
* [PHP: イミュータブルなオブジェクトの実装方法(属性が多いとき) - Qiita](https://qiita.com/suin/items/6c8a841643269059378a)

概要だけいうと、セッターは`$this`から新しいオブジェクトを作り、新オブジェクトの値を変更し、それを返すようにする。そうすれば、イミュータブルなオブジェクトにすることができる:

```php
final class Product
{
    private $name;

    public function setName(string $name): self
    {
        $new = clone $this;
        $new->name = $name;
        return $new;
    }
}
```

## EmailBuilderをイミュータブルにする

### イミュータブルじゃないBuilder実装の振り返り

前回までに作った`EmailBuilder`をイミュータブル化するにあたって、前回の実装を振り返っておこう。前回は、`setFrom`などのセッターをfluent interfaceにした。そのため、各セッターは自インスタンス(`$this`)を返すようにした。fluent interface化の変更影響は`EmailBuilder`だけで`Email`クラスは第1回から変更されていない。

```php
final class EmailBuilder
{
    /**
     * @var string[]
     */
    private $from = [];

    // ...他のプロパティ...

    public function setFrom(string ...$from): self
    {
        $this->from = $from;
        return $this;
    }
    
    // ...他のセッター...

    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
        );
    }

   // ...assert系メソッド...
}
```

### イミュータブル化するにあたっての影響範囲

この`EmailBuilder`をイミュータブルにする際も、`Email`クラスに手を加える必要はない。また、`EmailBuilder`のクライアントコードも変更する必要がない。つまり、テストコード`EmailTest`はイミュータブル化した`EmailBuilder`のテストに使うことができる。

### イミュータブルにする方法

`EmailBuilder`はプロパティ(属性)が多いので、「[PHP: イミュータブルなオブジェクトの実装方法(属性が多いとき)](https://qiita.com/suin/items/6c8a841643269059378a)」で説明したアプローチを取る。

例えば、`setFrom`メソッドは次のように実装を書き換える:

```php
final class EmailBuilder
{
    /**
     * @var string[]
     */
    private $from = [];

    // ...他のプロパティ...

    public function setFrom(string ...$from): self
    {
        $new = clone $this; // コピーした新インスタンスを作る
        $new->from = $from; // 新インスタンスの from を変更する
        return $new; // 新インスタンスを返すようにする
    }

    // ...以下略...
}
```

前回でfluent interface化しておいたので、戻り値の型宣言`self`はそのままでよい。もし、fluent interface化の手順を飛ばしている場合は、戻り値の型宣言を`: void`から`: self`に変更する必要がある。

他のセッターも同様に変更していく:

```php
    public function setTo(string ...$to): self
    {
        $new = clone $this;
        $new->to = $to;
        return $new;
    }

    public function setCc(string ...$cc): self
    {
        $new = clone $this;
        $new->cc = $cc;
        return $new;
    }

    public function setSubject(string $subject): self
    {
        $new = clone $this;
        $new->subject = $subject;
        return $new;
    }

    public function setBody(string $body): self
    {
        $new = clone $this;
        $new->body = $body;
        return $new;
    }
```

以上でBuilderのイミュータブル化は完了となる。

## イミュータブルさを確かめるテストを書く

Builderが本当にイミュータブルになったかを確かめるために、これまでに作った`EmailTest`クラスにテストを追加する。イミュータブルさのテストのしかたはいろいろあると思うが、セッターを叩いて新しいインスタンスが帰ってきていることが確認できれば、イミュータブルさが満たされていると判断できるので、`assertNotSame`でテストしていく:

```php
use PHPUnit\Framework\TestCase;

final class EmailTest extends TestCase
{
    // ...その他のテスト...

    /**
     * @test
     * @testdox EmailBuilderはイミュータブルである
     */
    public function immutability(): void
    {
        $builder1 = new EmailBuilder();
        $builder2 = $builder1->setFrom('alice@example.com');
        $builder3 = $builder1->setFrom('alice@example.com');
        self::assertEquals($builder2, $builder3);  // 状態は同じだけど、
        self::assertNotSame($builder2, $builder3); // インスタンスは異なる。
        self::assertNotSame($builder1, $builder1->setTo('dummy@recipient'));
        self::assertNotSame($builder1, $builder1->setCc('dummy@recipient'));
        self::assertNotSame($builder1, $builder1->setSubject('dummy_subject'));
        self::assertNotSame($builder1, $builder1->setBody('dummy_body'));
    }

    // ...その他のテスト...
}
```

## Builderをイミュータブルにしたことで発生するメリット

Builderをイミュータブルにすると何が嬉しいかというと、**途中まで作っておいたビルダーを再利用できる**というところだ。

たとえば、パスワードリセットメールのひな形を予め作っておける。

```php
$emailTemplate = (new EmailBuilder())
    ->setFrom('webmaster@example.com')
    ->setSubject('Choose a new password')
    ->setBody(
        'Someone requested a new password for ... account.' .
        'Click here to reset your password: https://...' .
        "If you didn't make this request then you can safely ignore this email :)"
    );
```

これを再利用すると、必要な部分を埋めるだけでメールが作れるようになる。また、変えたいところだけ変えることもできる。

```php
$emailForAlice = $emailTemplate
    ->setTo('alice@example.com') // 埋める必要がある部分
    ->setSubject('Please reset your password') // ひな形から変えたい部分
    ->build();
```

このようにセッターを実行しても、`$emailTemplate`はイミュータブルなので、ひな形が書き換わることがない。安心して再利用することができるわけだ。

こうした使い方はテストコードでドキュメント化しておくと良いだろう:

```php
use PHPUnit\Framework\TestCase;

final class EmailTest extends TestCase
{
    /**
     * @test
     * @testdox EmailBuilderはイミュータブルなので再利用できる
     */
    public function builder_is_immutable_so_that_you_can_reuse_it(): void
    {
        // 1. Prepare preconfigured EmailBuilder:
        $emailTemplate = (new EmailBuilder())
            ->setFrom('webmaster@example.com')
            ->setSubject('Choose a new password')
            ->setBody(
                'Someone requested a new password for ... account.' .
                'Click here to reset your password: https://...' .
                "If you didn't make this request then you can safely ignore this email :)"
            );

        // 2. By reusing it you can build different emails:
        $aliceEml = $emailTemplate
            ->setTo('alice@example.com')
            ->setSubject('Please reset your password')
            ->build();
        $bobEml = $emailTemplate
            ->setTo('bob@example.com')
            ->build();

        // Built emails are actually different:
        self::assertSame('Please reset your password', $aliceEml->getSubject());
        self::assertSame('Choose a new password', $bobEml->getSubject());
        self::assertSame(['alice@example.com'], $aliceEml->getTo());
        self::assertSame(['bob@example.com'], $bobEml->getTo());
    }
}
```

## セッターのリファクタリング

今回、セッターをいくつか変更したが、どのセッターも同じような内容の冗長的なコードになったので、リファクタリングしておくことにする。

どのメソッドもだいたい次のような構造として一般化できるので、

```php
    public function set◯◯◯(◯◯◯): self
    {
        $new = clone $this;
        $new->◯◯◯ = $◯◯◯;
        return $new;
    }
```

この構造を抽出した別メソッドを定義し、`copy`メソッドと名付ける:

```php
final class EmailBuilder
{
    // ...
    private function copy(string $key, $value): self
    {
        $new = clone $this;
        $new->{$key} = $value;
        return $new;
    }
    // ...
```

これを各セッターで使うようにする:

```php
final class EmailBuilder
{
    // ...
    public function setFrom(string ...$from): self
    {
        return $this->copy('from', $from);
    }

    public function setTo(string ...$to): self
    {
        return $this->copy('to', $to);
    }

    public function setCc(string ...$cc): self
    {
        return $this->copy('cc', $cc);
    }

    public function setSubject(string $subject): self
    {
        return $this->copy('subject', $subject);
    }

    public function setBody(string $body): self
    {
        return $this->copy('body', $body);
    }
    // ...
```

あとはテストをで動くかを確認して、問題なければリファクタリングは完了となる。

## まとめ

* 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実装】 `この投稿`
* [PHP: Builderパターンの実装手順 #4【Builder経由での生成を強制する】](https://qiita.com/suin/items/feafae06c644f871f7b8)