グローバル変数の代用?
Singletonパターンは、デザインパターンの中でも最も分かりやすいので、初学者は濫用しがちだ。
あるクラスFooをSingletonにすると、アプリケーションの随所で、次のようにして使うことができる。
Foo* foo = Foo::GetInstance(); foo->bar();
ふつうのオブジェクトは、どこで生成して、どこで保持して、…ということを、きちんと考えないといけない。いろいろな箇所で使う場合は、関数やクラスの間で、オブジェクトを受け渡してあげないといけない。
ところが、Singletonは、そのようなことをまったく考えずに、アプリケーションのどこでも自由に使える。
これは、よく考えてみると、次のようなコードと同じである。
extern Foo foo; // グローバル変数 〜〜〜〜〜〜〜〜〜 foo.bar();
つまり、グローバル変数の代用として、Singletonパターンを使ってしまっているケースがある。
変数をグローバルに宣言してしまえば、どこからでも参照できるので、手軽である。そのため、プログラミングの初心者には、グローバル変数を多用したがる人もいる。
グローバル変数を多用するのが誤った考えであることは、大抵の人は理解している。にも関わらず、グローバル変数の代用として、Singletonパターンを使っているケースが、少なくない。
この背景には、「デザインパターンを使っていないものより、使っているものの方が、良い設計である」と誤解されていることもあるようだ。デザインパターンを使っているだけで、何か良いことをしているような気になってしまうこともありそうだ。
だが、誤ったSingletonパターンの濫用は、グローバル変数を多用した場合と、結局はまったく同じことをしている。むしろ、グローバル変数を多用するよりも、害が大きいとも言える。というのも、Singletonパターンを使うと、インスタンスが1つしか存在しない、という制約が、クラスの制約になってしまうためだ。単純にグローバル変数を使う場合は、クラスにはその制約はない。
実際、いろいろな箇所でSingletonパターンを使っているケースを目にする。だが、本当にSingletonパターンを使うべきケースというのは、相当に少ないはずだ。
Singletonパターンを使うべきでない例
N体問題のシミュレーションをしているとしよう。N個の天体の初期位置や初速度を適当に指定してシミュレーションを実行した結果を、1つのデータセットとして保存する。初期位置や初速度を変えつつ、何度もシミュレーションを行うと、大量のデータセットができる。
そこで、シミュレーション結果を表示するアプリケーションを作ろう。アプリケーションでは、データセットを1つロードすると、初期状態や終了状態を画面に表示できるものとする。天体の空間密度の時間変化や、各天体の軌跡のパターンなど、いろいろなデータを表示するだろうし、さまざまな解析も可能だろう。このアプリケーションは、たくさんの画面を持つことになる。
実際にこのアプリケーションを作ってみると、おそらく、非常にたくさんの画面が、相互に関係しあう、非常に複雑なシステムになると思われる。それらの画面は、ほぼすべて、ロードされたデータセットにアクセスするだろう。しかも、このアプリケーションでは、1つしかデータセットをロードできない、と決まっている。
このようなケースで、データセットを表すクラスを、Singletonとしてデザインしてしまうことがある。しかし、これは誤りだ。
データセットのクラスは、おそらく、次のようなデータの集合となるだろう。
- シミュレーション実行日時。
- N個の天体の初期位置と初速度。
- シミュレーション終了時のN個の天体の状態。
- シミュレーション途中のさまざまな計算結果。
つまり、ただのデータである。
このデータセットが、常に1つしかロードされていない(メモリ上に1つしか存在しない)というのは、アプリケーションレベルの仕様である。ただのデータの集まりである、データセットのクラスだけを見れば、これが必ず1つしか存在してはいけない、という理由は存在しない。
データセットのクラスをSingletonにしてしまう、ということは、アプリケーションレベルの仕様を、誤って下位レベルのクラスに組み込んでしまう、ということになる。この場合は、データセットの実体を、グローバル変数として1つだけ持たせる方が、はるかに正しい設計となる。
Singletonパターンを使う目的
Singletonパターンは、実装者が誤ってインスタンスを生成しようとしてもできない、という点が、利点として挙げられることが多いが、必ずしも正しくないように思う。
たとえ、あるクラスをSingletonにしたとしても、実装者が次のような誤りを犯せば、アプリケーションは正しく動作しなくなる。
- Singletonクラスを使わないで、独自に処理してしまう。
- 後述する、PIXYシステム2のXmlDBHolderCacheクラスの例で言えば、次のようなケースである。
- XmlDBHolderCacheクラスを使わずに、ダイレクトにデータベース内のXMLファイルにアクセスしてしまう。
- 後述する、PIXYシステム2のXmlDBHolderCacheクラスの例で言えば、次のようなケースである。
- Singletonクラスとまったく同じ機能を持つクラスをもう1つ作って、そちらを使ってしまう。
Singletonパターンを使う目的は、実装者の誤りを防ぐことではない。そのクラスを見た人が分かるように、インスタンスを1つしか生成しないことを前提としてクラスが実装されていることを「表現」することにある。
つまり、Singletonパターンを使う目的は、そのクラスだけに閉じている。この点からも、アプリケーションレベルで、インスタンスが1つしかない、という仕様がある場合に、それをSingletonパターンとして設計するのは、誤りと言える。
コピーコンストラクタに関する注意
Singletonパターンでは、勝手にいくつものインスタンスを作れないように、コンストラクタをprivateにする。
C++の場合は、引数を持たないデフォルトコンストラクタの他に、コピーコンストラクタも暗黙のうちに作られているので、注意が必要である。
C++でSingletonパターンを使う際は、コピーコンストラクタも明示的に宣言し、privateにしなければならない。
下記のFooクラスは、一見すると、Singletonパターンになっているように見える。
class Foo { public: int value; private: static Foo* foo; Foo ( ) { value = 0; } virtual ~Foo ( ) { } public: static Foo* getInstance ( ) { if (foo == NULL) foo = new Foo(); return foo; } }; Foo* Foo::foo = NULL;
だが、コピーコンストラクタがprivateにされていないため、次のようにすると、インスタンスをいくつも作ることができてしまう。
Foo* foo1 = Foo::getInstance(); Foo* foo2 = new Foo(*foo1); foo1->value = 1; foo2->value = 2; printf("foo1 = %d\n", foo1->value); printf("foo2 = %d\n", foo2->value);
実行すると、次のように出力され、インスタンスが2つできたことが分かる。
foo1 = 1 foo2 = 2
Singletonオブジェクトの削除
Singletonパターンを使う目的には、下記の2つが考えられる。
- オブジェクトが、たかだか1個しか存在しない、という制約を表す。
- いったん作られた後は、1つのオブジェクトが存在し続ける、という制約を表す。
Singletonパターンでは、インスタンスを取得するメソッドとともに、インスタンスを削除するメソッドも用意することが多い。
class Parent { private: static Parent* parent; Parent ( ) { } Parent ( const Parent& p ) { } virtual ~Parent ( ) { } public: static Parent* getInstance ( ) { if (parent == NULL) parent = new Parent(); return parent; } static void deleteInstance ( ) { if (parent != NULL) delete parent; parent = NULL; } }; Parent* Parent::parent = NULL;
だがこれは、Singletonパターンを使う目的が、前者の「オブジェクトが、たかだか1個しか存在しない」という制約を表す場合の話だ。後者の「いったん作られた後は、1つのオブジェクトが存在し続ける」という制約を表す場合は、オブジェクトを削除するdeleteInstance()というメソッドは、用意するべきではない。
ここで、C++の場合は、1つの問題が生じる。
getInstance()しか用意しなかった場合は、生成したSingletonオブジェクトが破棄されることがなくなってしまう。例えばVisual C++を使って開発していると、アプリケーションを終了する際に、必ず「メモリリークが発生した」という警告が表示されてしまう。
だがこれは、1つのオブジェクトが存在し続ける、という制約から考えれば、当然の帰結と思われる。
仮に、アプリケーションの終了時に何らかの処理を呼び出す仕組みがあったとしても、そこでSingletonオブジェクトを破棄しても良いものだろうか? 終了処理とはいえ、実際にはまだアプリケーションは終了している訳ではない。Singletonオブジェクトの破棄をした後で、別のクラスの終了処理から、再度getInstance()が呼ばれてしまう可能性もある。
絶対に、1つのオブジェクトだけが最後まで永続する、という制約を満たすためには、OSがアプリケーションのメモリ領域を解放するまで、つまり、Visual C++の画面にメモリリークと表示されるまで、Singletonオブジェクトは存在しているべきだと思う。
なお、この場合、Singletonクラスのデストラクタに処理を書いても、決して呼び出されないことになるので、注意が必要である。
Singletonクラスを拡張する際の問題
継承
Singletonパターンでは、コンストラクタはprivateにしなければならない。
下記は、コンストラクタをprotectedにした例である。このように、コンストラクタをprotectedにすると、サブクラスの中で、いくらでもインスタンスを作ることができてしまう。
class Parent { private: static Parent* parent; protected: Parent ( ) { } private: Parent ( const Parent& p ) { } public: static Parent* getInstance ( ) { if (parent == NULL) parent = new Parent(); return parent; } }; class Child : public Parent { void foo ( ) { // いくらでも作れる。 Parent* p1 = new Parent(); Parent* p2 = new Parent(); Parent* p3 = new Parent(); } }; Parent* Parent::parent = NULL;
ところが、コンストラクタをprivateにすると、継承して拡張することはできなくなる。サブクラスから、自動的にスーパークラスのコンストラクタを呼ぼうとするが、privateであるために呼べなくなるからである。
委譲
SingletonであるParentクラスを拡張して、Childクラスを作るとする。この時、アプリケーション側では、次のように記述したい。
Child* child = Child::getInstance();
しかし、Childクラスを次のように書くと、これはもちろん動作しない。
class Child { public: static Child* getInstance ( ) { return (Child*)Parent::getInstance(); } };
結局、下記のように、ParentクラスにSingletonの仕組みがすでに実装されているのにも関わらず、まったく同じ仕組みを、Childクラスでも再び実装しなくてはならない。
class Child { private: static Child* child; private: Child ( ) { } Child ( const Child& c ) { } public: static Child* getInstance ( ) { if (child == NULL) child = new Child(); return child; } }; Child* Child::child = NULL;
PIXYシステム2での、Singletonパターンの使用例
XmlDBHolderCache
データベース内のXMLファイルを読み書きする際の、ディスクキャッシュである。
なお、このクラスは、すべてのフィールドとメソッドをstaticにする、という形でデザインしているので、厳密にはSingletonパターンには合致しないかもしれないが、意味的には同じ。Singletonパターンで書き直すことも簡単である。
背景
PIXYシステム2は、下記の3つのデータベースを持つ。
- 画像情報データベース
-
画像のデータベース。
- 同定用カタログデータベース
-
天体データのデータベース。
- 光度データベース
-
画像から測定した、天体データの光度のデータベース。
これらのデータベースは、ファイルシステム上に、XMLファイルの形で記録される。
それぞれのデータベースにおける、1つのレコードは、下記のようなデータである。
- 画像情報データベース
-
1枚の画像の情報(ファイル名、撮影日時、撮影位置、など)。
- 同定用カタログデータベース
-
1つの星のデータ(名前、赤経赤緯、光度、など)。
- 光度データベース
-
1つの光度観測データ(天体名、日時、測定光度、画像ファイル名、など)。
これらの1レコードは、1つのXML要素として表されている。例えば、1枚の画像の情報について言えば、このようなXML要素になっている。
<information> <image>ファイル名</image> <date>撮影日時</date> <center> 撮影位置 </center> : </information>
詳しくは、下記のページを参照。データベースに記録するXMLのDTD定義およびDTDツリーが参照できる。
ここから先は、撮影日時ごとに分類されている画像情報データベースを例として説明する。
同一の日に撮影された画像の情報は、1つのXMLファイルにまとめて記録される。例えば、一晩に100枚の画像を撮影すれば、XMLファイルには、下記のように、100個の<information>タグが並列に並ぶ。
<information> <image>画像1</image> : </information> <information> <image>画像2</image> : </information> : <information> <image>画像100</image> : </information>
一晩に100枚の画像を撮影すると、100枚の画像をPIXYシステム2で順次検査していき、検査が終わったものから、順にデータベースに登録していく。この時、すべて撮影日が同じなので、同一のファイルについて、100回のOpen/Closeを繰り返すことになる。同時に、XML形式とメモリ上のデータ形式との変換も、100回行われてしまうため、無駄な時間がかかってしまう。
そこで、ディスクキャッシュ機能を追加した。
解説
XmlDBHolderCacheクラスのインターフェースについては、下記を参照。
enable関数でtrueを指定すると、その時点からディスクキャッシュが有効となる。falseを指定すると、その時点でディスクキャッシュが無効となる。falseを指定した時は、メモリ上に保持していたデータはすべてディスク上に書き出される。
read関数とwrite関数を使って、データベース内のファイルにアクセスする。ディスクキャッシュが無効であれば、ダイレクトにファイルにアクセスするだけである。
ディスクキャッシュが有効な時にread関数を呼ぶと、もしそのファイルがメモリ上にすでにロードされていれば、そのデータを返す。そうでなければ、ファイルを読み込む。その際、最も最近に使われていない(least recently used)ファイルのデータが書き出され、メモリ上から削除される。
ディスクキャッシュが有効な時にwrite関数を呼ぶと、メモリ上のデータだけを更新し、ファイルアクセスは行わない。但し、メモリ上にデータが無ければ、ダイレクトにファイルに書き込む。