web-dev-qa-db-ja.com

シングルトンはいつ適切ですか?

シングルトンパターン は常にアンチパターンであると考える人もいます。どう思いますか?

67
Fishtoaster

シングルトンに対する2つの主な批判は、私が観察したものから2つの陣営に分類されます。

  1. シングルトンは、能力の低いプログラマーによって誤用および乱用されるため、すべてがシングルトンになり、Class :: get_instance()参照が散らばったコードが表示されます。一般的に言って、シングルトンパターンの使用に適格なリソースは(データベース接続などの)1つまたは2つだけです。
  2. シングルトンは本質的に静的クラスであり、1つ以上の静的メソッドとプロパティに依存しています。静的なものはすべて、ユニットテストを実行しようとすると、実際の具体的な問題を引き起こします。これは、モックやスタブできないコードの行き止まりを表すためです。その結果、シングルトン(またはその他の静的メソッドまたはクラス)に依存するクラスをテストする場合、そのクラスだけでなく静的メソッドまたはクラスもテストすることになります。

これらの両方の結果として、一般的なアプローチは、これらのクラスの単一のインスタンスを保持するために広範なコンテナーオブジェクトを作成することを使用し、コンテナーオブジェクトのみがこれらのタイプのクラスを変更しますが、他の多くのクラスは、コンテナオブジェクト。

32
Noah Goodrich

それはアンチパターンであることに同意します。どうして?それは、コードがその依存関係について嘘をつくことを可能にし、他のプログラマーが以前の不変のシングルトンに変更可能な状態を導入しないことを信頼できないからです。

クラスには文字列のみを受け取るコンストラクターがある場合があるため、それは分離してインスタンス化され、副作用がないと考えます。ただし、静かに、ある種のパブリックでグローバルに利用可能なシングルトンオブジェクトと通信しているため、クラスをインスタンス化するときは常に、異なるデータが含まれています。これは、APIのユーザーだけでなく、コードのテスト容易性にとっても大きな問題です。コードを適切にユニットテストするには、一貫性のあるテスト結果を取得するために、シングルトンのグローバル状態をマイクロ管理して認識する必要があります。

42
Magnus Wolffelt

シングルトンパターンは、基本的に遅延初期化されたグローバル変数です。グローバル変数は、プログラムの関連していないように見える部分の間の距離で不気味なアクションを可能にするため、一般的には正しく悪と見なされます。ただし、IMHOは、プログラムの初期化ルーチンの一部として(たとえば、構成ファイルまたはコマンドライン引数を読み取ることによって)1か所から一度設定され、その後定数として扱われるグローバル変数に問題はありません。このようなグローバル変数の使用は、コンパイル時に名前付き定数を宣言することとは、精神的にではなく、文字のみが異なります。

同様に、シングルトンについての私の見解は、プログラムの関連していないように見える部分間で変更可能な状態を渡すために使用された場合にのみ、それらは悪いということです。変更可能な状態が含まれていない場合、または含まれている変更可能な状態が完全にカプセル化されているため、マルチスレッド環境でもオブジェクトのユーザーがその状態を知る必要がない場合は、問題はありません。

34
dsimcha

なぜ人々はそれを使うのですか?

PHP=の世界でかなりの数のシングルトンを見てきました。正当化すべきパターンを見つけたユースケースを覚えていません。しかし、なぜその動機についてのアイデアを得たと思います人々はそれを使いました。

  1. シングルインスタンス

    「アプリケーション全体でクラスCの単一のインスタンスを使用します。」

    これは合理的な要件です。 「デフォルトのデータベース接続」用。これは、2番目のdb接続を作成しないことを意味するのではなく、通常はデフォルトの接続で作業することを意味します。

  2. シングルインスタンス化

    「クラスCを複数回インスタンス化しないでください(プロセスごと、リクエストごとなど)。」

    これが関係するのは、クラスをインスタンス化すると、他のインスタンスと競合する副作用がある場合のみです。

    多くの場合、これらの競合はコンポーネントを再設計することで回避できます。クラスコンストラクターから副作用を排除する。または、他の方法で解決することもできます。しかし、正当なユースケースがまだあるかもしれません。

    また、「1つのみ」の要件が本当に「プロセスごとに1つ」を意味するかどうかについても考慮する必要があります。例えば。リソースの同時実行性の要件は、「プロセス全体で1つ」ではなく、「プロセス全体で1つのシステム全体に1つ」です。そして、他のことについては、それはむしろ「アプリケーションコンテキスト」ごとであり、プロセスごとにたまたま1つのアプリケーションコンテキストを持つだけです。

    それ以外の場合は、この仮定を強制する必要はありません。これを適用すると、単体テスト用に別のインスタンスを作成できなくなります。

  3. グローバルアクセス

    これは、オブジェクトが使用される場所にオブジェクトを渡す適切なインフラストラクチャがない場合にのみ合法です。これはあなたのフレームワークや環境が悪いことを意味するかもしれませんが、それを修正するあなたの力の範囲内ではないかもしれません。

    価格は密結合、隠れた依存関係、そしてグローバルな状態の悪い点すべてです。しかし、あなたはおそらくすでにこれらに苦しんでいます。

  4. 遅延インスタンス化

    これはシングルトンの必要な部分ではありませんが、それらを実装する最も一般的な方法のようです。しかし、遅延インスタンス化は良いものですが、それを実現するためにシングルトンは本当に必要ありません。

典型的な実装

典型的な実装は、プライベートコンストラクターと静的インスタンス変数を備えたクラス、および遅延インスタンス化を備えた静的getInstance()メソッドです。

上記の問題に加えて、クラスは 独自のインスタンス化とライフサイクルを制御する を行うため、これは 単一責任の原則 にかみ合います。クラスはすでに持っています。

結論

多くの場合、シングルトンがなくても、グローバルな状態がなくても同じ結果を得ることができます。代わりに、依存性注入を使用する必要があり、 依存性注入コンテナー を検討する必要があります。

ただし、次の有効な要件が残っているユースケースもあります。

  • 単一インスタンス(単一インスタンス化ではない)
  • グローバルアクセス(青銅器時代のフレームワークを使用しているため)
  • 怠惰なインスタンス化(持っているだけでいいので)

したがって、この場合にできることは次のとおりです。

  • パブリックコンストラクターを使用して、インスタンス化するクラスCを作成します。

  • 静的インスタンス変数と、インスタンスにクラスCを使用する遅延インスタンス化を含む静的S :: getInstance()メソッドを使用して、別のクラスSを作成します。

  • Cのコンストラクターからすべての副作用を取り除きます。代わりに、これらの副作用をS :: getInstance()メソッドに入れます。

  • 上記の要件を持つクラスが複数ある場合は、クラスインスタンスを 小さなローカルサービスコンテナー で管理し、コンテナーにのみ静的インスタンスを使用することを検討できます。したがって、S :: getContainer()は遅延インスタンス化されたサービスコンテナーを提供し、コンテナーから他のオブジェクトを取得します。

  • 可能な場合は静的getInstance()を呼び出さないでください。可能な限り、代わりに依存性注入を使用してください。特に、相互に依存する複数のオブジェクトでコンテナアプローチを使用する場合、これらのいずれもS :: getContainer()を呼び出す必要はありません。

  • オプションで、クラスCが実装するインターフェースを作成し、これを使用してS :: getInstance()の戻り値を文書化します。

(まだこれをシングルトンと呼びますか?私はこれをコメントセクションに残します。)

利点:

  • グローバル状態を変更せずに、単体テスト用にCの個別のインスタンスを作成できます。

  • インスタンス管理はクラス自体から分離されます->懸念の分離、単一責任の原則。

  • S :: getInstance()にインスタンスに別のクラスを使用させたり、使用するクラスを動的に決定したりするのは非常に簡単です。

7
donquixote

個人的には、問題の特定のクラスに1、2、3、または限られた数のオブジェクトが必要な場合にシングルトンを使用します。または、クラスのユーザーに、クラスの複数のインスタンスを作成して、正しく機能させたくないことを伝えたいと思います。

また、コードのほぼすべての場所で使用する必要があり、オブジェクトをパラメーターとして必要な各クラスまたは関数にパラメーターとして渡したくない場合にのみ使用します。

さらに、他の関数の参照透過性を壊さない場合にのみ、シングルトンを使用します。何らかの入力が与えられると、常に同じ出力が生成されます。つまり私はそれをグローバルな状態に使用しません。おそらくそのグローバル状態が一度初期化され、決して変更されない限り。

使用しない場合は上記3を参照し、逆に変更してください。

1
Brian R. Bondy