静的プロパティの共有に注意する

静的プロパティは、クラスのインスタンス全体で一つの値を共有するプロパティです。共有された可変状態になりやすく、意図しない状態汚染に注意が必要な機能です。

staticはインスタンスをまたいで共有される

静的プロパティは、そのクラスのインスタンス全体で一つの値を共有します。便利に見えるのですが、私は「共有された可変状態」だと思って警戒するようにしています。インスタンスごとに独立していると思い込んで書くと、思わぬところで値が引きずられるからですね。

class Counter
{
    public static int $count = 0;
    public int $id;

    public function __construct()
    {
        self::$count++;      // 全インスタンスで共有
        $this->id = self::$count;
    }
}

$a = new Counter();
$b = new Counter();
echo Counter::$count; // 2 (aとbで同じ変数を増やしている)

この例なら意図どおりですが、問題は「共有したくなかったのに共有されていた」ケースです。設定値やキャッシュを何気なく static に置くと、あるインスタンスの操作が別のインスタンスに漏れてしまいます。

状態汚染はテストで露見する

静的プロパティの厄介さが一番出るのはテストですね。一つのテストで書き換えた静的状態が、次のテストにそのまま残ってしまう。テストの実行順で結果が変わる、といった再現しにくいバグの温床になります。どうしても static を持つなら、リセット用の口を用意しておくと救われることがあります。

class Config
{
    private static array $values = [];

    public static function set(string $k, mixed $v): void
    {
        self::$values[$k] = $v;
    }

    // テスト用に状態を戻せる口
    public static function reset(): void
    {
        self::$values = [];
    }
}

そもそも避けられないか考える

多くの場合、静的プロパティで共有していた状態は、インスタンスのプロパティにして依存注入で渡せば済みます。グローバルに一個だけ、が本当に必要な場面は思ったより少ないですね。共有したい設定はコンストラクタで受け取る形にするだけで、テストしやすく追いやすいコードになると思います。static は最後の手段くらいに考えておくのが安全な気がします。

// staticに頼らず、渡す
final class Service
{
    public function __construct(private array $config) {}
    public function timeout(): int
    {
        return $this->config['timeout'] ?? 30;
    }
}

まとめ

静的プロパティはインスタンスをまたいで共有されるので、意図しない状態汚染を起こしやすい機能だと思います。使うなら「共有される可変状態を持っている」と自覚し、リセット手段を用意しておくこと。そして多くの場合は依存注入に置き換えられるので、まずそちらを検討するのがいいという気がします。

よくある質問

Q. 静的プロパティはインスタンスごとに独立しますか。
A. いいえ、クラス全体で一つの値を共有します。独立していると思い込むと値が引きずられる原因になります。

Q. テストで静的状態が残って困ります。
A. リセット用のメソッドを用意しておくと、テスト間で状態を戻せます。テスト実行順に依存するバグを防げます。

Q. 静的プロパティは避けるべきですか。
A. 多くは依存注入でインスタンスに渡せば済みます。グローバルに一個だけ必要な場面は思ったより少ないです。

類似投稿

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です