グローバル状態とシングルトン
注意: 次のような書き方は、設計のよくないコードの症状です。
Foo::getInstance()DB::insert(...)Article::setDb($db)ClassName::$varまたはstatic::$var
あなたのコードにこうした書き方はありますか。もしあるなら、改善の余地があります。よくある書き方で、さまざまなライブラリやフレームワークの解決例でも見かける、と思うかもしれません。もしそうなら、それらのコードの設計には欠陥があります。
ここで学問的な純粋さの話をしているのではありません。これらの書き方にはひとつの共通点があります。グローバル状態を使っていることです。そしてグローバル状態はコードの品質に有害な影響を与えます。クラスは自分の依存関係について嘘をつくようになります。コードは予測できなくなります。開発者を混乱させ、その効率を下げます。
この章では、なぜそうなるのか、そしてどうやってグローバル状態を避けるのかを説明します。
グローバルな絡み合い
理想の世界では、オブジェクトは直接渡されたオブジェクトとだけやり取りするはずです。オブジェクト
A と B を作り、両者のあいだで参照を一度も渡さなければ、A も
B
も互いの状態にアクセスしたり変えたりできません。これはコードにとって非常に望ましい性質です。電池と電球のようなもので、電球は導線で電池につながないと光りません。
しかしグローバル(静的)変数やシングルトンでは、そうはいきません。オブジェクト
A は参照を渡されなくても、C::changeSomething()
を呼ぶことで無線でオブジェクト C にアクセスして変更できます。オブジェクト
B もグローバルな C に手を伸ばせば、A と B は C
を通じて互いに影響し合えてしまいます。
グローバル変数を使うと、外からは見えない無線の結合という新しい形が持ち込まれます。それは煙幕を作り、コードの理解と利用を難しくします。依存関係を本当に把握するには、開発者はクラスのインターフェースを頼りにするのではなく、ソースコードの一行一行を読まなければなりません。しかもこの結合はまったく不要なものです。グローバル状態が使われるのは、どこからでも簡単にアクセスでき、たとえばグローバル(静的)メソッド
DB::insert()
でデータベースに書き込めるからです。しかしこれから示すとおり、その手軽さは、それがもたらす深刻な面倒に比べればごくわずかなものです。
振る舞いという点で、グローバル変数と静的変数に違いはありません。どちらも同じくらい有害です。
遠隔からの不気味な作用
「遠隔からの不気味な作用」。アルベルト・アインシュタインが、量子物理学のある現象を指してそう呼んだのは有名です。彼はそれに不気味さを感じていました。 それは量子もつれのことで、一方の粒子の性質を測ると、たとえ何百万光年離れていても、もつれたもう一方の粒子に瞬時に影響します。光より速く伝わるものはないという宇宙の基本法則に、一見反しているように見えます。
ソフトウェアの世界では、「遠隔からの不気味な作用」は次のような状況を指します。(依存関係を明示的に渡していないので)独立していると思っていた処理を実行したのに、システムの遠い場所で思いがけないやり取りと状態の変化が、こちらの知らないうちに起きるのです。これはグローバル状態を通じてしか起こりません。
大きく成熟したコードベースを持つプロジェクトの開発チームに加わったところを想像してください。新しいリーダーから機能をひとつ実装するよう頼まれ、良い開発者らしく、まずテストを書き始めます。ただしプロジェクトに慣れていないので、「このメソッドを呼んだら何が起こるのか」といった探りのテストをたくさん書きます。そして次のようなテストを書いてみます。
function testCreditCardCharge()
{
$cc = new CreditCard('1234567890123456', 5, 2028); // あなたのカード番号
$cc->charge(100);
}
コードを何度か実行し、しばらくすると携帯に銀行の通知が届きます。実行のたびにクレジットカードから 100 ドルが引き落とされていたのです。🤦♂️
いったいどうしてテストが実際の請求を引き起こせるのでしょうか。クレジットカードを扱うのは単純ではありません。第三者のウェブサービスとやり取りし、その URL を知り、認証する必要があります。そうした情報はテストのどこにも書かれていません。さらに悪いことに、その情報がどこにあるかも分からないので、外部の依存関係をモックに置き換えて、テストのたびに 100 ドル取られるのを防ぐこともできません。新入りの開発者に、これからやろうとしていることが 100 ドルの損失につながると、どうやって分かれというのでしょうか。
これが遠隔からの不気味な作用です。
あなたは大量のソースコードをふるいにかけ、先輩に相談してプロジェクトの絡み合いを理解するはめになります。この困難が生じるのは、CreditCard
クラスのインターフェースが、必要なグローバル状態の初期化を明かさないからです。クラスのソースコードを見ても、どの初期化メソッドを呼ぶべきか分からないかもしれません。よくても、アクセスされているグローバル変数を見つけて、その初期化の仕方を推し量ろうとするくらいです。
こうしたプロジェクトのクラスは、病的な嘘つきです。CreditCard
クラスは、ただインスタンス化して charge()
メソッドを呼べばよいかのように振る舞います。しかし裏では、決済ゲートウェイを表す別のクラス
PaymentGateway とやり取りしています。PaymentGateway
のインターフェースも独立して初期化できるように見えるかもしれませんが、実際には設定ファイルから認証情報を引っ張ってきていたりします。もとの開発者たちは
CreditCard が PaymentGateway
を必要とすることを分かっています。そういうコードを書いたのですから。しかし新しく来た人にとっては完全な謎で、学ぶことにも貢献することにも立ちはだかります。
この状況をどう直せばよいでしょうか。簡単です。API に依存関係を宣言させるのです。
function testCreditCardCharge()
{
$gateway = new PaymentGateway(/* ... */);
$cc = new CreditCard('1234567890123456', 5, 2028);
$cc->charge($gateway, 100);
}
コードの中の相互依存が一目で分かるようになったことに注目してください。charge()
メソッドが PaymentGateway
を必要とすると宣言しているので、この依存関係を推し量ったり人に聞いたりする必要はもうありません。インスタンスを作る必要があると分かり、その過程で必要なアクセス情報にも行き当たります。それがなければコードはそもそも動きません。
そして何より、決済ゲートウェイをモックにできるので、テストを走らせるたびに 100 ドル取られることもなくなります。
グローバル状態は、API に宣言されていない依存関係にオブジェクトがこっそりアクセスすることを許し、あなたの API を病的な嘘つきに変えてしまいます。
これまでそう考えたことはなかったかもしれませんが、グローバル状態を使うたびに、あなたは秘密の無線通信路を作っているのです。この遠隔からの不気味な作用は、起こり得るやり取りを理解するために開発者にコードの一行一行を読むことを強い、生産性を下げ、新しいチームメンバーを混乱させます。そのコードを書いたあなたは本当の依存関係を知っていますが、あとから来る人には何も分かりません。
グローバル状態に頼るコードを書くのはやめ、依存関係を明示的に渡すようにしましょう。依存性注入を受け入れてください。
グローバル状態のもろさ
グローバル状態やシングルトンを使ったコードでは、その状態がいつ誰によって変えられたのか、決して確信が持てません。この危険は初期化のときにも現れます。次のコードはデータベース接続を作って決済ゲートウェイを初期化しようとしていますが、何度も例外を投げ、その原因を追うのはきわめて骨が折れます。
PaymentGateway::init();
DB::init('mysql:', 'user', 'password');
コードを丹念に追ってようやく、PaymentGateway
オブジェクトが無線でほかのオブジェクトにアクセスしていて、その一部がデータベース接続を必要とすることが分かります。ですからデータベースは
PaymentGateway
より先に初期化しなければなりません。しかしグローバル状態の煙幕がそれを隠します。これらのクラスの
API が正直で依存関係を宣言していたら、どれだけの時間が節約できたでしょうか。
$db = new DB('mysql:', 'user', 'password');
$gateway = new PaymentGateway($db, /* ... */);
データベース接続にグローバルにアクセスする場合も、同じような問題が起こります。
use Illuminate\Support\Facades\DB;
class Article
{
public function save(): void
{
DB::insert(/* ... */);
}
}
save()
メソッドを呼ぶとき、データベース接続が確立されているのか、誰がそれを確立する責任を持つのかは分かりません。(テストなどのために)データベース接続を動的に変える必要が出てきたら、DB::reconnect(...)
や DB::reconnectForTest() のようなメソッドを足すことになるでしょう。
例を見てみましょう。
$article = new Article;
// ...
DB::reconnectForTest();
Foo::doSomething();
$article->save();
$article->save()
が呼ばれたとき、本当にテスト用のデータベースが使われていると、どうして確信できるでしょうか。Foo::doSomething()
メソッドがグローバルなデータベース接続を変えていたら? それを確かめるには Foo
の、そしておそらくほかの多くのクラスのソースコードを調べる必要があります。しかもその調査は一時的な答えしかもたらしません。状況はあとで変わり得るからです。
データベース接続を Article クラスの中の静的変数に移したらどうでしょうか。
class Article
{
private static DB $db;
public static function setDb(DB $db): void
{
self::$db = $db;
}
public function save(): void
{
self::$db->insert(/* ... */);
}
}
何ひとつ変わりません。問題はグローバル状態そのものであって、それがどのクラスの中に隠れているかは関係ありません。この場合も先ほどと同じく、$article->save()
が呼ばれたとき、データがどのデータベースに書き込まれるのか確信が持てません。アプリケーションのどこにいる誰でも、Article::setDb()
を使っていつでもデータベースを変えられたのです。私たちの知らないうちに。
グローバル状態は、アプリケーションをきわめてもろくします。
しかしこの問題に対処する簡単な方法があります。正しく機能するために必要なものを、API に依存関係として宣言させればよいのです。
class Article
{
public function __construct(
private DB $db,
) {
}
public function save(): void
{
$this->db->insert(/* ... */);
}
}
$article = new Article($db);
// ...
Foo::doSomething();
$article->save();
この方法なら、データベース接続が隠れて思いがけず変わる心配がなくなります。記事がどこに保存されるのかを確信でき、無関係なクラスでの変更がそれに影響することもありません。コードはもろさを失い、安定します。
グローバル状態に頼るコードを書くのはやめ、依存関係を明示的に渡すようにしましょう。依存性注入を受け入れてください。
シングルトン
シングルトンはデザインパターンのひとつで、有名な Gang of Four の本による定義によれば、クラスのインスタンスをひとつに制限し、それへのグローバルなアクセスを提供します。このパターンの実装は、ふつう次のようなコードになります。
class Singleton
{
private static self $instance;
public static function getInstance(): self
{
self::$instance ??= new self;
return self::$instance;
}
// そしてクラスの機能を果たすほかのメソッド
}
残念ながら、シングルトンはアプリケーションにグローバル状態を持ち込みます。そして上で示したとおり、グローバル状態は望ましくありません。だからこそシングルトンはアンチパターンと見なされます。
コードでシングルトンを使わず、ほかのしくみで置き換えてください。本当にシングルトンは必要ありません。ただし、アプリケーション全体でクラスのインスタンスがひとつだけであることを保証したいなら、その責任は
DI
コンテナに委ねましょう。こうしてアプリケーションのスコープを持つシングルトン、一般にサービスと呼ばれるものができます。クラス自身は自分の唯一性を管理する務めから解放され(つまり
getInstance()
メソッドも静的なインスタンスのプロパティも持たなくなり)、自分の責務だけに集中できます。こうして単一責任の原則を破らずに済むようになります。
グローバル状態とテスト
テストを書くとき、私たちは理想的には、各テストが独立した単位であり、外部の状態が入ることも出ることもないと想定します。テストが終われば、それに関わる状態はガベージコレクタが自動的に片づけるはずです。これがテストを独立させます。ですからテストはどんな順序でも実行できます。
しかしグローバル状態やシングルトンがあると、この都合のよい前提は崩れます。状態はテストに漏れ込み、また漏れ出します。とたんに、テストの順序が問題になり得ます。
シングルトンを含むコードをテストできるようにするためだけに、開発者はしばしばその健全さを妥協せざるを得ません。たとえばシングルトンのインスタンスを差し替えられるようにする、といった具合です。そうした解はよくても場当たりで、保守と理解が難しいコードにつながります。グローバル状態を変えるテスト(やその
tearDown() メソッド)は、その変更を丹念に戻さなければなりません。
グローバル状態は、ユニットテストにおける最大の頭痛の種です。
どう直せばよいでしょうか。簡単です。シングルトンを使うコードを書くのはやめ、依存関係を明示的に渡すようにしましょう。依存性注入を受け入れてください。
グローバル定数
グローバル状態はシングルトンや静的変数の利用に限りません。グローバル定数にも当てはまります。
普遍的な事実を表す値(M_PI)や、それ自体で完結した情報を与える定数(PREG_BACKTRACK_LIMIT_ERROR)は、一般に問題ありません。逆に、情報をコードに無線で注入する手段として使われる定数は、実質的に隠れた依存関係です。次の例の
LOG_FILE がそうです。FILE_APPEND 定数の使い方はまったく正しいものです。
const LOG_FILE = '...';
class Foo
{
public function doSomething()
{
// ...
file_put_contents(LOG_FILE, $message . "\n", FILE_APPEND);
// ...
}
}
代わりに、ログファイルのパスを Foo
クラスのコンストラクタのパラメータとして宣言し、その API の明示的な一部にすべきです。
class Foo
{
public function __construct(
private string $logFile,
) {
}
public function doSomething()
{
// ...
file_put_contents($this->logFile, $message . "\n", FILE_APPEND);
// ...
}
}
これでログファイルへのパスを明示的に渡すようになりました。必要に応じて簡単に変えられ、テストもコードの保守も楽になります。
グローバル関数と静的メソッド
強調しておきたいのは、静的メソッドやグローバル関数を使うこと自体は本質的に問題ではない、という点です。DB::insert()
のようなメソッドの問題を説明しましたが、核心はいつも、その背後にあるグローバル状態、たいていは静的変数に保存された状態でした。DB::insert()
メソッドは、データベース接続を保持する静的変数に頼っています。その変数がなければ、このメソッドは実装できません。
Closure::fromCallable()、strlen()
など、決定的な静的メソッドや関数を使うことは、依存性注入と完全に両立します。これらの関数は、同じ入力に対して常に同じ結果を返すので予測できます。グローバル状態をまったく使いません。
とはいえ PHP には決定的でない関数もあります。たとえば htmlspecialchars()
関数です。第 3 パラメータの $encoding を省くと、設定オプション default_charset
の値(ini_get('default_charset'))が既定になります。ですから予測できない振る舞いを防ぐため、このパラメータは常に指定することをおすすめします。Nette
は一貫してそうしています。
strtolower() や strtoupper()
のような一部の関数は、少し前まではロケールの設定(setlocale())に応じて決定的でない振る舞いをしていました。これは多くの面倒を生み、とりわけトルコ語を扱うときに問題になりました。トルコ語では小文字にも大文字にも、点のある
I と点のない I の区別があるからです。その結果 strtolower('I') は
ı(点のない小文字の i)を返し、strtoupper('i') は
İ(点のある大文字の
I)を返して、原因不明のアプリケーションのエラーを数多く引き起こしました。ただしこの問題は
PHP 8.2 で修正され、これらの関数はもうロケールに依存しません。
これは、グローバル状態(ロケールの設定)が世界中の何千もの開発者を悩ませた良い例です。最終的な解は、関数をロケールに依存しないものにすること、つまり隠れた依存関係を取り除くことでした。
グローバル状態を使ってもよいのはどんなときか
グローバル状態を使ってもよい、限られた特定の場面はあります。たとえばデバッグ中に、変数の値をダンプしたり、あるコードの実行時間を測ったりする場合です。あとでコードから取り除く一時的な処置であれば、グローバルにアクセスできるダンパーやタイマーを使うのは正当です。これらの道具はアプリケーションの設計の中核ではありません。
もうひとつの例が PHP
の正規表現の関数(preg_*)で、コンパイル済みの正規表現を内部で静的メモリにキャッシュしています。コードの中で同じ正規表現の関数を何度も呼んでも、その式がコンパイルされるのは一度きりです。このキャッシュは性能を高め、利用者からはまったく見えないので、こうした内部の静的な状態の使い方は一般に問題ありません。
まとめ
なぜ次のことに意味があるのかを見てきました。
- コードから変更可能な静的プロパティ(グローバル状態)をすべて取り除く
- 依存関係を明示的に宣言する
- そして依存性注入を活用する
コードを設計するときは、変更可能な static $foo
のひとつひとつが問題の種になり得ることを思い出してください。DI
と相性のよい環境を作るには、グローバル状態を完全に取り除き、依存性注入に置き換えることが決定的に重要です。
その過程で、複数の責務を持つクラスを分ける必要に気づくかもしれません。ためらわずにそうしてください。単一責任の原則を目指しましょう。
Flaw: Brittle Global State & Singletonsなどの記事がこの章の土台になっている Miško Hevery 氏に感謝します。