🙌

PHPで名前付き引数を使い、Scalaのcopyメソッドを模倣する

2021/11/08に公開

はじめに

オブジェクトをimmutableにしたい時、phpで書くならばreturn new self (...)みたいにして「内部プロパティは変更せず、新しいオブジェクトを返す」という場面があると思います。

class Room
{
    public function __construct(private RoomId $roomId, private Status $status, private ReservationName $name) 
    {
        // 不変条件
    }

    public function reserve(): self
    {
        // 制約

        return new self($this->roomId, $status->reserve(), $this->name);
    }
}

$room = new Room(/* 引数 */);

$reservedRoom = $room->reserve();

この時new self(...)の引数には、変更したいものはstatusだけなのに、引数全てを渡す必要があります。こういうメソッドが増えてくると非常に冗長です。

一方で、Scalaにはcopyメソッドというものがあります。僕はPHPerなのでサンプルコードが書けないのですが、copy(status = 変更後の値)の様に書くと、指定したプロパティだけ変更したcopyを作る事ができるそうです。
詳細の説明はドキュメントを参照してください。

PHPでも同じ様な事ができれば、先述の様なコードが非常にスッキリしそうです。

「名前付き引数」とは

php8から導入された機能です。

既にある、位置を指定した引数を拡張する形で、PHP 8.0.0 から名前付き引数が導入されました。 名前付き引数は、位置ではなく、名前ベースで引数を渡すことを可能にします。 これによって、引数の意味が自己文書化(self-documenting)され、 引数を任意の順番で渡せるようになり、任意のデフォルト値を持つ引数をスキップできるようになります。
出典: PHP: 関数の引数 - Manual > 名前付き引数

この「名前付き引数」と「デフォルト引数値」と「null合体演算子」を組み合わせると、Salaのcopyメソッドを模倣する事ができます。

どうやるか

Roomクラスを以下の様に変更します。

class Room
{
    public function __construct(private RoomId $roomId, private Status $status, private ReservationName $name)
    {
        // 不変条件
    }

    public function reserve(): self
    {
        // 制約

	// copy呼び出すよう変更↓
        return $this->copy(status: $this->status->reserve());
    }

        // 追加
    private function copy(?RoomId $roomId = null, ?Status $status = null, ?ReservationName $name = null): self
    {
        return new self(
            $roomId ?? $this->roomId,
            $status ?? $this->status,
            $name   ?? $this->name
        );
    }
}

新しいインスタンスを作成したい場合は変更したい値だけ「名前付き引数」に指定して渡せば、他の引数は元のプロパティが使用されます。

注意点として、このcopyメソッドをpublicにしてしまうと、各メソッドで表現したい制約(=ビジネスルール)を無視したインスタンスが作られてしまう可能性があるので、privateにしておくことが望ましそうです。

Discussion