【70】シングルトンパターンの誘惑に負けない
シングルトン(Singleton)パターンは多くの問題の解決に役立つパターンです。このパターンでは、クラスのインスタンスは必ず 1 つしか生成されません。そのインスタンスは使用前に必ず初期化されます。そしてシングルトンをグローバルアクセスポイントとすることで、設計をシンプルにできます。こう書いていくと良いことずくめのようですが、この「古典的な」デザインパターンに何か短所はあるのでしょうか。
実はたくさんあります。それはよく考えてみるとわかります。確かにシングルトンパターンは魅力的なのですが、私の経験では、このパターンには利点よりも弊害の方が多いと言えます。まずテストの妨げになります。そして保守性の点でも不利です。残念ながらその事実は広く知られているとは言えないため、多くのプログラマを惹きつけているのです。つい使いたい誘惑にかられますが、その誘惑に抵抗しなくてはなりません。
シングルトンパターンに具体的にどんな問題があるかを次にまとめておきます。
- 「必要なインスタンスは 1 つだけ」という要望は、多くの場合推測にすぎない。 クラスを作った時には「インスタンスは絶対に 1 つしかいらないだろう」と考えていても、後にそうもいかなくなることが多いのです。アプリケーションを設計する時にそういう推測をあちこちでしていると、どこかで大きな問題に直面することになります。要件は変化するものです。良いデザインとは、それを織り込んでいるもののことを言います。シングルトンパターンは、要件の変化を織り込んでいるとは言えません。
- 理論的には独立しているはずのコード間に暗黙の依存関係を生んでしまう。 これが問題なのは、まず依存関係が一見してわかりにくいからです。また、生まれてしまう依存関係は、本来不必要なはずです。問題の存在がはっきりするのは、ユニットテストの時です。ユニットテストは疎結合が前提となっていて、また「本物の」実装コードの代わりに、適宜モック(テスト用の偽者)実装に差し替えられることも重要だからです。シングルトンパターンはその妨げになるのです。
- シングルトンパターンは永続化された状態を暗黙のうちに伴う。 これも、ユニットテストの妨げになります。ユニットテストは、どのような順序でもテストが実行できるために、互いに独立でなければなりません。そしてどのテストにおいても、実行前には必ず、プログラムを既知の状態に設定できるはずなのです。不変でない状態を伴うシングルトンを導入してしまうと、それが難しくなります。加えて、グローバルにアクセス可能で、かつ永続化された状態があると、コードから状態の推論が困難になります。特に、マルチスレッド環境でそれは顕著です。
- マルチスレッド環境での使用は特に危険が大きい。 マルチスレッドでは、シングルトンオブジェクトへのアクセスにはロックが必要になりますが、単純なロックでは効率が良いとは言えないため、いわゆる DCLP(Double-Checked Lockingパターン)を使うことが増えています。しかし困ったことに、これだと危険性が高まってしまう恐れがあります。多くの言語で、DCLP はスレッドセーフでないということがわかっているからです。にもかかわらず、少し誤解され、魅力的に見えてしまうことがあるのです。
シングルトンは、クリーンアップに関しても問題を起こしやすいと言えます。
- シングルトンを明示的に殺すための機能はありません。 これは場合によっては深刻な問題となります。たとえば、すべてのオブジェクトをクリーンアップするまで安全にアンロードできない、というプラグインアーキテクチャのプラグインにとっては大きな問題でしょう。
- プログラム終了の際には、シングルトンも自動的にクリーンアップされることになります。 ただし、複数のシングルトンがある時、どのような順序でクリーンアップされるかは決まっていません。相互に依存し合うシングルトンがアプリケーションに含まれている時、これは問題になります。アプリケーションのシャットダウン時に、あるシングルトンが別のシングルトンにアクセスしようとするけれど、そのシングルトンはすでにクリーンアップされてしまっている、という事態になる恐れがあるからです。
シングルトンの欠点の中には、ある種のメカニズムを付加することで克服できるものもあります。しかし、それによってコードはどうしても複雑になってしまいます。シングルトンパターンさえ選択しなければ、そういう心配はいらないのです。
シングルトンパターンは、必要なインスタンスが絶対に 1 つだけと確信できるクラス以外では使うべきではないでしょう。そして、シングルトンをグローバルアクセスポイントとすることも避けるべきです。グローバルアクセスポイントになってどこからアクセスされるかわからないというのは良くないのです。シングルトンへの直接のアクセスは、あらかじめ定めておいたごく少数の箇所からのみ行うようにします。そして他のコードからは、インタフェースを通じてアクセスするのです。他のコードは、そのインタフェースを実装するのがシングルトンなのか、あるいは他の種類のクラスなのか、ということには関知しません。つまりシングルトンに依存しないのです。それにより、ユニットテストの妨げとなる依存関係も生じずに済み、保守性も向上します。今後、シングルトンの実装やシングルトンへのアクセスを検討する時があれば、いったん立ち止まって、ここに書いたようなことをじっくり考えてみてください。