グローバル変数やSingletonに対して否定的な声は多くありますが、それでも正当な構造であることは間違いありません。それでは、Singletonが問題視されるのはどういう場合でしょうか。問題はテストを行うときです。
クライアントをテストするまでは、Singletonにどのような問題があるかはわからないでしょう。仮に、カタログと対話するライブラリAPIを作成するものとしましょう。この時点では、このような対話はおそらく問題にならないでしょう。なぜなら、Catalogクラスは単純なハッシュマップだからです。しかし、Catalogクラスが実際のデータベースと対話するとしたらどうなるでしょうか(リスト5を参照)。
import java.sql.*; import java.util.*; public class Catalog { private static final Catalog soleInstance = new Catalog(); public static Catalog soleInstance() { return soleInstance; } private Catalog() { // enforce singularity } public void add(Book book) { try { Connection connection = ConnectionPool.get(); Statement statement = connection.createStatement(); statement.execute("insert into book // ... // ... } } // }
テストでデータベースをどのように扱うかはいつも頭の痛い問題です。もちろん、CatalogクラスがOracleと正しくやり取りするかと、DDL全体がJavaコードと合っているかを検証する必要があります。さらに、アプリケーションの他のビジネスロジックも検証する必要があります。この検証を実際にデータベースとやり取りしながら行うこともできますが、そのようなテストは非常に時間がかかります。また、構築および保守という点でもさまざまな理由で手間がかかります。
システムの残りのクラス(Catalogのクライアント)をテストするために、ここで本当にやるべきことは、Catalogクラスの動作をテストダブル(test double:テスト用の代役の意)と呼ばれるものでエミュレートすることです。抽象レベルでは、Catalogクラスは特定の分類に関連付けられた本の格納と取得をサポートします。その永続的な動作を再現する必要があります。おそらくここで本当に必要なのは、前述のマップ実装だけでしょう。
しかし残念ながら、Singleton構造では、JDBCを使用するバージョンのCatalogしか使用できません。これがSingletonの第一の問題です。テストダブルを作成できないのです。
Singleton設計を見直して複数のインスタンスを作成しても害が出ないようにすることが難しい場合(あるいはそうしたくない場合)は、別のもっと簡単な解決策があります。それはSingletonを適用しないことです。クラスファイル内で、Catalogのコンストラクタに次のようなコメントを追加しましょう。
public Catalog() { // create only one, except for testing! }
この解決策に対しては、おそらく非常に厳しい反応が返ってくるでしょう。「それでも誰かがインスタンスを作成したらどうなるんだ?」――確かにその場合にはSingletonよりも大きな問題になります。しかし、もし開発者がクラスの適切な使い方を調べなかったり、忠告に配慮しなかったりするのが普通なのであれば、なぜJavadocを生成する必要があるのでしょうか。開発者がこのような問題を捕捉できないなら、コードレビューではいったい何をするのでしょうか。
実際、どんな規約を設けても、開発者はその気になれば簡単に迂回できてしまいます。Javaでは、リフレクションのトリックを使えばプライベートなフィールドに外部アクセスすることさえ可能なのです。場合によっては、単純に「○○をしないでください」と注意することが最善の解決策になることもあります。
CatalogクラスをSingletonのようにモデル化しつつ、フックを用いてテストダブルを無理やり吐き出させることも可能です。そのためには、Catalogクラスからプライベートなコンストラクタを削除します(リスト6を参照)。代わりにsetInstanceメソッドを用意して、実際の運用環境で使用するSingletonを、Catalogのテストダブルに置き換えられるようにします。コメントにも注目してください。また、再び運用環境のインスタンスを使用するようCatalogに命じるresetメソッドを用意することもできます。resetメソッドを呼び出しておけば、運用環境のCatalogインスタンスの使用を前提とする他のテストに悪い影響を与えずに済みます。
import java.util.*; public class Catalog { private static final Catalog DEFAULT = new Catalog(); private static Catalog soleInstance = DEFAULT; private Map<String,Book> books = new HashMap<String,Book>(); public static Catalog soleInstance() { return soleInstance; } // use for testing public static void setInstance(Catalog catalog) { soleInstance = catalog; } public static void reset() { soleInstance = DEFAULT; } // ... }
つまり、Singletonを放棄する必要はないということです。単体テストのときに問題が起きないよう配慮しさえすればよいのです。
