🧌

頼む。。。Enumを使うときは区分値は外から使わないでくれ。。。区分値はEnum内部だけで使うようにしてくれ。。本当に頼む。。

に公開

はじめに

みなさん!こんにちは tyamahoriです。ゲームボーイのポケモンは好きですか?僕は好きです。僕の子供時代の全てであったといっても過言ではありません。裏技でレベル100にしたり、不思議なアメを100個生成していたりしました。

結論

Enumを使うときは、Enumの外から区分値を使わずに生成しましょう。区分値の変更に弱くなります。ここを読んで伝わる方は、ここで離脱を推奨いたします。以下は、結論を説明するための具体例が続きます。

サンプルコード

今回の説明用にサンプルを用意しました。Enumの例をPHPで実装しています。

PHPによるEnumのサンプルコード
<?php

declare(strict_types=1);

namespace LaravelOrbStack\Samples;

use InvalidArgumentException;

/**
 * サンプルのEnumです。みんなゲームボーイでポケモンやったよね。。?
 */
enum ポケモンゲーム: string
{
    case= '赤';
    case= '緑';
    case= '青';
    

    public static function ここに3匹のポケモンがおるじゃろ?好きなのを選ぶのじゃ(string $ポケモン名): self
    {
        $ポケモンゲーム = self::tryFrom($ポケモン名);
        if ($ポケモンゲーム === null) {
            throw new InvalidArgumentException('これはゲームのEnumなんじゃ。。ポケモンを選んではいかんのじゃ。。');
        }

        return $ポケモンゲーム;
    }

    public static function 初めて買い与えられるバージョン(): self
    {
        return self::;
    }

    public static function 思い入れがあるバージョン(): self
    {
        return self::;
    }

    public function 初代シリーズ?(): bool
    {
        return match ($this) {
            self::, self::, self::=> true
        };
    }

    public function 通信ケーブルを親から買い与えられたものの何故か兄弟喧嘩して親に通信ケーブルを取り上げられズタボロに切り裂かれた経験があるよね・・・?(): bool
    {
        return match ($this) {
            self::, self::, => true,
            self::=> false
        };
    }

    public function オーキド博士からもらう最初の御三家ポケモンは絶対にヒトカゲだよなぁ?(): bool
    {
        return match ($this) {
            self::, self::, => true,
            self::=> false
        };
    }
}
PHPによるEnumのサンプルコードのテスト例
<?php

declare(strict_types=1);

namespace LaravelOrbStack\Samples\Test;

use InvalidArgumentException;
use LaravelOrbStack\Samples\ポケモンゲーム;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

class ポケモンゲームTest extends TestCase
{
    #[Test]
    public function 好きなポケモンを選んだらエラーになる(): void
    {
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage('これはゲームのEnumなんじゃ。。ポケモンを選んではいかんのじゃ。。');
        ポケモンゲーム::ここに3匹のポケモンがおるじゃろ?好きなのを選ぶのじゃ('ヒトカゲ');
    }

    #[Test]
    public function 初代シリーズかを判断できる(): void
    {
        $ポケモンゲーム = ポケモンゲーム::;

        self::assertTrue($ポケモンゲーム->初代シリーズ?());
    }

    #[Test]
    public function 初めて買い与えられたバージョンは緑(): void
    {
        $ポケモンゲーム = ポケモンゲーム::初めて買い与えられるバージョン();

        self::assertSame(ポケモンゲーム::, $ポケモンゲーム);
    }

    #[Test]
    public function 思い入れがあるバージョンは赤(): void
    {
        $ポケモンゲーム = ポケモンゲーム::思い入れがあるバージョン();

        self::assertSame(ポケモンゲーム::, $ポケモンゲーム);
    }

    #[Test]
    public function 初代シリーズは赤青緑(): void
    {
        $ポケモンゲーム = ポケモンゲーム::;

        self::assertTrue($ポケモンゲーム->初代シリーズ?());
    }

    #[Test]
    public function 通信ケーブルを親に引き裂かれた。。。(): void
    {
        $ポケモンゲーム = ポケモンゲーム::;

        self::assertTrue($ポケモンゲーム->通信ケーブルを親から買い与えられたものの何故か兄弟喧嘩して親に通信ケーブルを取り上げられズタボロに切り裂かれた経験があるよね・・・?());
    }

    #[Test]
    public function 御三家ポケモンはヒトカゲ(): void
    {
        $ポケモンゲーム = ポケモンゲーム::;

        self::assertTrue($ポケモンゲーム->オーキド博士からもらう最初の御三家ポケモンは絶対にヒトカゲだよなぁ?());
    }
}

区分値の増減に備えよう

区分値が後から増減する可能性に備えましょう。増えない、減らないと高を括ると後で泣きを見ます。

悪い例

コードは以下のとおりです。

#[Test]
#[DataProvider('初代パターン')]
public function 区分値の増減に備えよう。まずは悪い例(ポケモンゲーム $ポケモンゲーム): void
{
    $初代かどうか = in_array($ポケモンゲーム, [ポケモンゲーム::, ポケモンゲーム::, ポケモンゲーム::], true);

    self::assertTrue($初代かどうか);
}

/**
 * @return iterable<value-of<ポケモンゲーム>, array{0: ポケモンゲーム}>
 */
public static function 初代パターン(): iterable
{
    return [
        ポケモンゲーム::->value => [ポケモンゲーム::],
        ポケモンゲーム::->value => [ポケモンゲーム::],
        ポケモンゲーム::->value => [ポケモンゲーム::],
    ];
}

初代シリーズかを判定する処理をin_arrayを使って生成してみました。ここで質問です。ピカチュウバージョンは初代に含めますか。。?
リメイクのファイアレッドやリーフグリーンはどうでしょうか。判断はしなくてはいけません。皆さんのご意見はあるかなと思いますが、ここでは全て初代シリーズとさせてください。

ただ、in_arrayを使って至るところにハードコードで初代かどうかを判断していた場合、全て見直しが必要です。。修正が手間になります。

改善例

Enum側にロジックをもたせましょう。

// ポケモンゲームenum
enum ポケモンゲーム: string
{
    case= '赤';
    case= '緑';
    case= '青';
    case ピカチュウ = 'ピカチュウ';
    case ファイアレッド = 'ファイアレッド';
    case リーフグリーン = 'リーフグリーン';

    public function 初代シリーズ?(): bool
    {
        return match ($this) {
            self::, self::, self::, self::ピカチュウ, self::ファイアレッド, self::リーフグリーン => true,
        };
    }
}

実装当初は緑、赤、青でしたが、ここにピカチュウバージョン、ファイアレッドやリーフグリーンを追加します。
その後、初代シリーズかどうかを判断したい場合はEnumで定義している初代シリーズ?()を呼び出せば大丈夫ですね。金・銀・ルビー・サファイアが追加されても、Enum内部での対応で済みそうです。

#[Test]
public function 初代シリーズは赤青緑(): void
{
    $ポケモンゲーム = ポケモンゲーム::;

    self::assertTrue($ポケモンゲーム->初代シリーズ?());
}

Enumを生成する際は物語を表そう

悪い例

#[Test]
public function ダイレクトに区分値を利用している悪い例(): void
{
    $ポケモンゲーム = ポケモンゲーム::;

    self::assertSame(ポケモンゲーム::,$ポケモンゲーム);
}

タイトルでもあった通りEnumを区分値を使って生成しています。ここで注目したいのは、ポケモンゲーム::赤の部分なのですが、なぜなのかはこれからは意図が読み取れません。

改善例

Enum側にstaticメソッドや定数をもたせましょう。


public const self 初めて遊んだバージョン = self::;

public static function 初めて買い与えられるバージョン(): self
{
    return self::;
}

一気に物語が伝わります。状況によっては赤かもしれませんし、ピカチュウかもしれません。修正はEnum内に閉じ込められるので、変更はしやすいです。

#[Test]
public function 初めて買い与えられたバージョンは緑(): void
{
    $買い与えられたポケモンゲーム = ポケモンゲーム::初めて買い与えられるバージョン();
    $初めて遊んだポケモンゲーム = ポケモンゲーム::初めて遊んだバージョン;

    self::assertSame(ポケモンゲーム::, $買い与えられたポケモンゲーム);
    self::assertSame(ポケモンゲーム::, $初めて遊んだポケモンゲーム);
}

テストでは、メソッドや定数を確認すれば良さそうです。

おわりに

Enumの使い方について持論を述べてみました。

  • 区分値は増減があり得る
  • Enumの生成時には物語や意図を明示的に示す

上記の点を意識されてみてはいかがでしょうか。皆様のより良いEnumライフをお祈りしています。

Discussion