静的メソッドとテスタビリティ、便利さの裏にある差し替えにくさ
静的メソッドとはインスタンス不要で呼べる便利なメソッドですが、外部状態を隠すと差し替えできずテストしづらくなるため、純粋な処理に絞るのが安全です。
staticは呼びやすいが縛られる
ユーティリティを静的メソッドで書くのは手軽で、import なしにどこからでも呼べます。ただ、そのお手軽さがそのままテストのやりにくさに化けることがあるんですよね。呼び出し側は具体的なクラス名に直結してしまうので、差し替えの余地がなくなります。
class Clock
{
public static function now(): DateTimeImmutable
{
return new DateTimeImmutable();
}
}
class Coupon
{
public function isExpired(): bool
{
return Clock::now() > $this->expiresAt; // 現在時刻に依存
}
}
この isExpired をテストしたくても、Clock::now() が常に本物の現在時刻を返すので「期限切れの状態」を作れません。時間という外部状態に直結しているわけです。
静的メソッドはグローバル状態を呼び込みやすい
静的メソッドそのものが悪ではなく、問題は多くの場合「内部で共有状態や外部リソースを触る」点にあると思います。純粋な計算、たとえば入力だけで出力が決まる関数なら static でも困りません。困るのは現在時刻、乱数、DB、設定など、テストで固定したくなるものを static の奥に隠したときですね。
// これは static でも問題ない(純粋関数)
class Slug
{
public static function from(string $title): string
{
return strtolower(trim(preg_replace('/[^a-z0-9]+/i', '-', $title), '-'));
}
}
差し替えたい依存はインスタンスにする
先ほどの時計は、インターフェースにしてインスタンスとして注入すれば固定できます。テストでは好きな時刻を返す実装を渡せるので、期限切れの検証がすんなり書けます。
interface Clock { public function now(): DateTimeImmutable; }
class FixedClock implements Clock
{
public function __construct(private DateTimeImmutable $t) {}
public function now(): DateTimeImmutable { return $this->t; }
}
class Coupon
{
public function __construct(private Clock $clock) {}
public function isExpired(): bool
{
return $this->clock->now() > $this->expiresAt;
}
}
まとめ
静的メソッドは純粋な処理には向いていて、そこまで神経質になる必要はないと思います。線引きは「テストで固定したい外部状態を触るかどうか」で、触るものはインスタンス化して注入する、触らないものは static のままで気楽にいく。この使い分けができると、便利さとテスト容易性を両取りできる気がします。
よくある質問
Q. 静的メソッドは使ってはいけないのですか?
A. そんなことはありません。入力だけで出力が決まる純粋な処理なら、staticのままで気楽に使って問題ありません。
Q. どういうときに困るのですか?
A. 現在時刻・乱数・DB・設定など、テストで固定したい外部状態をstaticの奥に隠したときです。
Q. 差し替えたい依存はどうすればいいですか?
A. インターフェースにしてインスタンスとして注入すれば、テストで好きな値を返す実装を渡せます。