Java : シングルトン・パターンの乱用はやめよう
シングルトン(Singleton) パターンは、オブジェクト指向プログラミングにおけるデザインパターンの1つです。
デザインパターンの中では比較的シンプルで理解しやすく、便利なこともあるため、使っているかたもいるかもしれません。
ただし、乱用は禁物です。
どのクラスからでも便利にアクセスできる、という理由だけでシングルトン・パターンを使っているのであれば、それは改善したほうがよいでしょう。
本記事の対象読者:シングルトン・パターンを使ったことのあるかた
シングルトン・パターンとは
Wikipedia からの引用です。
上記のページでは、シングルトンの問題点も含めて解説しているので、一読することをおすすめします。
また、本サイトでも下記の記事にて解説しています。
よろしければそちらもご参照ください。
シングルトン・パターンは、デザインパターンの中でもシンプルで概念が理解しやすいです。
そのため、学習したばかりのころは、使ってみたくなるのはプログラマならあるあるです。
むしろ良いことです。
やってみないと良いところ悪いところが実感できないですからね。
Java での実装も比較的楽にできます。
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return Singleton.instance;
}
public void func() {
System.out.println("called!");
}
}
// called!
Singleton.getInstance().func();
- コンストラクタを private にして外部からの生成をできなくします。
- staticフィールドに、ただ1つとなるインスタンスを保持します。
これでシングルトン・パターンの要件である クラスのインスタンスが1つしか生成されない ことが保証されます。
デメリット
グローバル変数と同じ危険性を持つ
C や C++ を経験しているかたは、グローバル変数という言葉を聞いたことがあるかもしれません。
一般的に、グローバル変数はできるだけ使わない方がよいと言われています。
Wikipedia にもそのあたりの記述がありますね。
Java にはグローバル変数という用語は(おそらく)ありませんが、同様のことが可能です。
クラスのフィールドを public static で宣言した場合です。
public class Global {
public static int value;
}
この Global.value は、いつでもどのクラスからでも書き換え可能です。
グローバル変数と同じですね。
そして、シングルトン・パターンはグローバル変数の特徴を持ちます。
つまり、グローバル変数の問題点を抱えていることになります。
補足
プリミティブ型 (int や float など) や 不変オブジェクト(Stringなど) は、public static に final をつけると定数となるため問題ありません。
標準API でもよく使われています。
public class Constants {
public static final String VERSION = "1.0.2";
public static final int MAX_SIZE = 100;
}
可変オブジェクトは final をつけても定数とはなりません。
つまりグローバル変数の特徴を持ちます。ご注意ください。
public class Sample {
private int value;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
public class GlobalSample {
// sample変数はfinalのため書き換えできません。
// しかし、sampleオブジェクトは、sample.setValueで変更可能です。
public static final Sample sample = new Sample();
}
ユニットテストしづらい
シングルトンはアプリケーションでただ1つのインスタンスを持ちます。
これはアプリケーションとして実行しているときは良いのですが、ユニットテストするときに都合が悪くなることがあります。
通常、ユニットテストでは、1つのテストごとにすべての状態はクリアされるべきです。
テスト間に依存があってはいけません。
しかし、シングルトンは、そのままだと複数のテストにまたがってインスタンスが共有されてしまいます。
前のテストで変更したシングルトンの状態が、次のテストで引き継がれてしまうわけです。
public class Singleton {
private static final Singleton instance = new Singleton();
private int count;
private Singleton() {
}
public static Singleton getInstance() {
return Singleton.instance;
}
public void func() {
count++;
System.out.println("count : " + count);
}
}
public class SingletonTest {
@Test
public void test1() {
System.out.println("-- test1 --");
Singleton.getInstance().func();
}
@Test
public void test2() {
System.out.println("-- test2 --");
Singleton.getInstance().func();
}
// テスト結果
// ↓
//-- test1 --
//count : 1
//-- test2 --
//count : 2
}
問題を解決するために、下記のような小細工をすることになるかもしれません。
public class Singleton {
// リセットが可能なようにfinalを削除
private static Singleton instance = new Singleton();
private int count;
private Singleton() {
}
// Unit Testのためだけのメソッド
static void reset() {
instance = new Singleton();
}
public static Singleton getInstance() {
return Singleton.instance;
}
public void func() {
count++;
System.out.println("count : " + count);
}
}
public class SingletonTest {
@Test
public void test1() {
System.out.println("-- test1 --");
Singleton.reset();
Singleton.getInstance().func();
}
@Test
public void test2() {
System.out.println("-- test2 --");
Singleton.reset();
Singleton.getInstance().func();
}
// テスト結果
// ↓
//-- test1 --
//count : 1
//-- test2 --
//count : 1
}
1つ1つのテスト開始ごとに reset メソッドを呼ぶようにします。
これでテスト間の依存はなくなりますが、ちょっと無理やり感がありますね…
あとは、ユニットテストを書くと、テスト対象に関連したオブジェクトをスタブ・モック化したい場合があります。
シングルトンをスタブ・モック化したい場合、面倒になることが多いです。
まず継承できるようにコンストラクタを protected に変更する必要があります。
スタブ・モックに差し替えるためのメソッドも追加します。(もしくはリフレクションで強引に)
public class Singleton {
private static Singleton instance = new Singleton();
// 継承できるようにprotectedに変更
protected Singleton() {
}
// Unit Testのためだけのメソッド
static void setMock(Singleton mock) {
instance = mock;
}
public static Singleton getInstance() {
return Singleton.instance;
}
public void func() {
System.out.println("called!");
}
}
public class SingletonMock extends Singleton {
@Override
public void func() {
System.out.println("mock called!");
}
}
// Mockなし
Singleton.getInstance().func(); // called!
// Mockあり
Singleton.setMock(new SingletonMock());
Singleton.getInstance().func(); // mock called!
だんだんと、シングルトンである制約が緩くなってきました…
そんなこんなで、シングルトン・パターンは、ユニットテストとの相性が悪いです。
本当にシングルであることが必要か?
シングルトン・パターンを使うときは、 本当にインスタンスを1つだけに制限したいか? ということを考えてみましょう。
もしインスタンスが複数あっても別に困らない…ということであれば、シングルトン・パターンは必要ありません。
Wikipedia ではロケールやルック・アンド・フィールを例にしています。
他には、アプリケーションの基盤となりうるフレームワークなどにも使ってよいかもしれません。
とはいえ、それらにシングルトン・パターンは絶対に必要か?といわれると、別に必須ではないと思います。
利便性のためのシングルトン
シングルトンにするとプログラミングしやすくなることはもちろんあると思います。
(多数のクラスで共有して使いたいオブジェクトがあるけど、コンストラクタにいちいちパラメータとして渡すのが面倒くさい…などなど)
その場合は、デメリットを理解したうえで使うようにしましょう。
個人的にはあまりおすすめはしません。
まとめ
シングルトン・パターンにはデメリットがあります。
- グローバル変数と同じ危険性を抱える
- ユニットテストしづらくなる
本当にインスタンスを1つだけに制限したいか?ということをよく検討し、デメリットを理解したうえで使うようにしたいですね。
関連記事
- 標準APIにならう命名規則
- コメントが少なくて済むコードを目指そう
- メソッドのパラメータ(引数)は使う側でチェックしよう
- 不変オブジェクト(イミュータブル) とは
- 依存性の注入(DI)をもっと気軽に
- 不要になったコードはコメントアウトで残さずに削除しよう
- 簡易的な Builder パターン
- 読み取り専用(const) のインタフェースを作る
- 図解/デザインパターン一覧 (GoF)
- Abstract Factory パターン
- Adapter パターン
- Bridge パターン
- Builder パターン
- Chain of Responsibility パターン
- Command パターン
- Composite パターン
- Decorator パターン
- Facade パターン
- Factory Method パターン
- Flyweight パターン
- Interpreter パターン
- Iterator パターン
- Mediator パターン
- Memento パターン
- Observer パターン
- Prototype パターン
- Proxy パターン
- Singleton パターン
- State パターン
- Strategy パターン
- Template Method パターン
- Visitor パターン