Singletons sind also schlecht, was dann?


553

In letzter Zeit wurde viel über die Probleme bei der Verwendung (und Überbeanspruchung) von Singletons diskutiert. Ich war auch schon früher in meiner Karriere einer dieser Leute. Ich kann jetzt das Problem erkennen, und dennoch gibt es immer noch viele Fälle, in denen ich keine gute Alternative sehe - und nicht viele der Anti-Singleton-Diskussionen bieten wirklich eine.

Hier ist ein reales Beispiel aus einem großen Projekt, an dem ich in letzter Zeit beteiligt war:

Die Anwendung war ein Thick-Client mit vielen separaten Bildschirmen und Komponenten, der große Datenmengen aus einem Serverstatus verwendet, der nicht zu oft aktualisiert wird. Diese Daten wurden im Grunde genommen in einem Singleton "Manager" -Objekt zwischengespeichert - dem gefürchteten "globalen Zustand". Die Idee war, diesen einen Ort in der App zu haben, an dem die Daten gespeichert und synchronisiert werden, und dann können alle neuen Bildschirme, die geöffnet werden, das meiste von dem abfragen, was sie von dort benötigen, ohne wiederholte Anfragen nach verschiedenen unterstützenden Daten vom Server zu stellen. Eine ständige Anfrage an den Server würde zu viel Bandbreite in Anspruch nehmen - und ich spreche von Tausenden von Dollar zusätzlichen Internetrechnungen pro Woche, was inakzeptabel war.

Gibt es einen anderen Ansatz, der hier angemessen sein könnte, als im Grunde ein solches globales Datenmanager-Cache-Objekt zu haben? Dieses Objekt muss offiziell natürlich kein "Singleton" sein, aber es ist konzeptionell sinnvoll, eines zu sein. Was ist hier eine schöne saubere Alternative?


10
Welches Problem soll der Einsatz eines Singleton lösen? Wie ist es besser, dieses Problem zu lösen als mit Alternativen (wie einer statischen Klasse)?
Anon.

14
@Anon: Wie verbessert die Verwendung einer statischen Klasse die Situation? Gibt es noch eine enge Kopplung?
Martin York

5
@Martin: Ich schlage nicht vor, dass es "besser" macht. Ich schlage vor, dass in den meisten Fällen ein Singleton eine Lösung für die Suche nach einem Problem ist.
Anon.

9
@Anon: Nicht wahr. Statische Klassen geben Ihnen (fast) keine Kontrolle über die Instanziierung und erschweren das Multithreading noch mehr als Singletons (da Sie den Zugriff auf jede einzelne Methode anstatt nur auf die Instanz serialisieren müssen). Singletons können auch mindestens eine Schnittstelle implementieren, die statische Klassen nicht können. Statische Klassen haben sicherlich ihre Vorteile, aber in diesem Fall ist der Singleton definitiv das kleinere von zwei erheblichen Übeln. Eine statische Klasse, die irgendeinen veränderlichen Zustand implementiert, ist wie ein großes blinkendes Neon "WARNUNG: BAD DESIGN AHEAD!" Schild.
Aaronaught

7
@Aaronaught: Wenn Sie nur den Zugriff auf den Singleton synchronisieren, ist Ihre Parallelität unterbrochen . Ihr Thread könnte kurz nach dem Abrufen des Singleton-Objekts unterbrochen werden, ein anderer Thread wird gestartet und die Race-Bedingung wird übernommen. Die Verwendung eines Singleton anstelle einer statischen Klasse bedeutet in den meisten Fällen lediglich, die Warnzeichen zu entfernen und das Problem zu lösen .
Anon.

Antworten:


809

Hierbei ist es wichtig, zwischen einzelnen Instanzen und dem Singleton-Entwurfsmuster zu unterscheiden .

Einzelne Instanzen sind einfach Realität. Die meisten Apps können jeweils nur mit einer Konfiguration, einer Benutzeroberfläche, einem Dateisystem usw. verwendet werden. Wenn viele Zustände oder Daten gepflegt werden müssen, möchten Sie mit Sicherheit nur eine Instanz haben und diese so lange wie möglich am Leben erhalten.

Das Singleton- Entwurfsmuster ist ein sehr spezifischer Typ einer einzelnen Instanz, und zwar:

  • Zugriff über ein globales statisches Instanzfeld;
  • Wird entweder bei der Programminitialisierung oder beim ersten Zugriff erstellt.
  • Kein öffentlicher Konstruktor (kann nicht direkt instanziieren);
  • Nie explizit freigegeben (implizit freigegeben bei Programmbeendigung).

Aufgrund dieser speziellen Designauswahl führt das Muster zu mehreren potenziellen Langzeitproblemen:

  • Unfähigkeit, abstrakte oder Interface-Klassen zu verwenden;
  • Unfähigkeit zur Unterklasse;
  • Hohe Kopplung über die Anwendung (schwierig zu ändern);
  • Schwierig zu testen (kann in Unit-Tests nicht fälschen / verspotten);
  • Schwierig zu parallelisieren im Falle eines veränderlichen Zustands (erfordert eine umfangreiche Verriegelung);
  • und so weiter.

Keines dieser Symptome ist tatsächlich für einzelne Instanzen endemisch, nur das Singleton-Muster.

Was können Sie stattdessen tun? Verwenden Sie einfach nicht das Singleton-Muster.

Zitat aus der Frage:

Die Idee war, diesen einen Ort in der App zu haben, an dem die Daten gespeichert und synchronisiert werden, und dann können alle neuen Bildschirme, die geöffnet werden, nur das meiste von dem abfragen, was sie von dort benötigen, ohne wiederholte Anfragen nach verschiedenen unterstützenden Daten vom Server zu stellen. Eine ständige Anfrage an den Server würde zu viel Bandbreite in Anspruch nehmen - und ich spreche von Tausenden von Dollar zusätzlichen Internetrechnungen pro Woche, was inakzeptabel war.

Dieses Konzept hat einen Namen, wie Sie andeuten, aber unsicher klingen. Es heißt Cache . Wenn Sie Lust haben, können Sie es als "Offline-Cache" oder nur als Offline-Kopie von Remote-Daten bezeichnen.

Ein Cache muss kein Singleton sein. Es kann brauchen eine einzelne Instanz sein , wenn Sie die gleichen Daten zu holen für mehrere Cache - Instanzen vermeiden möchten; das heißt aber nicht, dass man eigentlich alles allen aussetzen muss .

Das erste, was ich tun würde, ist, die verschiedenen Funktionsbereiche des Caches in separate Schnittstellen aufzuteilen. Angenommen, Sie haben den weltweit schlechtesten YouTube-Klon auf Basis von Microsoft Access erstellt:

                          MSAccessCache
                                ▲
                                |
              + ----------------- + ----------------- +
              | | |
         IMediaCache IProfileCache IPageCache
              | | |
              | | |
          VideoPage MyAccountPage MostPopularPage

Hier haben Sie mehrere Schnittstellen , die die spezifischen Datentypen beschreiben, auf die eine bestimmte Klasse möglicherweise zugreifen muss - Medien, Benutzerprofile und statische Seiten (wie die Startseite). All dies wird von einem Mega-Cache implementiert , aber Sie entwerfen Ihre einzelnen Klassen so, dass sie stattdessen die Schnittstellen akzeptieren, sodass es ihnen egal ist, welche Art von Instanz sie haben. Sie initialisieren die physische Instanz einmal, wenn Ihr Programm gestartet wird, und geben die Instanzen dann einfach über Konstruktoren und öffentliche Eigenschaften weiter (in einen bestimmten Schnittstellentyp umgewandelt).

Dies wird übrigens Abhängigkeitsinjektion genannt ; Sie müssen weder Spring noch einen speziellen IoC-Container verwenden, solange Ihr allgemeines Klassendesign die Abhängigkeiten vom Aufrufer akzeptiert, anstatt sie einzeln zu instanziieren oder auf den globalen Status zu verweisen .

Warum sollten Sie das schnittstellenbasierte Design verwenden? Drei Gründe:

  1. Dies erleichtert das Lesen des Codes. An den Schnittstellen können Sie genau erkennen, von welchen Daten die abhängigen Klassen abhängen.

  2. Wenn Sie feststellen, dass Microsoft Access nicht die beste Wahl für ein Daten-Back-End ist, können Sie es durch etwas Besseres ersetzen - sagen wir, SQL Server.

  3. Falls und wenn Sie , dass SQL Server erkennen nicht die beste Wahl für die Medien ist speziell , können Sie Ihre Implementierung brechen , ohne einen anderen Teil des Systems zu beeinträchtigen . Hier kommt die wahre Kraft der Abstraktion ins Spiel.

Wenn Sie noch einen Schritt weiter gehen möchten, können Sie einen IoC-Container (DI-Framework) wie Spring (Java) oder Unity (.NET) verwenden. Fast jedes DI-Framework führt eine eigene Lebensdauerverwaltung durch und ermöglicht es Ihnen, einen bestimmten Dienst als einzelne Instanz zu definieren (häufig als "Singleton" bezeichnet, dies dient jedoch nur der Vertrautheit). Grundsätzlich ersparen Ihnen diese Frameworks die meiste Affenarbeit beim manuellen Weitergeben von Instanzen, sind jedoch nicht unbedingt erforderlich. Sie benötigen keine speziellen Werkzeuge, um dieses Design zu implementieren.

Der Vollständigkeit halber möchte ich darauf hinweisen, dass das obige Design auch wirklich nicht ideal ist. Wenn Sie mit einem Cache arbeiten (so wie Sie sind), sollten Sie eigentlich eine völlig separate Ebene haben . Mit anderen Worten, ein Design wie dieses:

                                                        + - IMediaRepository
                                                        |
                          Cache (generisch) --------------- + - IProfileRepository
                                ▲ |
                                | + - IPageRepository
              + ----------------- + ----------------- +
              | | |
         IMediaCache IProfileCache IPageCache
              | | |
              | | |
          VideoPage MyAccountPage MostPopularPage

Dies hat den Vorteil, dass Sie Ihre CacheInstanz nicht einmal trennen müssen, wenn Sie sich für eine Umgestaltung entscheiden. Sie können ändern, wie Medien gespeichert werden, indem Sie sie einer alternativen Implementierung von zuführen IMediaRepository. Wenn Sie sich überlegen, wie dies zusammenpasst, werden Sie feststellen, dass immer nur eine physische Instanz eines Caches erstellt wird, sodass Sie niemals dieselben Daten zweimal abrufen müssen.

Nichts davon soll heißen, dass jedes einzelne Softwareteil auf der Welt nach diesen strengen Maßstäben hoher Kohäsion und loser Kopplung entwickelt werden muss. Dies hängt von der Größe und dem Umfang des Projekts, Ihrem Team, Ihrem Budget, den Fristen usw. ab. Wenn Sie jedoch nach dem besten Design fragen (anstelle eines Singletons), dann ist dies das Richtige.

PS Wie andere bereits gesagt haben, ist es wahrscheinlich nicht die beste Idee für die abhängigen Klassen, sich dessen bewusst zu sein, dass sie einen Cache verwenden - das ist ein Implementierungsdetail, das sie einfach nie interessieren sollten. Abgesehen davon würde die Gesamtarchitektur immer noch sehr ähnlich wie oben dargestellt aussehen. Sie würden die einzelnen Schnittstellen nur nicht als Caches bezeichnen . Stattdessen würden Sie sie Services oder ähnliches nennen.


131
Erster Beitrag, den ich jemals gelesen habe, der DI als Alternative zum globalen Staat erklärt. Vielen Dank für die Zeit und Mühe, die wir in diese Sache gesteckt haben. Durch diesen Beitrag geht es uns allen besser.
MrLane

4
Warum kann der Cache kein Singleton sein? Handelt es sich nicht um einen Singleton, wenn Sie ihn weitergeben und die Abhängigkeitsinjektion verwenden? Bei Singleton geht es nur darum, uns auf eine Instanz zu beschränken, nicht darum, wie darauf zugegriffen wird, oder? Siehe meine Einstellung
Erik Engheim

29
@AdamSmith: Hast du tatsächlich eine dieser Antworten gelesen ? Ihre Frage wird in den ersten beiden Absätzen beantwortet. Singleton-Muster! == Einzelinstanz.
Aaronaught

5
@ Kawas und Adam Smith - Lesen Sie Ihre Links Ich habe das Gefühl, Sie haben diese Antwort nicht wirklich gelesen - alles ist bereits da.
Wilbert

19
@ Kawas Ich glaube, dass das Fleisch dieser Antwort die Unterscheidung zwischen Einzelinstanz und Singleton ist. Singleton ist schlecht, Single-Instance nicht. Die Abhängigkeitsinjektion ist eine nette, allgemeine Möglichkeit, einzelne Instanzen zu verwenden, ohne Singletons verwenden zu müssen.
Wilbert,

48

In dem Fall, den Sie angeben, scheint die Verwendung eines Singleton nicht das Problem zu sein, sondern das Symptom eines Problems - ein größeres architektonisches Problem.

Warum fragen die Bildschirme das Cache-Objekt nach Daten ab? Das Caching sollte für den Client transparent sein. Es sollte eine geeignete Abstraktion zum Bereitstellen der Daten geben, und die Implementierung dieser Abstraktion könnte das Zwischenspeichern verwenden.

Das Problem ist wahrscheinlich, dass Abhängigkeiten zwischen Teilen des Systems nicht korrekt eingerichtet sind, und dies ist wahrscheinlich systembedingt.

Warum müssen die Bildschirme wissen, woher sie ihre Daten beziehen? Warum werden die Bildschirme nicht mit einem Objekt versehen, das ihre Datenanforderungen erfüllen kann (hinter denen ein Cache versteckt ist)? Oft ist die Verantwortung für die Erstellung von Bildschirmen nicht zentralisiert, und es gibt keinen klaren Grund, die Abhängigkeiten einzuschleusen.

Auch hier beschäftigen wir uns mit großen architektonischen und gestalterischen Problemen.

Es ist auch sehr wichtig zu verstehen, dass die Lebensdauer eines Objekts vollständig von der Art und Weise getrennt werden kann, wie das Objekt zur Verwendung gefunden wird.

Ein Cache muss während der gesamten Lebensdauer der Anwendung aktiv sein (um nützlich zu sein), damit die Lebensdauer des Objekts der eines Singleton entspricht.

Das Problem mit Singleton (zumindest die übliche Implementierung von Singleton als statische Klasse / Eigenschaft) ist, wie andere Klassen, die es verwenden, es finden.

Bei einer statischen Singleton-Implementierung besteht die Konvention darin, sie einfach dort einzusetzen, wo sie benötigt wird. Dadurch wird die Abhängigkeit jedoch vollständig ausgeblendet und die beiden Klassen werden eng miteinander verbunden.

Wenn wir der Klasse die Abhängigkeit zur Verfügung stellen , ist diese Abhängigkeit explizit und alle konsumierende Klasse, die Kenntnis haben muss, ist der Vertrag, über den sie verfügen kann.


2
Es gibt eine gigantische Datenmenge, die bestimmte Bildschirme möglicherweise benötigen, aber nicht unbedingt benötigen. Und Sie wissen nicht, bis Benutzeraktionen durchgeführt wurden, die dies definieren - und es gibt viele, viele Kombinationen. Daher wurden einige allgemeine globale Daten im Client zwischengespeichert und synchronisiert (meistens bei der Anmeldung abgerufen), und nachfolgende Anforderungen bauen den Cache mehr auf, da explizit angeforderte Daten in der Regel erneut verwendet werden die gleiche Sitzung. Das Hauptaugenmerk liegt auf der Reduzierung der Anforderungen an den Server, weshalb ein clientseitiger Cache erforderlich ist. <cont>
Bobby Tables

1
<cont> Es ist im Wesentlichen transparent. In dem Sinne, dass ein Rückruf vom Server erfolgt, wenn bestimmte erforderliche Daten noch nicht zwischengespeichert wurden. Die Implementierung (logisch und physisch) dieses Cache-Managers ist jedoch ein Singleton.
Bobby Tables

6
Ich bin mit Qstarin hier: Die Objekte, die auf die Daten zugreifen, sollten nicht wissen (oder müssen wissen), dass die Daten zwischengespeichert sind (das ist ein Implementierungsdetail). Die Benutzer der Daten fordern lediglich die Daten an (oder eine Schnittstelle zum Abrufen der Daten).
Martin York

1
Das Caching ist im Wesentlichen ein Implementierungsdetail. Es gibt eine Schnittstelle, über die Daten abgefragt werden, und die Objekte, die sie erhalten, wissen nicht, ob sie aus dem Cache stammen oder nicht. Unter diesem Cache-Manager befindet sich jedoch ein Singleton.
Bobby Tables

2
@ Bobby Tables: Dann ist Ihre Situation nicht so schlimm, wie es schien. Dieser Singleton (vorausgesetzt, Sie meinen eine statische Klasse, nicht nur ein Objekt mit einer Instanz, die so lange lebt wie die App) ist immer noch problematisch. Es verbirgt die Tatsache, dass Ihr datenlieferndes Objekt von einem Cache-Anbieter abhängig ist. Es ist besser, wenn das explizit und externalisiert ist. Entkopple sie. Für die Testbarkeit ist es wichtig, dass Sie Komponenten problemlos austauschen können. Ein Cache-Anbieter ist ein hervorragendes Beispiel für eine solche Komponente (wie oft wird ein Cache-Anbieter von ASP.Net unterstützt).
Quentin-Starin

45

Ich habe gerade zu dieser Frage ein ganzes Kapitel geschrieben . Meist im Zusammenhang mit Spielen, aber das meiste sollte außerhalb von Spielen gelten.

tl; dr:

Das Singleton-Muster "Vierergruppen" hat zwei Funktionen: Sie können bequem von überall auf ein Objekt zugreifen und sicherstellen, dass nur eine Instanz davon erstellt werden kann. In 99% der Fälle kümmert es Sie nur um die erste Hälfte, und wenn Sie in der zweiten Hälfte karren, um sie zu erhalten, ist dies eine unnötige Einschränkung.

Darüber hinaus gibt es bessere Lösungen für den bequemen Zugriff. Ein Objekt global zu machen, ist die nukleare Option, um das zu lösen, und macht es einfach, Ihre Einkapselung zu zerstören. Alles, was an Globalen schlecht ist, gilt nur für Singletons.

Wenn Sie es nur verwenden, weil Sie viele Stellen im Code haben, die dasselbe Objekt berühren müssen, versuchen Sie, eine bessere Möglichkeit zu finden, es nur diesen Objekten zu geben, ohne es der gesamten Codebasis auszusetzen. Andere Lösungen:

  • Lassen Sie es ganz fallen. Ich habe viele Singleton-Klassen gesehen, die keinen Status haben und nur eine Fülle von Hilfsfunktionen sind. Diese brauchen überhaupt keine Instanz. Machen Sie sie einfach zu statischen Funktionen oder verschieben Sie sie in eine der Klassen, die die Funktion als Argument verwendet. Sie würden keine spezielle MathKlasse brauchen, wenn Sie nur tun könnten 123.Abs().

  • Gib es weiter. Die einfache Lösung, wenn eine Methode ein anderes Objekt benötigt, besteht darin, es einfach weiterzugeben. Es ist nichts Falsches daran, einige Objekte weiterzugeben.

  • Legen Sie es in die Basisklasse. Wenn Sie viele Klassen haben, die alle Zugriff auf ein bestimmtes Objekt benötigen und eine Basisklasse gemeinsam haben, können Sie dieses Objekt zu einem Mitglied in der Basis machen. Wenn Sie es konstruieren, übergeben Sie das Objekt. Jetzt können die abgeleiteten Objekte es alle bekommen, wenn sie es brauchen. Wenn Sie den Schutz aktivieren, stellen Sie sicher, dass das Objekt weiterhin gekapselt bleibt.


1
Können Sie das hier zusammenfassen?
Nicole

1
Fertig, aber ich ermutige Sie trotzdem, das ganze Kapitel zu lesen.
Munificent

4
eine andere Alternative: Dependency Injection!
Brad Cupit

1
@BradCupit darüber spricht er auch auf dem Link ... Ich muss sagen, ich versuche immer noch, alles zu verdauen. Aber es war die aufschlussreichste Lektüre über Singletons, die ich je gelesen habe. Bis jetzt war ich positiv Singletons benötigt wurden, ebenso wie globale Vars, und ich war die Förderung der Toolbox . Jetzt weiß ich es einfach nicht mehr. Mr munificient können Sie mir sagen, ob der Service Locator nur eine statische Toolbox ist ? Wäre es nicht besser, ein Singleton (also eine Toolbox) daraus zu machen?
Cregox

1
"Toolbox" sieht für mich "Service Locator" ziemlich ähnlich. Ob Sie ein Static dafür verwenden oder es selbst zu einem Singleton machen, ist meiner Meinung nach für die meisten Programme nicht so wichtig. Ich tendiere eher zur Statik, denn warum beschäftige ich mich mit verzögerter Initialisierung und Heap-Zuweisung, wenn Sie dies nicht müssen?
Munificent

21

Es ist nicht der globale Zustand an sich, der das Problem ist.

Du musst dir wirklich nur Sorgen machen global mutable state. Der konstante Zustand wird nicht von Nebenwirkungen beeinflusst und ist daher weniger problematisch.

Das Hauptanliegen bei Singleton ist, dass es die Kopplung erhöht und so das Testen erschwert. Sie können die Kopplung verringern, indem Sie den Singleton von einer anderen Quelle (z. B. einer Factory) beziehen. Auf diese Weise können Sie den Code von einer bestimmten Instanz entkoppeln (obwohl Sie stärker an die Factory gekoppelt sind (aber zumindest kann die Factory alternative Implementierungen für verschiedene Phasen haben)).

In Ihrer Situation, denke ich, können Sie damit durchkommen, solange Ihr Singleton tatsächlich eine Schnittstelle implementiert (so dass eine Alternative in anderen Situationen verwendet werden kann).

Ein weiterer großer Nachteil von Singletons ist, dass das Entfernen von Singletons aus dem Code und das Ersetzen durch etwas anderes zu einer sehr schwierigen Aufgabe wird (da ist wieder diese Kopplung).

// Example from 5 minutes (con't be too critical)
class ServerFactory
{
    public:
        // By default return a RealServer
        ServerInterface& getServer();

        // Set a non default server:
        void setServer(ServerInterface& server);
};

class ServerInterface { /* define Interface */ };

class RealServer: public ServerInterface {}; // This is a singleton (potentially)

class TestServer: public ServerInterface {}; // This need not be.

Das macht Sinn. Das lässt mich auch denken, dass ich Singletons nie wirklich missbraucht habe, sondern nur daran gezweifelt habe, dass ich sie benutzt habe. Aber ich kann mir keinen direkten Missbrauch vorstellen, den ich gemäß diesen Punkten begangen habe. :)
Bobby Tables

2
(du meinst wahrscheinlich per se nicht "per say")
nohat

4
@nohat: Ich bin Muttersprachler des "Queens English" und lehne daher alles ab, was französisch aussieht, es sei denn, wir verbessern es (wie le weekendhoppla, das gehört zu unseren). Danke :-)
Martin York

21
an sich ist Latein.
Anon.

2
@Anon: OK. Das ist dann nicht so schlimm ;-)
Martin York

19

Dann was? Da hat es keiner gesagt: Toolbox . Das ist, wenn Sie globale Variablen möchten .

Missbrauch durch Singleton kann vermieden werden, indem das Problem aus einem anderen Blickwinkel betrachtet wird. Angenommen, eine Anwendung benötigt nur eine Instanz einer Klasse und die Anwendung konfiguriert diese Klasse beim Start: Warum sollte die Klasse selbst dafür verantwortlich sein, ein Singleton zu sein? Es erscheint logisch, dass die Anwendung diese Verantwortung übernimmt, da die Anwendung dieses Verhalten erfordert. Die Anwendung, nicht die Komponente, sollte der Singleton sein. Die Anwendung stellt dann eine Instanz der Komponente für jeden anwendungsspezifischen Code zur Verfügung. Wenn eine Anwendung mehrere solcher Komponenten verwendet, kann sie diese zu einer sogenannten Toolbox zusammenfassen.

Einfach ausgedrückt ist die Toolbox der Anwendung ein Singleton, der entweder für die Konfiguration selbst oder für die Konfiguration durch den Startmechanismus der Anwendung verantwortlich ist ...

public class Toolbox {
     private static Toolbox _instance; 

     public static Toolbox Instance {
         get {
             if (_instance == null) {
                 _instance = new Toolbox(); 
             }
             return _instance; 
         }
     }

     protected Toolbox() {
         Initialize(); 
     }

     protected void Initialize() {
         // Your code here
     }

     private MyComponent _myComponent; 

     public MyComponent MyComponent() {
         get {
             return _myComponent(); 
         }
     }
     ... 

     // Optional: standard extension allowing
     // runtime registration of global objects. 
     private Map components; 

     public Object GetComponent (String componentName) {
         return components.Get(componentName); 
     }

     public void RegisterComponent(String componentName, Object component) 
     {
         components.Put(componentName, component); 
     }

     public void DeregisterComponent(String componentName) {
         components.Remove(componentName); 
     }

}

Aber rate mal was? Es ist ein Singleton!

Und was ist ein Singleton?

Vielleicht beginnt hier die Verwirrung.

Für mich ist der Singleton ein Objekt, das immer und nur eine Instanz haben muss. Sie können jederzeit und überall darauf zugreifen, ohne sie instanziieren zu müssen. Deshalb ist es so eng mit static. Zum Vergleich staticist es im Grunde dasselbe, außer dass es keine Instanz ist. Wir müssen es nicht instanziieren, wir können es auch nicht, weil es automatisch zugewiesen wird. Und das kann und bringt Probleme.

Aus meiner Erfahrung heraus löste das einfache Ersetzen staticvon Singleton viele Probleme in einem mittelgroßen Projekt für Patchwork-Taschen, an dem ich beteiligt bin. Das bedeutet nur, dass es für schlecht gestaltete Projekte eine gewisse Verwendung hat. Ich denke , es ist zu viel Diskussion , wenn die Singletonmuster ist nützlich oder nicht , und ich kann nicht wirklich behaupten , wenn es in der Tat ist schlecht . Dennoch gibt es im Allgemeinen gute Argumente für Singleton gegenüber statischen Methoden .

Ich bin mir nur sicher, dass Singletons schlecht sind, wenn wir sie verwenden, während wir gute Praktiken ignorieren. Damit ist es in der Tat nicht so einfach umzugehen. Aber schlechte Praktiken können auf jedes Muster angewendet werden. Und, ich weiß, es ist zu allgemein, um zu sagen, dass ... ich meine, es steckt einfach zu viel dahinter.

Versteh mich nicht falsch!

Einfach ausgedrückt, wie globaler Vars , Singletons sollte nach wie vor jederzeit vermieden werden . Vor allem, weil sie übermäßig missbraucht werden. Globale Unterschiede lassen sich jedoch nicht immer vermeiden, und wir sollten sie in diesem letzten Fall verwenden.

Wie auch immer, es gibt viele andere Vorschläge als die Toolbox, und genau wie die Toolbox hat auch jeder seine Anwendung ...

Andere Alternativen

  • Der beste Artikel, den ich gerade über Singletons gelesen habe, schlägt Service Locator als Alternative vor. Für mich ist das im Grunde genommen eine " statische Toolbox ", wenn man so will. Mit anderen Worten, machen Sie den Service Locator zu einem Singleton und Sie haben eine Toolbox. Das widerspricht natürlich dem ursprünglichen Vorschlag, den Singleton zu meiden, aber nur, um Singleton durchzusetzen, geht es darum, wie er verwendet wird, und nicht um das Muster an sich.

  • Andere schlagen Factory Pattern als Alternative vor. Es war die erste Alternative, die ich von einem Kollegen hörte, und wir haben sie schnell für unsere Verwendung als globale Variable entfernt . Es hat sicher seine Verwendung, aber auch Singletons.

Beide oben genannten Alternativen sind gute Alternativen. Aber alles hängt von Ihrer Nutzung ab.

Es ist einfach falsch, anzunehmen, dass Singletons um jeden Preis vermieden werden sollten ...

  • Aaronaughts Antwort lautet , aus einer Reihe von Gründen niemals Singletons zu verwenden . Aber sie sind alle Gründe gegen die Art und Weise, wie sie missbraucht und missbraucht werden, und nicht direkt gegen das Muster selbst. Ich bin mit all den Sorgen über diese Punkte einverstanden, wie kann ich das nicht? Ich denke nur, dass es irreführend ist.

Die Unfähigkeiten (zu abstrakt oder Unterklasse) sind zwar da, aber na und? Es ist nicht dafür gedacht. Soweit ich das beurteilen kann, gibt es keine Unfähigkeit, eine Schnittstelle herzustellen . Es kann auch eine hohe Kopplung geben, aber das liegt nur daran, wie sie üblicherweise verwendet wird. Muss es nicht . Tatsächlich hat die Kopplung an sich nichts mit dem Singleton-Muster zu tun. Da dies geklärt ist, entfällt bereits die Schwierigkeit zu testen. Was die Schwierigkeit der Parallelisierung betrifft, so hängt dies von der Sprache und der Plattform ab, sodass dies wiederum kein Problem für das Muster darstellt.

Praxisbeispiele

Ich sehe oft 2, sowohl zugunsten als auch gegen Singletons. Webcache (mein Fall) und Protokolldienst .

Einige werden argumentieren , dass die Protokollierung ein perfektes Singleton-Beispiel ist, denn ich zitiere:

  • Die Anforderer benötigen ein bekanntes Objekt, an das Anforderungen zur Protokollierung gesendet werden sollen. Dies bedeutet einen globalen Zugangspunkt.
  • Da der Protokollierungsdienst eine einzelne Ereignisquelle ist, für die sich mehrere Listener registrieren können, muss nur eine Instanz vorhanden sein.
  • Obwohl sich verschiedene Anwendungen auf verschiedenen Ausgabegeräten anmelden können, ist die Art und Weise, wie sie ihre Listener registrieren, immer dieselbe. Alle Anpassungen werden über die Listener vorgenommen. Clients können die Protokollierung anfordern, ohne zu wissen, wie oder wo der Text protokolliert wird. Daher würde jede Anwendung den Protokollierungsdienst genauso verwenden.
  • Jede Anwendung sollte mit nur einer Instanz des Protokollierungsdienstes auskommen können.
  • Jedes Objekt kann ein Protokollierungsanforderer sein, einschließlich wiederverwendbarer Komponenten, sodass sie nicht an eine bestimmte Anwendung gekoppelt werden sollten.

Während andere argumentieren, ist es schwierig, den Protokolldienst zu erweitern, sobald Sie feststellen, dass es sich eigentlich nicht nur um eine Instanz handeln sollte.

Nun, ich sage beide Argumente sind gültig. Das Problem liegt auch hier nicht im Singleton-Muster. Es ist auf architektonische Entscheidungen und Gewichtung, wenn Refactoring ein tragfähiges Risiko ist. Ein weiteres Problem, wenn Refactoring in der Regel die letzte erforderliche Korrekturmaßnahme ist.


@gnat Danke! Ich dachte nur daran, die Antwort zu überarbeiten, um eine Warnung bezüglich der Verwendung von Singletons hinzuzufügen ... Ihr Zitat passt perfekt!
Cregox

2
froh, dass es dir gefallen hat. Ich bin mir nicht sicher, ob dies hilft, Abstimmungen zu vermeiden - wahrscheinlich fällt es den Lesern schwer, in diesem Beitrag auf ein konkretes Problem hinzuweisen, das in der Frage dargelegt wurde, insbesondere angesichts der hervorragenden Analyse, die in der vorherigen Antwort
gnat

@gnat ja, ich wusste, dass dies ein langer Kampf war. Ich hoffe die Zeit wird es zeigen. ;-)
cregox

1
Ich stimme Ihrer allgemeinen Meinung hier zu, auch wenn eine Bibliothek, die nicht viel mehr zu sein scheint als ein extrem nackter IoC-Container (noch grundlegender als zum Beispiel Funq ) , vielleicht etwas zu begeistert ist . Service Locator ist eigentlich ein Anti-Pattern. Es ist ein nützliches Tool, das hauptsächlich in Legacy- / Brownfield-Projekten sowie in "Poor-Man's DI" -Projekten zum Einsatz kommt. Dort wäre es zu teuer, alles neu zu faktorisieren, um einen IoC-Container ordnungsgemäß zu verwenden.
Aaronaught

1
@ Kawas: Ah, sorry - wurde verwechselt mit java.awt.Toolkit. Mein Standpunkt ist jedoch derselbe: ToolboxKlingt eher nach einem Grabbag aus nicht zusammenhängenden Teilen und Stücken als nach einer zusammenhängenden Klasse mit einem einzigen Zweck. Klingt für mich nicht nach gutem Design. (Beachten Sie, dass der Artikel, auf den Sie sich beziehen, aus dem Jahr 2001 stammt, bevor Abhängigkeitsinjektion und DI-Container an der Tagesordnung waren.)
Jon Skeet,

5

Mein Hauptproblem mit dem Singleton-Entwurfsmuster ist, dass es sehr schwierig ist, gute Komponententests für Ihre Anwendung zu schreiben.

Jede Komponente, die von diesem "Manager" abhängig ist, fragt dazu ihre Singleton-Instanz ab. Und wenn Sie einen Komponententest für eine solche Komponente schreiben möchten, müssen Sie Daten in diese Singleton-Instanz einfügen, was möglicherweise nicht einfach ist.

Wenn andererseits Ihr "Manager" über einen Konstruktorparameter in die abhängigen Komponenten injiziert wird und die Komponente den konkreten Typ des Managers nicht kennt, nur eine Schnittstelle oder eine abstrakte Basisklasse, die der Manager implementiert, dann eine Einheit test kann beim Testen von Abhängigkeiten alternative Implementierungen des Managers bereitstellen.

Wenn Sie IOC-Container verwenden, um die Komponenten zu konfigurieren und zu instanziieren, aus denen Ihre Anwendung besteht, können Sie Ihren IOC-Container auf einfache Weise so konfigurieren, dass nur eine Instanz des "Managers" erstellt wird, sodass Sie dieselbe Instanz erhalten, die den globalen Anwendungscache steuert .

Wenn Sie sich jedoch nicht für Komponententests interessieren, kann ein Singleton-Entwurfsmuster durchaus in Ordnung sein. (aber ich würde es trotzdem nicht tun)


Schön erklärt, dies ist die Antwort, die das Problem des Testens von Singletons am besten erklärt
José Tomás Tocino,

4

Ein Singleton ist nicht grundsätzlich schlecht , in dem Sinne, dass Design-Computing gut oder schlecht sein kann. Es kann immer nur richtig sein (gibt die erwarteten Ergebnisse) oder nicht. Es kann auch nützlich sein oder nicht, wenn es den Code klarer oder effizienter macht.

Ein Fall, in dem Singletons nützlich sind, ist, wenn sie eine Entität darstellen, die wirklich einzigartig ist. In den meisten Umgebungen sind Datenbanken eindeutig, es gibt wirklich nur eine Datenbank. Das Herstellen einer Verbindung zu dieser Datenbank kann kompliziert sein, da hierfür spezielle Berechtigungen erforderlich sind oder mehrere Verbindungstypen durchlaufen werden müssen. Die Organisation dieser Verbindung zu einem Singleton ist wahrscheinlich nur aus diesem Grund sehr sinnvoll.

Sie müssen jedoch auch sicherstellen, dass der Singleton wirklich ein Singleton und keine globale Variable ist. Dies ist wichtig, wenn es sich bei der einzelnen, eindeutigen Datenbank tatsächlich um vier Datenbanken handelt, jeweils eine für Produktions-, Staging-, Entwicklungs- und Testvorrichtungen. Ein Database Singleton findet heraus, mit welcher dieser Datenbanken eine Verbindung hergestellt werden soll, greift nach der einzelnen Instanz für diese Datenbank, stellt bei Bedarf eine Verbindung her und sendet sie an den Aufrufer zurück.

Wenn ein Singleton nicht wirklich ein Singleton ist (dies ist der Fall, wenn sich die meisten Programmierer aufregen), gibt es keine Möglichkeit, eine korrekte Instanz zu injizieren.

Ein weiteres nützliches Merkmal eines gut gestalteten Singleton-Musters ist, dass es häufig nicht beobachtbar ist. Der Anrufer bittet um eine Verbindung. Der Dienst, der es bereitstellt, kann ein zusammengefasstes Objekt zurückgeben oder, wenn er einen Test ausführt, für jeden Aufrufer ein neues Objekt erstellen oder stattdessen ein Scheinobjekt bereitstellen.


3

Die Verwendung des Singleton-Musters, das tatsächliche Objekte darstellt, ist vollkommen akzeptabel. Ich schreibe für das iPhone und es gibt viele Singletons im Cocoa Touch-Framework. Die Anwendung selbst wird durch einen Singleton der Klasse dargestellt UIApplication. Es gibt nur eine Anwendung, die Sie sind, daher ist es angemessen, diese mit einem Singleton darzustellen.

Die Verwendung eines Singletons als Datenmanager-Klasse ist in Ordnung, solange das Design stimmt. Wenn es sich um einen Bereich von Dateneigenschaften handelt, ist dies nicht besser als der globale Bereich. Wenn es eine Reihe von Gettern und Setzern ist, ist das besser, aber immer noch nicht großartig. Wenn es sich um eine Klasse handelt, die wirklich alle Schnittstellen zu Daten verwaltet, einschließlich des Abrufens entfernter Daten, des Zwischenspeicherns, Einrichtens und Herunterfahrens ... Das könnte sehr nützlich sein.


2

Singletons sind nur die Projektion einer serviceorientierten Architektur in ein Programm.

Eine API ist ein Beispiel für einen Singleton auf Protokollebene. Sie greifen auf Twitter, Google usw. über Singletons zu. Warum werden Singletons in einem Programm schlecht?

Es kommt darauf an, wie Sie sich ein Programm vorstellen. Wenn Sie sich ein Programm eher als eine Gesellschaft von Diensten vorstellen als zufällig gebundene zwischengespeicherte Instanzen, dann sind Singletons absolut sinnvoll.

Singletons sind ein Service Access Point. Die öffentliche Schnittstelle zu einer eng gebundenen Bibliothek von Funktionen, die möglicherweise eine sehr ausgefeilte interne Architektur verbirgt.

Ich sehe einen Singleton also nicht anders als eine Fabrik. Dem Singleton können Konstruktorparameter übergeben werden. Er kann in einem Kontext erstellt werden, der beispielsweise weiß, wie der Standarddrucker anhand aller möglichen Auswahlmechanismen aufgelöst wird. Zum Testen können Sie Ihr eigenes Mock einfügen. Es kann also sehr flexibel sein.

Der Schlüssel befindet sich intern in einem Programm, wenn ich es ausführe und ein bisschen Funktionalität benötige. Ich kann auf den Singleton mit der vollen Sicherheit zugreifen, dass der Dienst verfügbar und einsatzbereit ist. Dies ist der Schlüssel, wenn in einem Prozess verschiedene Threads beginnen, die eine Statusmaschine durchlaufen müssen, um als bereit angesehen zu werden.

Normalerweise würde ich eine XxxServiceKlasse umschließen, die ein Singleton um eine Klasse umschließt Xxx. Der Singleton ist überhaupt nicht in der Klasse Xxx, er ist in eine andere Klasse aufgeteilt XxxService. Dies liegt daran, Xxxdass mehrere Instanzen möglich sind, obwohl dies nicht wahrscheinlich ist. Wir möchten jedoch, dass Xxxauf jedem System eine Instanz global verfügbar ist. XxxServicebietet eine schöne Trennung von Bedenken. XxxEs muss keine Singleton-Richtlinie erzwungen werden, wir können sie jedoch bei Bedarf Xxxals Singleton verwenden.

So etwas wie:

//XxxService.h:
/**
 * Provide singleton wrapper for Xxx object. This wrapper
 * can be autogenerated so is not made part of the object.
 */

#include "Xxx/Xxx.h"


class XxxService
{
    public:
    /**
     * Return a Xxx object as a singleton. The double check
     * singleton algorithm is used. A 0 return means there was
     * an error. Developers should use this as the access point to
     * get the Xxx object.
     *
     * <PRE>
     * @@ #include "Xxx/XxxService.h"
     * @@ Xxx* xxx= XxxService::Singleton();
     * <PRE>
     */

     static Xxx*     Singleton();

     private:
         static Mutex  mProtection;
};


//XxxService.cpp:

#include "Xxx/XxxService.h"                   // class implemented
#include "LockGuard.h"     

// CLASS SCOPE
//
Mutex XxxService::mProtection;

Xxx* XxxService::Singleton()
{
    static Xxx* singleton;  // the variable holding the singleton

    // First check to see if the singleton has been created.
    //
    if (singleton == 0)
    {
        // Block all but the first creator.
        //
        LockGuard lock(mProtection);

        // Check again just in case someone had created it
        // while we were blocked.
        //
        if (singleton == 0)
        {
            // Create the singleton Xxx object. It's assigned
            // to a temporary so other accessors don't see
            // the singleton as created before it really is.
            //
            Xxx* inprocess_singleton= new Xxx;

            // Move the singleton to state online so we know that is has
            // been created and it ready for use.
            //
            if (inprocess_singleton->MoveOnline())
            {
                LOG(0, "XxxService:Service: FAIL MoveOnline");
                return 0;
            }

            // Wait until the module says it's in online state.
            //
            if (inprocess_singleton->WaitTil(Module::MODULE_STATE_ONLINE))
            {
                LOG(0, "XxxService:Service: FAIL move to online");
                return 0;
            }

            // The singleton is created successfully so assign it.
            //
            singleton= inprocess_singleton;


        }// still not created
    }// not created

    // Return the created singleton.
    //
    return singleton;

}// Singleton  

1

Erste Frage, finden Sie viele Fehler in der Anwendung? Vielleicht vergessen, den Cache zu aktualisieren oder den Cache zu beschädigen oder es schwierig zu finden, ihn zu ändern? (Ich erinnere mich, dass eine App die Größe nicht ändern würde, wenn Sie nicht auch die Farbe ändern würden. Sie können jedoch die Farbe zurück ändern und die Größe beibehalten.)

Was Sie tun würden, ist, diese Klasse zu haben, aber ALLE STATISCHEN MITGLIEDER ZU ENTFERNEN. Ok, das ist nicht notwendig, aber ich empfehle es. In Wirklichkeit initialisieren Sie die Klasse einfach wie eine normale Klasse und übergeben den Zeiger. Sagen Sie nicht "ClassIWant.APtr (). LetMeChange.ANYTHINGATALL (). Andhave_no_structure ()".

Es ist mehr Arbeit, aber wirklich weniger verwirrend. Einige Orte, an denen Sie Dinge nicht ändern sollten, die Sie jetzt nicht mehr können, da sie nicht mehr global sind. Alle meine Manager-Klassen sind reguläre Klassen, behandeln Sie es einfach so.


1

IMO, dein Beispiel klingt okay. Ich würde vorschlagen, wie folgt auszurechnen: Cache-Objekt für jedes (und hinter jedem) Datenobjekt; Cache-Objekte und die DB-Accessor-Objekte haben dieselbe Schnittstelle. Dies gibt die Möglichkeit, Caches in und aus dem Code zu tauschen. Plus es gibt eine einfache Expansionsroute.

Grafik:

DB
|
DB Accessor for OBJ A
| 
Cache for OBJ A
|
OBJ A Client requesting

DB-Accessor und Cache können vom selben Objekt oder Ententyp erben, sodass sie unabhängig davon wie dasselbe Objekt aussehen. Solange Sie stecken / kompilieren / testen können und es noch funktioniert.

Dies entkoppelt Dinge, sodass Sie neue Caches hinzufügen können, ohne ein Uber-Cache-Objekt bearbeiten zu müssen. YMMV. IANAL. USW.


1

Ein bisschen spät zur Party, aber trotzdem.

Singleton ist wie alles andere ein Werkzeug in einer Toolbox. Hoffentlich haben Sie mehr in Ihrem Werkzeugkasten als nur einen einzigen Hammer.

Bedenken Sie:

public void DoSomething()
{
    MySingleton.Instance.Work();
}

vs

public void DoSomething(MySingleton singleton)
{
    singleton.Work();
}
DoSomething(MySingleton.instance);

1. Fall führt zu hoher Kopplung usw .; 2. Weg hat keine Probleme @Aaronaught beschreibt, soweit ich das beurteilen kann. Es dreht sich alles darum, wie Sie es verwenden.


Nicht zustimmen. Obwohl der zweite Weg "besser" ist (verringerte Kopplung), gibt es immer noch Probleme, die von DI gelöst werden. Sie sollten sich nicht darauf verlassen, dass der Consumer Ihrer Klasse die Implementierung Ihrer Services bereitstellt. Dies geschieht besser im Konstruktor, wenn die Klasse erstellt wird. Ihre Schnittstelle sollte nur das Nötigste an Argumenten erfordern. Es besteht auch eine gute Chance, dass Ihre Klasse eine einzelne Instanz für die Ausführung benötigt. Auch hier ist es riskant und unnötig, sich auf den Consumer zu verlassen, um diese Regel durchzusetzen.
AlexFoxGill

Manchmal ist Di ein Overkill für eine bestimmte Aufgabe. Die Methode in einem Beispielcode könnte ein Konstruktor sein, aber nicht unbedingt - ohne ein konkretes Beispiel zu betrachten, ist es ein strittiges Argument. DoSomething könnte auch ISomething benötigen und MySingleton könnte diese Schnittstelle implementieren - ok, das ist nicht in einem Beispiel, aber es ist nur ein Beispiel.
Evgeni

1

Lassen Sie jeden Bildschirm den Manager in seinem Konstruktor aufnehmen.

Wenn Sie Ihre App starten, erstellen Sie eine Instanz des Managers und geben sie weiter.

Dies wird als Inversion of Control bezeichnet und ermöglicht es Ihnen, den Controller bei Konfigurationsänderungen und in Tests auszutauschen. Darüber hinaus können Sie mehrere Instanzen Ihrer Anwendung oder Teile Ihrer Anwendung parallel ausführen (gut zum Testen!). Zuletzt stirbt Ihr Manager mit seinem eigenen Objekt (der Startklasse).

So strukturieren Sie Ihre App wie einen Baum, in dem die übergeordneten Elemente alle untergeordneten Elemente enthalten. Implementieren Sie keine App wie ein Mesh, bei der jeder jeden kennt und sich über globale Methoden findet.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.