はじめに
- 韓国人として、日本語とコンピュータの勉強を同時に行うために、ここに文章を書いています
- 翻訳ツールの助けを借りて書いた文章なので、誤りがあるかもしれません
シングルトンパターン
シングルトンパターンは、クラスのインスタンスがただ1つだけ生成されることを保証し、そのインスタンスへのグローバルなアクセス手段を提供するデザインパターンです
シングルトンパターンの主な特徴
- 唯一のインスタンスが存在することを保証
- そのインスタンスに簡単にアクセスできる仕組みを提供
- クラスのインスタンス化を制御(例: クラスのコンストラクタを非公開にする)
唯一のインスタンスが存在することを保証
クラスのインスタンスがただ1つだけ存在 することを保証するのが、シングルトンパターンの最初の特徴です。
このような設計は、システム内でシステム全体の状態を維持する必要がある場合 に効果的です。
たとえば、設定情報(Configuration)、データベース接続(DB Connection)、ロギングシステムのようなリソースを管理する際に、複数のインスタンスが存在すると次のような問題が発生する可能性があります。
- 状態の不一致: 例えば、設定情報を管理するインスタンスが複数ある場合、1つのインスタンスで設定を変更しても他のインスタンスには反映されず、一貫性が失われることがあります
- リソースの浪費: データベース接続の場合、それぞれのインスタンスが個別のコネクションプールを生成すると、不要にシステムリソースを占有することになります
このように、シングルトンパターンはシステムの一貫性を維持し、リソースを効率的に使用するために、ただ1つのインスタンスが存在することを保証します。
現実世界の例を挙げると、スマートフォンの背景テーマがダークモードに設定されている場合、どのアプリを開いても一貫してダークモードが維持されるべきです。
もしAアプリはライトモード、Bアプリはダークモード、あるいは同じアプリ内でも部分的にライトモードとダークモードが混在している状態だと、非常に不便です。このように、システム内で一貫した状態を維持する必要がある場合にシングルトンパターンが求められると言えます。
そのインスタンスに簡単にアクセスできるようにする
シングルトンパターンの2つ目の特徴は、唯一のインスタンスをどこからでも簡単に利用できるようにする ことです。
最初の特徴で述べたように、システムのシステム全体の状態 を保持する場合、
たとえば、ロギングシステム、データベース接続、設定情報のようなリソースは、アプリケーションの複数の部分から同じ方法でアクセスする必要があります。
先ほどのスマートフォンのダークモードを例に挙げると、AアプリもBアプリも、システムの画面モード設定を参照できる必要があります。
ユーザーがアプリごとにモードを設定する手間を省き、一貫性を保ちながらシステムの設定値を参照できるべきです。
このように、シングルトンパターンはグローバルなアクセシビリティを提供することで、システムの使いやすさを向上させ、管理の一貫性を維持します。
クラスのインスタンス化を制御(例: クラスのコンストラクタを非公開にする)
シングルトンパターンの3つ目の特徴は、クラスのインスタンス生成を制御することです。
そのために、コンストラクタを非公開にして、外部から直接オブジェクトを生成できないように制限します。
もし外部からコンストラクタを呼び出してオブジェクトを追加生成できてしまうと、シングルトンパターンの意図が完全に崩れてしまいます。
再びスマートフォンのダークモードを例にすると、当然ですが、ダークモードを設定するためのシステム設定は1つでなければなりません。
設定アプリが複数存在し、どれを使用すべきか分からなくなったら困ります。
このようにクラスのコンストラクタを非公開にしてインスタンス生成を制御することは、シングルトンパターンの安定性と一貫性を維持するための重要な要素です。
シングルトンではないコード例
public class Setting {
private boolean darkMode;
public Setting() {
this.darkMode = false;
}
public boolean isDarkMode() {
return darkMode;
}
public void setDarkMode(boolean darkMode) {
this.darkMode = darkMode;
}
}
次のように、Setting
クラス内部にダークモードの設定を担当するコードがあると仮定します。
上記のように Setting
を何度も生成すると、ダークモードを有効にしても
このような実行結果になります。
new
キーワードでオブジェクトを生成するたびに、完全に独立した新しい Setting
が作られるため、
システム全体で1つの設定を使用するという意図に合致しません。
そこで、このコードをシングルトンパターンに変更してみます。
シングルトンパターンの実装方法
シングルトンパターンの実装方法はシンプルです。
- クラスのすべてのコンストラクタを
private
にして、他のオブジェクトからインスタンス化できないようにする-
new
キーワードを使ったインスタンス生成を防ぎます
-
- インスタンスを返す静的メソッドを提供する
-
static
を利用して、唯一のインスタンスを返すメソッドを提供します
-
- インスタンスを保持する
private static
変数を持つ- 唯一のインスタンスを参照するための
static
フィールドをprivate
に宣言します
- 唯一のインスタンスを参照するための
これらの3つの条件を確認しながら、シングルトンパターンを実装してみます。
即時初期化(Eager Initialization)
インスタンスをクラスのロード時に生成する方法です。
public class EagerSetting {
private static final EagerSetting INSTANCE = new EagerSetting();
private boolean darkMode;
private EagerSetting() {
this.darkMode = false;
}
public static EagerSetting getInstance() {
return INSTANCE;
}
public boolean isDarkMode() {
return darkMode;
}
public void setDarkMode(boolean darkMode) {
this.darkMode = darkMode;
}
}
私は次のように書いてみました。
private static final EagerSetting INSTANCE = new EagerSetting();
クラスが最初にロードされるとき、次のようにオブジェクトをロードし、
その参照を返すことができます。
public static EagerSetting getInstance() {
return INSTANCE;
}
getInstance()
を定義し、インスタンスを返すメソッドを作成しました。
結果
以前のテストのように new
キーワードでインスタンスを使用するテストは、コンパイルエラーが発生します。
コンストラクタを private
に定義することで、外部からインスタンスを生成することをしっかり防げました。
そのため、インスタンスを取得するための getInstance()
を使ってテストを進めてみます
> Task :testClasses
true
true
true
テスト結果は次のようになり、インスタンスが1つだけ生成されたことを確認できます。
JVMがクラスをロードするとき、Method Area
に instance
変数が配置され、
new EagerSetting();
によって生成されたオブジェクトが Heap 領域に作成され、
instance
がそのオブジェクトを参照するようになります。
即時初期化戦略の欠点
- クラスがロードされる際に必ずインスタンスが生成されるため、使用されない場合にメモリの浪費が発生します
private EagerSetting() {
this.darkMode = false;
try{
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
もし次のように5秒の待機を設定した場合、
テストコードの実行速度が、ロードにかかる5秒分だけ増加します。
このように、使用しない場合でも必ずインスタンスが生成されるため、不要なメモリが浪費されます。
さらに、処理負荷の高い初期化がある場合、プログラムの開始時間がその分遅延する可能性があります。
遅延初期化(Lazy Initialization)
- インスタンスを使用するタイミングで初期化する方法です
public class LazySetting {
private static LazySetting instance;
private boolean darkMode;
private LazySetting() {
this.darkMode = false;
}
public static LazySetting getInstance(){
if (instance == null){
instance = new LazySetting();
}
return instance;
}
public boolean isDarkMode(){
return darkMode;
}
public void setDarkMode(boolean darkMode){
this.darkMode = darkMode;
}
}
次のような点が変更されました。
private static LazySetting instance;
クラスをロードしたタイミングで Heap メモリにインスタンスを生成するのではなく、
public static LazySetting getInstance(){
if (instance == null){
instance = new LazySetting();
}
return instance;
}
もしすでに生成されている場合はその instance
を返し、
生成されていない場合は新たにインスタンスを生成するようにします。
この方法であれば、使用するタイミングで初期化を行うため、即時初期化の欠点を補うことができます。
結果
> Task :testClasses
true
true
true
当然ながら、結果は以前と同様にシングルトンがしっかりと保証されています。
デバッグを確認すると、最初の実行時に new
キーワードを使ってインスタンスが生成され、
2回目以降は既に生成されたインスタンスが返されることで、シングルトンが保証されていることを再確認できました。
遅延初期化戦略の欠点
private EagerSetting() {
this.darkMode = false;
try{
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- インスタンスが使用される直前に初期化が行われるため、処理負荷の高い初期化がある場合、クライアントのリクエスト時に初期化処理によってリクエストが遅延したり、タイムアウトする可能性があります
一方で、早期初期化はクラスのロード時に初期化があらかじめ行われるため、このような問題を防ぐことができます。
- 遅延初期化方式には同時実行性の問題があります。
例えば、次のように100個のスレッドを同時に作成し、getInstance()
を呼び出す場合、複数のインスタンスが生成される可能性があります。
> Task :testClasses
count instance 3
出力結果を見ると、インスタンスが3つ生成されてしまったことを確認できます。
public static LazySetting getInstance(){
if (instance == null){
instance = new LazySetting();
}
return instance;
}
これは、thread1
が if (instance == null)
をチェックしている間に、別のスレッドである thread2
も同じ if (instance == null)
を通過してしまう可能性があるためです。
要するに、この if
文に同時にアクセスするスレッドが多ければ多いほど、多くのインスタンスが生成されることになります。
この問題により、遅延初期化方式ではシングルトンが破られる可能性があります。
遅延初期化方式の並行処理性能の解決
-
synchronized
を使用する
public static synchronized LazySetting getInstance(){
if (instance == null){
instance = new LazySetting();
}
return instance;
}
このように、該当メソッドに synchronized
を付与し、1つのスレッドだけがアクセスできるようにする方法があります。
しかし、使用頻度が高い場合、ロックが頻繁に発生し、低い並行処理性能のために処理のボトルネックが生じる可能性があります。
- 2つ目の方法は、前述のように早期初期化方式を活用することです。
public class EagerSetting {
private static final EagerSetting INSTANCE = new EagerSetting();
private boolean darkMode;
private EagerSetting() {
this.darkMode = false;
}
public static EagerSetting getInstance() {
return INSTANCE;
}
public boolean isDarkMode() {
return darkMode;
}
public void setDarkMode(boolean darkMode) {
this.darkMode = darkMode;
}
}
次のようにクラスのロード時に初期化を行えば、同時実行性の問題は完全に解消されます。
二重チェックロッキング(Double-Checked Locking)
getInstance()
メソッドで synchronized
による低い同時実行性の問題が発生しました。
これを解決するために、2回 チェックを行うことで、遅延初期化と高い同時実行性を両立する方法があります。
この方法を二重チェックロッキング(Double-Checked Locking)と呼びます。
public class DoubleCheckSetting {
private static volatile DoubleCheckSetting instance;
private boolean darkMode;
private DoubleCheckSetting() {
this.darkMode = false;
}
public static DoubleCheckSetting getInstance(){
if (instance == null){
synchronized (DoubleCheckSetting.class){
if (instance == null){
instance = new DoubleCheckSetting();
}
}
}
return instance;
}
public boolean isDarkMode(){
return darkMode;
}
public void setDarkMode(boolean darkMode){
this.darkMode = darkMode;
}
}
if (instance == null){
次のように、instance
が初期化されているかを確認します。
もし遅延初期化のケースと同様に複数のスレッドが if
文内に入った場合、
synchronized (DoubleCheckSetting.class){
if (instance == null){
instance = new DoubleCheckSetting();
}
}
クラスに synchronized
を付与して同時実行性を制御します。
この方法では、ロックの発生頻度が減少し、遅延初期化の getInstance()
全体に synchronized
を付けるより効率的です。
volatile を使用する理由
private static volatile DoubleCheckSetting instance;
このように volatile
を使用して、命令の並び替えの問題を解決します。
instance = new DoubleCheckSetting();
ここで次のような操作が発生します。
- メモリを割り当てる
-
DoubleCheckSetting
のコンストラクタを呼び出す -
instance
変数に値を割り当てる
しかし、コンパイラまたはCPUの最適化によって命令の並び替えが発生する可能性があります。
- メモリを割り当てる
-
instance
変数に値を割り当てる -
DoubleCheckSetting
のコンストラクタを呼び出す
このように命令の並び替えが発生した場合、特定のスレッドが未初期化のオブジェクトを使用してしまう可能性があります。
例えば、ダークモードがデフォルトオプションの場合、
private DoubleCheckSetting() {
this.darkMode = true;
}
次のように true
に初期化されるべきところが、JVM の初期化規則に従い、boolean の初期値は false
であるため、
public boolean isDarkMode() {
return darkMode;
}
ダークモードを確認した際に false
が返される可能性があります(未初期化の状態)。
このような問題を解決するために、volatile
キーワードを使用して 命令の並び替え を制御します。
結果
以前、同時実行性の問題が発生していたコードを再実行してみると、
> Task :testClasses
count instance 1
次のようにシングルトンが保証されていることを確認できます。
ダブルチェック戦略の欠点
-
volatile
キーワードは Java 1.5 で導入されました- もし Java 1.5 未満のバージョンを使用している場合、この方法は利用できません
- コードが複雑になるという欠点があります
ホルダーによる初期化 (Initialization on Demand Holder)
ダブルチェックよりも簡単な方法として、静的な内部クラスを使用する方法があります。
public class HolderSetting {
private boolean darkMode;
private HolderSetting() {
darkMode = false;
}
private static class Holder {
private static final HolderSetting INSTANCE = new HolderSetting();
}
public static HolderSetting getInstance() {
return Holder.INSTANCE;
}
public void setDarkMode(boolean darkMode) {
this.darkMode = darkMode;
}
public boolean isDarkMode() {
return darkMode;
}
}
この方法では、Java 1.5 未満のバージョンでも使用可能であり、遅延ローディングが可能で、コードの複雑さという欠点も解消され、Thread-safe が保証されます。
さらに、synchronized
ブロックがないため、パフォーマンスのオーバーヘッドもありません。
上記すべての方法の欠点(シングルトンを壊す方法)
これまでの4つの方法でシングルトンを実装しましたが、完全にシングルトンを保証するわけではありません。
シングルトンを壊す方法について調査しました。
シリアライゼーション/デシリアライゼーション
シリアライゼーションとデデシリアライゼーションの過程を通じて、シングルトンを破ることが可能です。
import java.io.Serializable;
public class HolderSetting implements Serializable {
private boolean darkMode;
private HolderSetting() {
darkMode = false;
}
private static class Holder {
private static final HolderSetting INSTANCE = new HolderSetting();
}
public static HolderSetting getInstance() {
return Holder.INSTANCE;
}
public void setDarkMode(boolean darkMode) {
this.darkMode = darkMode;
}
public boolean isDarkMode() {
return darkMode;
}
}
次のように、シリアライゼーションを使用するために Serializable
を追加し、
ByteArrayOutputStream byteArrayOutput = new ByteArrayOutputStream();
ObjectOutputStream objectOutput = new ObjectOutputStream(byteArrayOutput);
objectOutput.writeObject(originalInstance);
シリアライゼーションによってオブジェクトをバイト列に変換します。
ByteArrayInputStream byteArrayInput = new ByteArrayInputStream(byteArrayOutput.toByteArray());
ObjectInputStream objectInput = new ObjectInputStream(byteArrayInput);
deserializedInstance = (HolderSetting) objectInput.readObject();
その後、次のようにバイト列をオブジェクトに変換すると、この過程でコンストラクタを呼び出さずに新しいオブジェクトが生成されます。
結果
> Task :testClasses
false
結果的に、シリアライゼーション -> デシリアライゼーションの過程を通じてシングルトンの規則が破られてしまいます。
解決方法
public class HolderSetting implements Serializable {
private boolean darkMode;
private HolderSetting() {
darkMode = false;
}
private static class Holder {
private static final HolderSetting INSTANCE = new HolderSetting();
}
public static HolderSetting getInstance() {
return Holder.INSTANCE;
}
public void setDarkMode(boolean darkMode) {
this.darkMode = darkMode;
}
public boolean isDarkMode() {
return darkMode;
}
@Serial
private Object readResolve() {
return getInstance();
}
}
@Serial
private Object readResolve() {
return getInstance();
}
次のように readResolve()
を定義することで、
readObject()
がオブジェクトを生成した後に readResolve()
が呼び出されます。
@Serial
シリアライズシリアライゼーション関連のメソッドやフィールドを誤って宣言した場合に、コンパイル時に警告を提供します。
Java 14 で導入されました
> Task :testClasses
true
readResolve()
が呼び出され、同じオブジェクトが返されることを確認できます。
複数のクラスローダ (Multiple class loaders)
クラスローダを使用すると、シングルトンパターンを破ることができます。
異なるクラスローダが同じクラスをロードする場合、それぞれが独立したインスタンスを生成するためです。
次のようにクラスローダを使用してインスタンスを比較すると、
結果
> Task :testClasses
false
false
結果として、シングルトンが破られたという結果が出ました。
シングルトンが破られる理由
URLClassLoader loader1 = new URLClassLoader(new URL[]{url}, null);
URLClassLoader loader2 = new URLClassLoader(new URL[]{url}, null);
Class<?> class1 = loader1.loadClass("org.example.singleton.HolderSetting");
Class<?> class2 = loader2.loadClass("org.example.singleton.HolderSetting");
各クラスローダは独立したネームスペースを持っています。
つまり、JVM はそれぞれを別のクラスとして認識します。
同様に、static
フィールドもそれぞれ独立して存在することになります。
private static class Holder {
private static final HolderSetting INSTANCE = new HolderSetting();
}
そのため、異なるシングルトンインスタンスが生成され、それらを比較すると false
が返されます。
クラスローダのネームスペース
クラスローダのネームスペースとは、クラスローダがロードしたクラスの固有領域です。
同じ名前のクラスでも、異なるクラスローダがロードすると、JVM はそれを別のクラスとして扱います。
リフレクション機能 (Reflection)
リフレクション機能を使用して private
コンストラクタにアクセスすることで、シングルトンを破ることが可能です。
結果
> Task :testClasses
false
リフレクションを使用してランタイム中に、
Constructor<HolderSetting> constructor = HolderSetting.class.getDeclaredConstructor();
constructor.setAccessible(true);
次のように private
コンストラクタを取得し、アクセスを許可します。
HolderSetting instance2 = constructor.newInstance();
その場合、次のようにアクセス修飾子を無視してコンストラクタを呼び出すことができます。
クローン(Clone)
クローンを使用してオブジェクトをコピーし、新しいオブジェクトを作成することが可能です。
clone()
Object
クラスの clone()
を確認すると、次のように定義されています。
/**
* Creates and returns a copy of this object. The precise meaning
* of "copy" may depend on the class of the object. The general
* intent is that, for any object {@code x}, the expression:
* <blockquote>
* <pre>
* x.clone() != x</pre></blockquote>
* will be true, and that the expression:
* <blockquote>
* <pre>
* x.clone().getClass() == x.getClass()</pre></blockquote>
* will be {@code true}, but these are not absolute requirements.
* While it is typically the case that:
* <blockquote>
* <pre>
* x.clone().equals(x)</pre></blockquote>
* will be {@code true}, this is not an absolute requirement.
* <p>
* By convention, the returned object should be obtained by calling
* {@code super.clone}. If a class and all of its superclasses (except
* {@code Object}) obey this convention, it will be the case that
* {@code x.clone().getClass() == x.getClass()}.
* <p>
* By convention, the object returned by this method should be independent
* of this object (which is being cloned). To achieve this independence,
* it may be necessary to modify one or more fields of the object returned
* by {@code super.clone} before returning it. Typically, this means
* copying any mutable objects that comprise the internal "deep structure"
* of the object being cloned and replacing the references to these
* objects with references to the copies. If a class contains only
* primitive fields or references to immutable objects, then it is usually
* the case that no fields in the object returned by {@code super.clone}
* need to be modified.
*
* @implSpec
* The method {@code clone} for class {@code Object} performs a
* specific cloning operation. First, if the class of this object does
* not implement the interface {@code Cloneable}, then a
* {@code CloneNotSupportedException} is thrown. Note that all arrays
* are considered to implement the interface {@code Cloneable} and that
* the return type of the {@code clone} method of an array type {@code T[]}
* is {@code T[]} where T is any reference or primitive type.
* Otherwise, this method creates a new instance of the class of this
* object and initializes all its fields with exactly the contents of
* the corresponding fields of this object, as if by assignment; the
* contents of the fields are not themselves cloned. Thus, this method
* performs a "shallow copy" of this object, not a "deep copy" operation.
* <p>
* The class {@code Object} does not itself implement the interface
* {@code Cloneable}, so calling the {@code clone} method on an object
* whose class is {@code Object} will result in throwing an
* exception at run time.
*
* @return a clone of this instance.
* @throws CloneNotSupportedException if the object's class does not
* support the {@code Cloneable} interface. Subclasses
* that override the {@code clone} method can also
* throw this exception to indicate that an instance cannot
* be cloned.
* @see java.lang.Cloneable
*/
@IntrinsicCandidate
protected native Object clone() throws CloneNotSupportedException;
重要な部分をまとめると次のようになります
x.clone() != x
x.clone().getClass() == x.getClass()
clone()
は元のオブジェクトとは異なる新しいオブジェクトであり、
複製されたクラスは元のオブジェクトと同じです。
This method performs a 'shallow copy' of this object, not a 'deep copy' operation
clone()
メソッドは、オブジェクトの浅いコピーのみを実行し、深いコピーは行いません。
"If the class of this object does not implement the interface Cloneable, then a CloneNotSupportedException is thrown."
オブジェクトを複製するには、Cloneable
インターフェースを必ず実装する必要があります。
この部分を参考にすると、理解が少し深まるかもしれません。
public class HolderSetting implements Cloneable {
private boolean darkMode;
private HolderSetting() {
darkMode = false;
}
private static class Holder {
private static final HolderSetting INSTANCE = new HolderSetting();
}
public static HolderSetting getInstance() {
return Holder.INSTANCE;
}
public void setDarkMode(boolean darkMode) {
this.darkMode = darkMode;
}
public boolean isDarkMode() {
return darkMode;
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
次のように Cloneable
インターフェースを実装し、clone()
メソッドを定義します。
結果
> Task :testClasses
false
次のように、先ほどの clone()
メソッドの説明にある通り、異なるオブジェクトを返し、
シングルトンが破られるという事実を確認できました。
enum
を使用したシングルトン
enum
を使用してシングルトンを保証する方法があります。
『Effective Java』という書籍にも次のような内容が記載されています:
- 列挙型のコンストラクタは
private
である - そのため、クライアントがクラスを直接生成したり拡張したりすることはできず、列挙型で宣言されたインスタンスは1つしか存在しない
public enum EnumSetting {
INSTANCE;
private boolean darkMode;
EnumSetting() {
this.darkMode = false;
}
public boolean isDarkMode() {
return darkMode;
}
public void setDarkMode(boolean darkMode) {
this.darkMode = darkMode;
}
}
以前のリフレクション、シリアライゼーション、クラスローダー、クローンについて、
enum
がシングルトンを適切に保持できるかテストを行いました。
結果
シリアライゼーションとデシリアライゼーション
シリアライゼーションとデシリアライゼーションにおいて、同じインスタンスが返されることを確認できました。
つまり、enum
はシリアライゼーションおよびデシリアライゼーション時にシングルトンを保証します。
クラスローダー
- 結果は
false
でした。つまり、シングルトンが破られました。
そもそも独立したクラスローダーがenum
クラスをロードした場合、異なるインスタンスが生成されるため(前述の説明を参照)、
enum
はクラスローダー方式でロードされる場合、シングルトンを保証することはできませんでした。
リフレクション
java.lang.NoSuchMethodException
というランタイムエラーが発生しました。
enum
はリフレクションを防止します。
欠点
- 即時初期化の欠点を共有します。この
enum
はクラスがロードされる際にインスタンスを生成するため、
使用されない場合、メモリリソースの非効率的な使用が発生します- 遅延ローディングを使用したい場合、他の方法を選択するのが適切です
- また、
enum
クラスは継承が不可能なため、拡張性に欠けます
クローン
enum
クラスは内部的に clone()
を定義しており、
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
以下のコードを実行すると CloneNotSupportedException
エラーが返されます。
シングルトンパターンの注意点
- シングルトンオブジェクトはシステム全体の状態を共有するため、テスト時に依存性を分離するのが難しいです
- テストの実行時、そのオブジェクトを直接参照するため、モックに置き換えることができません
- シングルトンオブジェクトを直接参照すると、コードの結合度が高まります。
強い結合に関する私の投稿 も参考にしてください - シングルトンパターンは可読性が非常に低下します。
- 明示的に生成する必要がなく、引数の渡し方にも依存しないため、コード内で呼び出された場合でも依存性を確認しにくいです
- シングルトンは、自らの固有性を保証する責任に加え、正常に機能を実行する責任も負うため、単一責任原則(SRP)に違反します
参考文献
https://en.wikipedia.org/wiki/Singleton_pattern
https://refactoring.guru/design-patterns/singleton
https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4/dashboard
https://www.reddit.com/r/csharp/comments/d37bp2/what_when_why_singleton_design_pattern/?rdt=55162