it-swarm.dev

Singletons sind also schlecht, was dann?

In letzter Zeit wurde viel über die Probleme bei der Verwendung (und Überbeanspruchung) von Singletons diskutiert. Ich war früher in meiner Karriere auch einer dieser Menschen. Ich kann sehen, wo das Problem jetzt liegt, und dennoch gibt es immer noch viele Fälle, in denen ich keine nette 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 kürzlich 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 Platz in der App zu haben, der die Daten speichert und synchronisiert, und dann können alle neuen Bildschirme, die geöffnet werden, nur das meiste abfragen, was sie von dort benötigen, ohne wiederholt verschiedene unterstützende Daten vom Server anzufordern. Ständige Anfragen an den Server würden zu viel Bandbreite beanspruchen - und ich spreche von Tausenden von Dollar zusätzlichen Internetrechnungen pro Woche, was nicht akzeptabel war.

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

570
Bobby Tables

Es ist wichtig, hier zwischen einzelnen Instanzen und dem Singleton-Entwurfsmuster zu unterscheiden.

Einzelne Instanzen sind einfach Realität. Die meisten Apps funktionieren jeweils nur mit einer Konfiguration, einer Benutzeroberfläche, einem Dateisystem usw. Wenn viele Status oder Daten verwaltet werden müssen, möchten Sie sicherlich 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, insbesondere einer, der lautet:

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

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

  • Unfähigkeit, abstrakte oder Schnittstellenklassen zu verwenden;
  • Unfähigkeit zur Unterklasse;
  • Hohe Kopplung über die Anwendung (schwer zu modifizieren);
  • Schwer zu testen (kann in Unit-Tests nicht gefälscht/verspottet werden);
  • Im veränderlichen Zustand schwer zu parallelisieren (erfordert umfangreiche Verriegelung);
  • und so weiter.

Keines dieser Symptome ist in einzelnen 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 Platz in der App zu haben, der die Daten speichert und synchronisiert, und dann können alle neuen Bildschirme, die geöffnet werden, nur das meiste abfragen, was sie von dort benötigen, ohne wiederholt verschiedene unterstützende Daten vom Server anzufordern. Ständige Anfragen an den Server würden zu viel Bandbreite beanspruchen - und ich spreche von Tausenden von Dollar zusätzlichen Internetrechnungen pro Woche, was nicht akzeptabel 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 muss möglicherweise eine einzelne Instanz sein, wenn Sie vermeiden möchten, dass dieselben Daten für mehrere Cache-Instanzen abgerufen werden. das heißt aber nicht, dass man tatsächlich alles allen aussetzen muss .

Als erstes würde ich die verschiedenen Funktionsbereiche des Caches in separate Schnittstellen aufteilen. Angenommen, Sie haben den weltweit schlechtesten YouTube-Klon basierend auf 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 Zugriff haben muss - Medien, Benutzerprofile und statische Daten Seiten (wie die Titelseite). All das 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 übergeben dann einfach die Instanzen (die in einen bestimmten Schnittstellentyp umgewandelt wurden) über Konstruktoren und öffentliche Eigenschaften.

Dies nennt man übrigens Dependency Injection ; Sie müssen weder Spring noch einen speziellen IoC-Container verwenden, solange Ihr allgemeines Klassendesign seine Abhängigkeiten vom Aufrufer akzeptiert, anstatt zu instanziieren sie allein oder referenzieren den globalen Zustand .

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

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

  2. Wenn Sie feststellen, dass Microsoft Access nicht die beste Wahl für ein Daten-Backend war, können Sie es durch etwas Besseres ersetzen - beispielsweise SQL Server.

  3. Wenn Sie feststellen, dass SQL Server nicht speziell für Medien die beste Wahl ist, können Sie Ihre Implementierung aufteilen, ohne einen anderen Teil des zu beeinflussen System. 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 seine eigene Lebensdauerverwaltung durch und ermöglicht es Ihnen, einen bestimmten Dienst als einzelne Instanz zu definieren (häufig als "Singleton" bezeichnet) das ist nur zur Vertrautheit). Grundsätzlich ersparen Ihnen diese Frameworks den größten Teil der 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 (wie Sie sind), sollten Sie tatsächlich 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 Cache -Instanz nicht einmal auflösen müssen, wenn Sie sich für eine Umgestaltung entscheiden. Sie können die Speicherung von Medien ändern, indem Sie einfach eine alternative Implementierung von IMediaRepository eingeben. Wenn Sie darüber nachdenken, wie dies zusammenpasst, werden Sie feststellen, dass immer noch nur eine physische Instanz eines Caches erstellt wird, sodass Sie nie zweimal dieselben Daten abrufen müssen.

Nichts davon bedeutet, dass jede einzelne Software auf der Welt nach diesen hohen Standards für hohe Kohäsion und lose Kopplung ausgelegt sein muss. Dies hängt von der Größe und dem Umfang des Projekts, Ihrem Team, Ihrem Budget, den Fristen usw. ab. Wenn Sie jedoch fragen, welches Design am besten geeignet ist (anstelle eines Singletons), dann ist dies das Richtige.

P.S. Wie andere gesagt haben, ist es wahrscheinlich nicht die beste Idee für die abhängigen Klassen, sich darüber im Klaren 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 zu dem aussehen, was oben abgebildet ist. Sie würden die einzelnen Schnittstellen einfach nicht als Caches bezeichnen. Stattdessen würden Sie sie Services oder ähnliches nennen.

826
Aaronaught

In dem Fall, den Sie geben, klingt es so, als ob die Verwendung eines Singleton nicht das Problem ist, 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 für die Bereitstellung der Daten geben, und die Implementierung dieser Abstraktion könnte Caching 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 sind 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, sodass es keinen klaren Grund gibt, die Abhängigkeiten einzufügen.

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

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 gefunden wird verwenden.

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 jedoch, wie andere Klassen, die es verwenden, es finden.

Bei einer statischen Singleton-Implementierung besteht die Konvention darin, sie einfach überall dort zu verwenden, wo sie benötigt wird. Aber das verbirgt die Abhängigkeit vollständig und verbindet die beiden Klassen eng miteinander.

Wenn wir die Abhängigkeit von der Klasse angeben , ist diese Abhängigkeit explizit und die konsumierende Klasse muss nur den Vertrag kennen, den sie verwenden kann.

48
quentin-starin

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

tl; dr:

Das Singleton-Muster "Gang of Four" bewirkt zwei Dinge: 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 ist alles, was Sie interessiert, die erste Hälfte davon, und das Karren entlang der zweiten Hälfte, um es zu erhalten, fügt unnötige Einschränkungen hinzu.

Darüber hinaus gibt es bessere Lösungen für einen bequemen Zugriff. Ein Objekt global zu machen, ist die nukleare Option, um dies zu lösen, und macht es viel einfacher, Ihre Kapselung zu zerstören. Alles, was an Globals schlecht ist, gilt vollständig für Singletons.

Wenn Sie es nur verwenden, weil Sie viele Stellen im Code haben, die dasselbe Objekt berühren müssen, versuchen Sie, einen besseren Weg zu finden, um es nur diesen Objekten zu geben, ohne es auszusetzen die gesamte Codebasis. Andere Lösungen:

  • Lassen Sie es ganz hinter sich. Ich habe viele Singleton-Klassen gesehen, die keinen Status haben und nur Taschen mit 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 Math Klasse benötigen, wenn Sie nur 123.Abs() ausführen könnten.

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

  • Fügen Sie es in die Basisklasse ein. Wenn Sie viele Klassen haben, die alle Zugriff auf ein bestimmtes Objekt benötigen und eine Basisklasse gemeinsam nutzen, können Sie dies tun Machen Sie dieses Objekt zu einem Mitglied auf der Basis. Wenn Sie es erstellen, übergeben Sie das Objekt. Jetzt können alle abgeleiteten Objekte es bekommen, wenn sie es brauchen. Wenn Sie es schützen, stellen Sie sicher, dass das Objekt weiterhin gekapselt bleibt.

45
munificent

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

Wirklich, Sie müssen sich nur Sorgen machen um global mutable state. Der konstante Zustand wird nicht durch Nebenwirkungen beeinflusst und ist daher weniger problematisch.

Das Hauptanliegen von Singleton ist, dass es die Kopplung hinzufügt und so Dinge wie das Testen schwierig macht. Sie können die Kopplung reduzieren, 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 Fabrik gekoppelt sind (aber zumindest die Fabrik kann alternative Implementierungen für verschiedene Phasen haben)).

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

Ein weiterer großer Nachteil von Singletons ist, dass das Entfernen und Ersetzen durch Singletons aus dem Code zu einer wirklich schwierigen Aufgabe wird (es gibt 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.
21
Martin York

Dann was? Da hat es niemand gesagt: Toolbox. Das ist, wenn Sie globale Variablen wollen.

Singleton Missbrauch 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 ziemlich logisch, dass die Anwendung diese Verantwortung übernimmt, da die Anwendung diese Art von 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 verantwortlich ist oder es dem Startmechanismus der Anwendung ermöglicht, sie zu konfigurieren ...

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 dort die Verwirrung.

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

Aus meiner Erfahrung heraus löste das einfache Ersetzen von static für Singleton viele Probleme in einem mittelgroßen Patchwork-Taschenprojekt, an dem ich arbeite. Das bedeutet nur, dass es für schlecht gestaltete Projekte eine gewisse Verwendung hat. Ich denke, es gibt zu viel Diskussion wenn das Singleton-Musternützlich ist oder nicht und ich kann nicht wirklich argumentieren, ob es tatsächlich schlecht) ist . Aber es gibt immer noch gute Argumente für Singleton gegenüber statischen Methoden im Allgemeinen .

Ich bin mir sicher, dass Singletons nur schlecht sind, wenn wir sie verwenden und dabei gute Praktiken ignorieren. Das ist in der Tat nicht so einfach zu handhaben. 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, genau wie globale Vars , Singletons sollten still = jederzeit vermieden werden . Vor allem, weil sie übermäßig missbraucht werden. Globale Vars können jedoch nicht immer vermieden werden, 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 jeder seine Anwendung ...

Andere Alternativen

  • Der bester Artikel, den ich gerade über Singletons gelesen habe schlägt Service Locator als Alternative vor. Für mich ist das im Grunde eine " Static Toolbox ", wenn Sie so wollen. 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 das Problem des Singletons durchzusetzen, ist seine Verwendung und nicht das Muster an sich.

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

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

Jetzt zu implizieren, dass Singletons um jeden Preis vermieden werden sollten, ist einfach falsch ...

  • Aaronaught 's Antwort schlägt vor, aus einer Reihe von Gründen niemals Singletons zu verwenden . Aber sie sind alle Gründe dafür, wie es missbraucht und missbraucht wird, 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 was nun? Es ist nicht dafür gedacht. Es gibt keine Unfähigkeit zur Schnittstelle, soweit ich kann sagen . Eine hohe Kopplung kann ebenfalls vorhanden sein, aber das liegt nur daran, wie sie häufig verwendet wird. Es muss nicht . Tatsächlich hat die Kopplung an sich nichts mit dem Singleton-Muster zu tun. Wenn dies geklärt ist, ist auch die Schwierigkeit des Testens bereits beseitigt. Die Schwierigkeit der Parallelisierung hängt von der Sprache und der Plattform ab, was wiederum kein Problem für das Muster darstellt.

Praktische Beispiele

Ich sehe oft, dass 2 verwendet werden, sowohl für als auch gegen Singletons. Web-Cache (mein Fall) und Protokolldienst .

Die Protokollierung einige werden argumentieren ist ein perfektes Singleton-Beispiel, weil, und ich zitiere:

  • Die Anforderer benötigen ein bekanntes Objekt, an das sie Anforderungen an das Protokoll senden können. Dies bedeutet einen globalen Zugangspunkt.
  • Da der Protokollierungsdienst eine einzelne Ereignisquelle ist, bei der sich mehrere Listener registrieren können, muss nur eine Instanz vorhanden sein.
  • Obwohl sich verschiedene Anwendungen möglicherweise bei verschiedenen Ausgabegeräten anmelden, ist die Art und Weise, wie sie ihre Listener registrieren, immer dieselbe. Alle Anpassungen erfolgen über die Listener. Clients können die Protokollierung anfordern, ohne zu wissen, wie oder wo der Text protokolliert wird. Jede Anwendung würde daher den Protokollierungsdienst genauso verwenden.
  • Jede Anwendung sollte nur mit einer Instanz des Protokollierungsdienstes davonkommen 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, dass es schwierig ist, den Protokolldienst zu erweitern, sobald Sie schließlich 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 hängt von architektonischen Entscheidungen und der Gewichtung ab, ob Refactoring ein tragfähiges Risiko darstellt. Dies ist ein weiteres Problem, wenn Refactoring normalerweise die letzte erforderliche Korrekturmaßnahme ist.

20
cregox

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 eingefügt wird und die Komponente den konkreten Typ des Managers nicht kennt, nur eine Schnittstelle oder abstrakte Basisklasse, die der Manager implementiert, dann eine Einheit test kann alternative Implementierungen des Managers beim Testen von Abhängigkeiten bereitstellen.

Wenn Sie zum Konfigurieren und Instanziieren der Komponenten, aus denen Ihre Anwendung besteht, IOC Container) verwenden, können Sie Ihren IOC Container) einfach konfigurieren, um nur eine Instanz des zu erstellen "Manager", mit dem Sie dasselbe erreichen können, nur eine Instanz, die den globalen Anwendungscache steuert.

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

5
Pete

Ein Singleton ist nicht grundlegend schlecht in dem Sinne, dass alles, was mit Design-Computing zu tun hat, 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 spezielle Berechtigungen erforderlich sind oder mehrere Verbindungstypen durchlaufen werden. Allein aus diesem Grund ist es wahrscheinlich sehr sinnvoll, diese Verbindung in einem Singleton zu organisieren.

Sie müssen jedoch auch sicherstellen, dass der Singleton wirklich ein Singleton und keine globale Variable ist. Dies ist wichtig, wenn die einzelne, eindeutige Datenbank tatsächlich 4 Datenbanken umfasst, jeweils eine für Produktions-, Staging-, Entwicklungs- und Testvorrichtungen. Ein Datenbank-Singleton ermittelt, mit welcher dieser Verbindungen eine Verbindung hergestellt werden soll, greift auf die einzelne Instanz für diese Datenbank zu, verbindet sie bei Bedarf und gibt sie an den Aufrufer zurück.

Wenn ein Singleton nicht wirklich ein Singleton ist (dies ist der Zeitpunkt, an dem sich die meisten Programmierer aufregen), ist es ein träge instanziierter Global, es gibt keine Möglichkeit, eine korrekte Instanz einzufügen.

Ein weiteres nützliches Merkmal eines gut gestalteten Singleton-Musters ist, dass es oft nicht beobachtbar ist. Der Anrufer fragt nach einer Verbindung. Der Dienst, der es bereitstellt, kann ein gepooltes Objekt zurückgeben. Wenn er einen Test durchführt, kann er für jeden Aufrufer ein neues erstellen oder stattdessen ein Scheinobjekt bereitstellen.

Die Verwendung des Singleton-Musters, das tatsächliche Objekte darstellt, ist durchaus 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 UIApplication dargestellt. Es gibt nur eine Anwendung, die Sie sind. Daher ist es angebracht, diese mit einem Singleton darzustellen.

Die Verwendung eines Singletons als Datenmanagerklasse ist in Ordnung, solange es richtig entworfen wurde. Wenn es sich um einen Eimer mit Dateneigenschaften handelt, ist dies nicht besser als der globale Bereich. Wenn es sich um eine Reihe von Gettern und Setzern handelt, ist das besser, aber immer noch nicht großartig. Wenn es sich um eine Klasse handelt, die wirklich die gesamte Schnittstelle zu Daten verwaltet, einschließlich möglicherweise das Abrufen von Remote-Daten, das Zwischenspeichern, das Einrichten und das Herunterfahren ... Das könnte sehr nützlich sein.

3
Dan Ray

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

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

Es hängt davon ab, wie Sie über ein Programm denken. Wenn Sie sich ein Programm eher als eine Gesellschaft von Diensten als als zufällig gebundene zwischengespeicherte Instanzen vorstellen, 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. Sie können 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 Modell einfügen. Es kann also sehr flexibel sein.

Der Schlüssel befindet sich intern in einem Programm, wenn ich ihn ausführe und ein wenig Funktionalität benötige. Ich kann mit voller Sicherheit darauf zugreifen, dass der Dienst aktiv und einsatzbereit ist. Dies ist der Schlüssel, wenn in einem Prozess verschiedene Threads beginnen, die eine Zustandsmaschine durchlaufen müssen, um als bereit zu gelten.

Normalerweise würde ich eine XxxService Klasse umschließen, die einen Singleton um die Klasse Xxx umschließt. Der Singleton ist überhaupt nicht in der Klasse Xxx, sondern in eine andere Klasse, XxxService, unterteilt. Dies liegt daran, dass Xxx mehrere Instanzen haben kann, obwohl dies nicht wahrscheinlich ist, aber wir möchten immer noch eine Xxx Instanz, auf die global auf jedem System zugegriffen werden kann. XxxService bietet eine schöne Trennung von Bedenken. Xxx muss keine Singleton-Richtlinie erzwingen, wir können jedoch Xxx als Singleton verwenden, wenn dies erforderlich ist.

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  
3
Todd Hoff

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).

Strukturieren Sie Ihre App also wie einen Baum, in dem die Dinge darüber alles besitzen, was darunter verwendet wird. Implementieren Sie keine App wie ein Netz, in der jeder jeden kennt und sich durch globale Methoden findet.

1

IMO, dein Beispiel klingt okay. Ich würde vorschlagen, Folgendes herauszufiltern: 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 den Code hinein und aus ihm heraus zu tauschen. Außerdem bietet es eine einfache Expansionsroute.

Grafik:

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

DB-Accessor und Cache können von demselben Objekt oder Ententyp so erben, dass sie wie dasselbe Objekt aussehen. Solange Sie Plug/Compile/Test können und es noch funktioniert.

Dadurch werden die Dinge entkoppelt, sodass Sie neue Caches hinzufügen können, ohne ein Uber-Cache-Objekt ändern zu müssen. YMMV. IANAL. USW.

1
Paul Nathan

Erste Frage, finden Sie viele Fehler in der Anwendung? Vielleicht vergessen Sie, den Cache oder den fehlerhaften Cache zu aktualisieren, oder finden Sie es schwierig, ihn zu ändern? (Ich erinnere mich, dass eine App die Größe nur ändern würde, wenn Sie auch die Farbe geändert haben. 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 ENTFERNEN. Ok das ist nicht notwendig, aber ich empfehle es. Wirklich, Sie initialisieren 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 können, da sie nicht mehr global sind. Alle meine Manager-Klassen sind reguläre Klassen, behandeln Sie es einfach so.

1
user2528

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 geht nur darum, wie Sie es verwenden.

1
Evgeni