- 初めに
突然ですが、シングルトンパターンについて皆さんはどう思いますか?
私の周りで最近シングルトンパターンについての議論があったのでこの機会に私も深く検討してみました。
備忘録としてまとめた方が良いかと思うのでこのブログを公開させていただきます。
有用であれば是非「いいね!とシェア!」していただければと幸いです。
- シングルトンパターン
GoFによるとシングルトンパターンは下記のように説明されています。Ensure a class only has one instance, and provide a global point of access to it
意味がわかりづらいかもしれないですが、ポイントとしては二つあります。
・システム全体にクラスのインスタンスが一つだけある
・このインスタンスのオブジェクトはグローバルアクセス可能なインターフェイスがある確かにシングルトンパターンが簡単だと思う人が多いです。しかし実際は簡単なのでしょうか?以下の疑問を聞かせてください。
・スタティックと比べたら、何の違いがあるか
・正しく実装できるか上記の疑問について、見た感じは簡単そうですが簡単かどうか深く検討しましょう。
- シングルトンとスタティックの違い
まず、それぞれの方法で実装してみましょうか// Using Singleton public class AppConfig { public int appId; private static AppConfig self; public static AppConfig getAppConfigInstance() { if(self == null) { self = new AppConfig(); } return self; } } config = AppConfig.getAppConfigInstance(); appId = config.appId; // Using static public class AppConfig { public static int appId; } appId = AppConfig.appId;
上記の実装を見ると、シングルトンは一箇所だけでアクセスであることがわかるが、スタティックもそうだろうね。シングルトンと比べたら、スタティックの方は実装は簡単だと思いますが、基本的な違いは以下となります。
Life-time
・シングルトンの場合はオブジェクトの初期化〜プログラムの終了
・スタティックの場合はクラスがロードされる時〜プログラムの終了
Abstraction
・シングルトンの場合は可能
・スタティックの場合は不可
つまり、基本的にシングルトンを使った方が多いかと思います。なぜかというのは例えば上記の例により、AppConfigだけではなく、WindowsAppConfigが必要なのであれば、スタティックを使ったら、実装が大変だと思って、シングルトンを使ったら、改修が少なくなります。ではシングルトンを良く理解して使いましょう。 - シングルトン実装
シングルトンとスタティックの違いはあまり簡単ではないですね。何となく分かると思いますが、実装についてどうすれば良いかと疑問となります。元々の定義をもう一度見てみましょう。Ensure a class only has one instance, and provide a global point of access to it
正しい実装方は二つポイントを守らないといけません。
・システム全体にクラスのインスタンスが一つだけある(1)
・このインスタンスのオブジェクトはグローバルアクセス可能なインターフェイスがある(2)
確かに簡単だと思う人が多いですよね。以下のルールに守れば良いだろうね。
(1)「constructor」は「private」で作って、自由にオブジェクトを作ることは不可能となる
(2)ひとつのスタティックメソッドだけで戻り値としてオブジェクトを返すpublic class AppConfig { public int appId; private static AppConfig self; private AppConfig() { appId = new Random().nextInt() % 100; } public static AppConfig getAppConfigInstance() { if (self == null) { self = new AppConfig(); } return self; } } public class Main { public static void main(String[] args) { AppConfig androidConfig = AppConfig.getAppConfigInstance(); AppConfig iOSAppConfig = AppConfig.getAppConfigInstance(); // The same values was printed System.out.println(androidConfig.appId); System.out.println(iOSAppConfig.appId); } }
正しいかな?(笑)。これは正しくないですよー。以下のソースコードを見ましょう
public static void main(String[] args) { AppConfig config = AppConfig.getInstance(); System.out.println(config.appId); AppConfig appConfig = null; try { Class<AppConfig> clazz = AppConfig.class; Constructor<AppConfig> constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true); appConfig = constructor.newInstance(); } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException | InstantiationException e) { e.printStackTrace(); } // The different value was printed System.out.println(appConfig.appId); }
ああ、通常に違う値が表示されますよね。やっぱり正しくないですね。なお、上記の実装方には、二つオブジェクトが作られるので、「constructor」にて改修だけで良いかなと思います。
import java.util.Random; public class AppConfig { public int appId; private static AppConfig self; private AppConfig() { if (self != null) { throw new UnsupportedOperationException("Use getAppConfigInstance() instead."); } appId = new Random().nextInt() % 100; } public static AppConfig getAppConfigInstance() { if (self == null) { self = new AppConfig(); } return self; } }
正しいかな?(笑)。これも正しくありません。マルチスレッドで実装して見ましょうか?
public class Main { public static void main(String[] args) { Thread threadOne = new Thread(new Runnable() { @Override public void run() { AppConfig config = AppConfig.getAppConfigInstance(); System.out.println(config.appId); } }); Thread threadTwo = new Thread(new Runnable() { @Override public void run() { AppConfig config = AppConfig.getAppConfigInstance(); System.out.println(config.appId); } }); threadOne.start(); threadTwo.start(); } }
数回くらい実施すると、違う値が表示されますね。まだ正しくないですよね。どこが間違いかな思ってて、「threadOne」「threadTwo」にて同時に「if(self == null)」実施すると、もちろん二つオブジェクトが作られますよ。なので、オブジェクトの作る際に同期することが必要となります。
import java.util.Random; public class AppConfig { private static volatile AppConfig self; public int appId; private AppConfig() { if (self != null) { throw new UnsupportedOperationException("Use getAppConfigInstance() instead."); } appId = new Random().nextInt() % 100; } public static synchronized AppConfig getAppConfigInstance() { if (self == null) { self = new AppConfig(); } return self; } }
ここまで見ると、シングルトンはやっぱり難しいかもしれません。では、正しい実装方を紹介させていただきます。
import java.util.Random; public class AppConfig { private static volatile AppConfig self; public int appId; private AppConfig() { if (self != null) { throw new UnsupportedOperationException("Use getAppConfigInstance() instead."); } appId = new Random().nextInt() % 100; } public static synchronized AppConfig getAppConfigInstance() { if (self == null) { self = new AppConfig(); } return self; } }
- まとめ
シングルトンが有用なパターンだと思うが、以下のデメリットがあるので、使う時に確認した方が良いかと思います。
・テストコードが大変
・結合度が強い
・正しい実装が簡単ではない
・マルチスレッドに対して、コントロールが難しい
特に、現代のシステムには並行性が大事なのに、シングルトンを使うとバグを発生しやすくて、スレッドのコントロールが超大変になってしまうので、気をつけて使いましょう。 - 参考
How to make the perfect Singleton?
Singleton Design Pattern
Design Patterns: Elements of Reusable Object-Oriented Software
2018.6.6
技術開発