PHP
PHPでオブジェクト指向
PHPのデザインパターン

PHP: イミュータブルなオブジェクトの実装方法(属性が多いとき)

本稿ではオブジェクトの属性(プロパティ)の数が多い場合に、どうやってイミュータブルなオブジェクトを実装したらいいかについて解説する。

本稿で例示したコードの完全版はGitHubにて公開している。

前回、イミュータブルなオブジェクトの実装方法について書いたが、この実装方法はオブジェクトの属性が少ないときは問題ない。しかし、属性が3つ4つ…と増えるに連れて実装が難しくなる。

例えば、次のようなクラスを考えてみると分かる。このProductクラスは属性が5つある。

final class Product
{
    private $name;
    private $description;
    private $price;
    private $colorCode;
    private $sizeCode;

    public function __construct(
        string $name,
        string $description,
        int $price,
        string $colorCode,
        string $sizeCode
    ) {
        $this->name = $name;
        $this->description = $description;
        $this->price = $price;
        $this->colorCode = $colorCode;
        $this->sizeCode = $sizeCode;
    }
    // ...
}

イミュータブルなオブジェクトにしようとしたら、次のように変更したい属性の値を引数で受け取るが、変更しない属性は新しいオブジェクトでも引き継ぐ必要があるため、コンストラクタにいちいち渡していかなければらない。

    public function setColorCode(string $colorCode): self
    {
        return new self(
            $this->name,
            $this->description,
            $this->price,
            $colorCode,
            $this->sizeCode
        );
    }

こうしたメソッドが一つだけならいいが、いくつもあると似たような長いコードを持つメソッドをいくつも生やさないとならない。例えば次のように:

    public function setName(string $name): self
    {
        return new self(
            $name,
            $this->description,
            $this->price,
            $this->colorCode,
            $this->sizeCode
        );
    }

    public function setDescription(string $description): self
    {
        return new self(
            $this->name,
            $description,
            $this->price,
            $this->colorCode,
            $this->sizeCode
        );
    }

    public function setPrice(int $price): self
    {
        return new self(
            $this->name,
            $this->description,
            $price,
            $this->colorCode,
            $this->sizeCode
        );
    }

    public function setColorCode(string $colorCode): self
    {
        return new self(
            $this->name,
            $this->description,
            $this->price,
            $colorCode,
            $this->sizeCode
        );
    }

    public function setSizeCode(string $sizeCode): self
    {
        return new self(
            $this->name,
            $this->description,
            $this->price,
            $this->colorCode,
            $sizeCode
        );
    }

こうなってくると、オブジェクトにプロパティを増やすときもすべてのメソッドを変更する必要が出てきて厄介だ。

この解決策としては、newを使うのではなくclone1を使う方法がある。clone $thisをすると$thisと同じ値を持った別のインスタンスを得られる。その新たなインスタンスに対して、変更したい属性をセットするようにする:

final class Product
{
    // ...

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

    // ...
}

こうすることで、古いオブジェクトの属性値を引き継ぎつつ、変更したい属性だけに作用するコードになる。もしも、オブジェクトに属性が加わったとしても、上のようなメソッドは影響を受けにくい。


  1. cloneキーワードはいわゆるシャローコピーだ。したがって、インスタンス自体は別モノにコピーされるが、インスタンスが持っているプロパティがオブジェクトの場合、そのプロパティはコピー元のインスタンスと共有された状態になる。しかし、そのプロパティ自体もイミュータブルなオブジェクトであれば、ディープコピーする必要は特にないと考える。この見解は『実践ドメイン駆動設計』における値オブジェクトのコピーコンストラクタの考え方と同じである。 

    もうひとつのコンストラクタは、既存の値をコピーして新しい値を作るために使うもので、コピーコンストラクタと呼ばれる。このコンストラクタが行うのは、いわゆるシャローコピーだ。自己委譲によって自身のプライマリコンストラクタを呼び、そのパラメータとして、対応する自身の各属性を渡す。すべての属性/プロパティをコピーして渡し、まったく別のオブジェクトではあるけれども値は等しいものを作るという、いわゆるディープコピー(クローン)を行うこともできる。しかし、これは複雑な処理になってしまうし、値を扱う場合にはその必要はないことが多い。それでもディープコピーが必要だというのなら、その処理を追加すればいい。ただ、不変な値を扱うときには、インスタンス間で属性/プロパティを共有したところでなんら問題はないはずだ

    ヴァーン・ヴァーノン (2015) 『実践ドメイン駆動設計』 (高木正弘訳) 「第6章 値オブジェクト」 翔泳社.