Singleton Pattern

Tumbleweed

Captain
Registriert
März 2008
Beiträge
3.600
Dieses design pattern ist ja sehr verschrien, wenn man sich im Netz mal so umsieht. Ich selbst habe es schon ziemlich oft verwendet, vermutlich auch aus Gründen der Bequemlichkeit. Trotzdem muss ich sagen, obwohl ich weiß wie man es implementiert und was ich damit erreiche, fühle ich mich in einer Diskussion darüber eher schwach bewaffnet.

In den meisten Argumentationen, die man so findet, wird darauf hingewiesen, dass es einfach schlechtes Design ist, wenn man ein Singleton braucht. Das wird unterstützt mit Argumenten wie tight coupling der Klassen (loose coupling erstrebenswert), dann wird noch darauf hingewiesen, dass ein Singleton ja einen state hat (was anscheinend auch negativ ist) und letztlich wird noch auf die schlechte Testbarkeit hingewiesen. Ach ja und auf globale Variablen (denen Singletons wohl ähneln) wird ja auch herabgesehen.

Ich kann keines der Argumente so richtig erklären bzw. entkräften. Vielleicht kann ja jemand etwas gegen mein Halbwissen tun, sodass ich da endlich mal sicher argumentieren kann und es verstanden habe, was dafür bzw. dagegen spricht. Einfach nur in das gleiche Horn stoßen wie die Mehrheit, das liegt mir nicht. Wenn dann möchte ich schon wissen warum ich für oder gegen etwas bin. :D

Um das Thema vielleicht mal auf ein paar Eckpunkte zu reduzieren:

  • Warum sind globale Variablen schlecht?
  • Warum ist loose coupling erstrebenswert und was verbirgt sich eigentlich dahinter?
  • Ist Singleton in manchen Fällen vielleicht sogar alternativlos bzw. wären alle Alternativen einfach nur furchtbar umständlich im Vergleich? (DB connection pooling, thread pooling etc.)
  • Warum ist es negativ, dass ein Singleton einen state hat und was ist damit gemeint?
Ich habe absichtlich kein Sprach-Präfix für den Thread gewählt, da das Entwurfsmuster ja in vielen Sprachen funktioniert, allerdings geht es mir schon darum rein objektorientiert zu programmieren (vorzugsweise in Java, aber darum geht es eher weniger).
Ich habe übrigens schon einige blogs, Forendiskussionen und sonstige Artikel darüber gelesen und googlen kann ich auch. Daher brauche ich nicht wirklich eine Flut von links, die mich zu Seiten führen, auf denen auch wieder nur das übliche Getröte nach dem Motto "ich habe mal gehört Singleton ist böse, also lass es einfach" herrscht. ;) Nachdem ich neulich durch einen netten Link hier sogar gelernt habe, dass GOTO in manchen Fällen gar nicht so übel ist, wie es inzwischen verbreitet wird, bin ich noch vorsichtiger damit auf solche "band wagons" aufzuspringen. :rolleyes:
 
Globale Variablen:

sind schlecht, weil sie von überall aus verändert werden können und man so u.a. Informationen nach "außen" Preis gibt.


Lose Kopplung:

in dem Zusammenhang wird auch von Kohäsion gesprochen. Die Kohäsion beschreibt den Zusammenhalt von den Bestandteilen eines Moduls oder einer Komponente. Die Kohäsion sollte möglichst hoch sein. Denn jedes Modul soll eine Aufgabe erfüllen, diese aber komplett.

Hingegen möchte man eine lose Kopplung zwischen Modulen haben. Kopplung ist ein Maß für die Komplexität von Schnittstellen, also die Abhängigkeiten zwischen Modulen. Diese sollte möglichst gering sein. Dadurch kann man solche leichter austauschen. Außerdem ist das Modul leichter anzupassen, da Änderungen sich nicht auf andere Module auswirken. Und das Modul kann auch besser getestet werden, da man es isoliert betrachten kann.

Zum Singleton-Pattern:

hier findet man die Instanz an einer zentralen Stelle und kann von überall darauf zugreifen. Dies entspricht also einer globalen Variablen und verstößt somit gegen die Prinzipien der Objektorientierung.
 
Zuletzt bearbeitet:
- Warum globale Variablen schlecht sind: Zum einen kann jeder Code damit herumspielen - man kann sich also möglicherweise nicht darauf verlassen, daß die Variable gerade den "richtigen" Wert hat. Außerdem kann einer globalen Variable - genauso wie einem Singleton - kein Code (im Lauf des eigentlichen Programms) zugeordnet werden, der dafür verantwortlich ist. In Java ist das weniger tragisch, aber in C++ etwa handelt man sich so schnell Speicherlecks ein, da man dort Objekte selbst aufräumen muß, wenn man sie nicht mehr braucht. Bei einem Singleton ist aber nicht klar, wann es nicht mehr gebraucht wird und welcher Code es dann wegräumen soll.

- Loose coupling bedeutet, daß die Abhängigkeit von Klassen bzw. Programmkomponenten untereinander möglichst minimal sein soll. Dadurch erreicht man eine höhere Flexibilität und Robustheit (eine Klasse / Komponente kann intern verändert oder gegen eine andere ausgetauscht werden, ohne daß die anderen davon betroffen sind). Eine Klasse, die ein Singleton einsetzt, steht dagegen zu diesem in starker Abhängigkeit ("tight coupling"). Besser ist der Einsatz von Dependency Injection - im einfachsten Fall bedeutet dieser hochtrabende Ausdruck schlicht, daß ein Objekt seine Abhängigkeiten nicht selbst anfordert, sondern sie ihm von außen (etwa durch den Konstruktor) mitgegeben werden. Dann könnte z. B. eine DB-Connection sehr einfach durch eine andere - oder zum Testen durch einen Dummy - ersetzt werden. Mit Singletons ginge das nicht ohne weiteres, außer man ersetzt global das ganze Singleton.

- Mit State / Zustand ist gemeint, daß das Singleton interne Daten haben kann, die veränderlich sind und von denen das Ergebnis seiner Operationen mit abhängt (statt nur von der Eingabe). Im Normalfall ist das kein Problem, kann aber in dem Moment zu Komplikationen führen, wo parallel gearbeitet wird und mehrere Threads gleichzeitig auf das Singleton zugreifen.

Ich bin beim Programmieren allerdings recht pragmatisch - ich benutze immer das, was mir gerade am besten hilft, mein Vorhaben möglichst einfach (und sauber) umzusetzen. Wenn das Singletons sind, dann gerne auch die. Es kommt darauf an, ob mir ihre Nachteile in der konkreten Situation tatsächlich schaden.
 
hi,

alles sehr schön erklärt hier ;)

loose coupling: (ml ganz einfach :) )
Unterteilung in Daten und Schnittstellen, die Daten können nur über Schnittstellen verändert werden, warum, Kohärenz und Wiederverwertbarkeit

zum Singleton hab ich auch noch was:

mit Locks ist das auch kein Problem bei mehreren Threads,
wie aber NullPointer schon so schön gemeint hat:
ich benutze immer das, was mir gerade am besten hilft, mein Vorhaben möglichst einfach (und sauber) umzusetzen
wird dann halt oft sehr schnell sehr komplex.
kommt auf die Situation an...

also meine Profs haben keine Skrupel Singletons einzusetzen, und irgendwelche Panikmache das Singletons schlecht seien, naja, das liegt doch meistens nur an der Umsetzung und nicht am Singleton.

gruß
 
Singletons sind aber eigentlich auch ein Anti-Pattern [1, 2, 3] an der Stelle eines Singletons ist Dependency Injection so gut wie immer eine bessere Wahl.
Und warum sind sie schlecht? Die Testbarkeit von Code sinkt gegen Null, man kann Singleton-Objekte in UnitTests nicht mehr ausmocken: Unit Tests werden unbrauchbar


[1] http://jandiandme.blogspot.com/2006/07/vom-pattern-zum-anti-pattern.html
[2] http://agilemindtricks.blogspot.com/2007/11/singleton-anti-pattern.html
[3] http://blogs.msdn.com/b/scottdensmore/archive/2004/05/25/140827.aspx
 
Ich nehme einfach mal ein Beispiel, wo ich bisher immer ein Singleton eingesetzt habe und versuche die Kritikpunkte (so ich sie denn richtig verstanden habe) zu widerlegen.
Code:
package com.example.database;

import java.sql.Connection;
import java.sql.SQLException;

import com.jolbox.bonecp.BoneCP;
import com.jolbox.bonecp.BoneCPConfig;

public class MySqlConPool implements DBConnectionPool
{

    private static class LazyHolder
    {
        private static final MySqlConPool    INSTANCE    = new MySqlConPool();
    }
    
    private final BoneCP connectionPool;

    private MySqlConPool()
    {
        try
        {
            Class.forName("com.mysql.jdbc.Driver").newInstance();
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
        
        BoneCPConfig config = new BoneCPConfig();
        config.setJdbcUrl("jdbc:mysql://127.0.0.1/users");
        config.setUsername("user");
        config.setPassword("password");
        config.setMinConnectionsPerPartition(5);
        config.setMaxConnectionsPerPartition(10);
        config.setPartitionCount(1);

        BoneCP newPool = null;
        try
        {
            newPool = new BoneCP(config);
        }
        catch (SQLException e)
        {
            System.err.println("Failed to create db con pool");
            e.printStackTrace();
        }
        finally
        {
            connectionPool = newPool;
        }
    }
    
    public Connection getConnection()
    {
        if (connectionPool == null)
            return null;
        
        Connection con = null;
        try
        {
            con = connectionPool.getConnection();
        }
        catch (SQLException e)
        {
            e.printStackTrace();
        }
        
        return con;
    }
    
    public void shutdownPool()
    {
        connectionPool.shutdown();
    }

    public static final DBConnectionPool getInstance()
    {
        return LazyHolder.INSTANCE;
    }
}
Ich habe hier keinerlei public fields, um die ich mich sorgen müsste. Daher fällt das Argument der bösen globals schon mal flach.
Abgesehen davon habe ich durch Verwendung des Initialization on Demand Holder nicht die Gefahr mehrerer Instanzen durch konkurrierende Erzeugung.
Da ich ein Interface implementiere kann ich in den erzeugenden Klassen einfach ein DBConnectionPool field halten dessen Instanz austauschbar ist, für den Fall, dass ich plötzlich einen anderen Pool (Postgre,MSSQL,...) verwende und dabei vielleicht auch andere pool settings verwenden möchte. Erfüllt das schon die Kriterien für ein loose coupling?

Das sind alles Behauptungen, die ich einfach mal aufgestellt habe, um eventuell Denkfehler meinerseits aufdecken zu können. Was mir diese Implementierung erlaubt, ist die simple Erzeugung einer DBConnectionPool Instanz in allen Klassen, in denen ich DB Zugriff benötige und das ohne, dass ich mich darum kümmern muss, dass ich den connection pool vor allen anderen Klassen erzeuge, um ihn dann auf umständlichem Wege per Konstruktor an alle Klassen weiterzugeben, die mal DB Zugriff brauchen werden.

Quasi:
Code:
    private final DBConnectionPool dbConPool;
    
    public SomeConstructor()
    {
        dbConPool = MySqlConPool.getInstance();
    }
Das einzige, was jetzt aus meiner Sicht noch kritisch wäre, wäre ein Scheitern bei der Erzeugung des pools, also wenn es im Konstruktor des Singletons knallt.
 
Tumbleweed schrieb:
Da ich ein Interface implementiere kann ich in den erzeugenden Klassen einfach ein DBConnectionPool field halten dessen Instanz austauschbar ist, für den Fall, dass ich plötzlich einen anderen Pool (Postgre,MSSQL,...) verwende und dabei vielleicht auch andere pool settings verwenden möchte. Erfüllt das schon die Kriterien für ein loose coupling?


Code:
    private final DBConnectionPool dbConPool;
    
    public SomeConstructor()
    {
        dbConPool = MySqlConPool.getInstance();
    }
Grundsätzlich: Lose Kopplung im Sinne von "vollständig Entkoppelt" ist praktisch nicht mit vertretbarem Aufwand erreichbar. Ich würde eher sagen, dass etwas loser oder eben weniger lose gekoppelt ist.
Soll heißen: "Lose Kopplung" ist relativ und von den konkreten Anforderungen abhängig. Je loser Anwendungskomponenten gekoppelt sind desto komplexer wird die Anwendung. Man muss also immer die richtige Balance finden.

Was du machst ist zwar loser gekoppelt als wenn du kein Interface verwendet hättest allerdings ist wäre mir deine Implementierung noch nicht lose genug gekoppelt:

Angenommen du stellst auf eine Oracle-Datenbank um. Du hast allerdings den "MySqlConPool" bereits in z. B. 20 Klassen verwendet. Jetzt musst du jede Verwendung ändern um einen anderen Connection-Pool zu verwenden. Hättest du den Connection-Pool z. B. im Konstruktor übergeben und einen Dependecy-Injection-Container verwendet müsstest du nur eine Zeile Code ändern und hast nicht die Gefahr Referenzen zu übersehen.

Ein weiter Punkt ist Testbarkeit: Du möchtest eine Klasse testen die einen Connection-Pool verwendet. Da deine Unit-Test aber nicht auf die Datenbank zugreifen dürfen, musst du den Connection-Pool durch einen Mock ersetzen. Den Code deiner Klasse darfst du aber nicht ändern, da das ja dem Sinn eines Tests widersprechen würde.
 
Ich merke schon, dependency injection und testing sollte ich mir auf die TODO-Liste schreiben. Von DI weiß ich fast nichts und testing habe ich bisher in der Regel vernachlässigt. :rolleyes:
 
Singletons sind auch in manchen Umgebungen wie einem Tomcat-Server ein großes Problem. Dort wird das Singelton dann nicht für jede Session erstellt sondern eben nur einmal pro laufendem Server... dann teilen sich quasi die verschiedenen Sessions ein Singleton, was normalerweise nicht im Sinne des Erfinders ist...
 
Neben den Sachen die hier genannt wurden, hätte ich da noch:

Locality of Reference sinkt gewaltig
http://de.wikipedia.org/wiki/Lokalitätseigenschaft
Kann einem optimierenden Compiler das Leben zur Hölle machen, resultiert häufiger in cache misses, etc...

Außerdem, ein über Threads gesharetes Singleton ist auch übel. Wird mit Locking zugegriffen, läuft man Gefahr, dass das Programm mit jedem weiteren Thread langsamer wird. Definitiv ein großer Hemmer für die Skalierbarkeit. Zumindest ein "Singleton" pro Thread als Thread-Local-Storage sollte dann drin sein.
Im Kontext Threads ist ein Singleton auch immer ein Kandidat für False-Sharing:
http://en.wikipedia.org/wiki/False_sharing
 
Die hier vorgebrachten Argumente gegen Singletons kann ich nur teilweise nachvollziehen. Dass Singleton gleichbedeutend ist mit globaler Variable ist aber genauso Unsinn wie Argumente über Lose Kopplung und Kohäsion.

Lose Kopplung und Kohäsion beziehen sich auf Module, und nicht auf einzelne Klassen. Ist ein Singleton Teil eines Moduls und wird dort nur intern verwendet fällt die Argumentation flach.

Dass ein Singleton gleichbedeutend mit einer globalen Variable ist stimmt so auch nicht. Es _kann_ gleichbedeutend sein. Allerdings ist es genauso gut möglich über Fassadenklassen einen Getter für eine Singletonklasse zu implementieren -> somit ist es keine globale Variable mehr (keine öffentliche Sichtbarkeit).

Der weitere Vorteil dieser Vorgehensweise ist, dass eine Instanz der Singletonklasse nur dann erstellt wird, wenn sie wirklich gebraucht wird. Große Vorteile ergeben sich dadurch v.a. bei Skriptsprachen, die bei jedem Aufruf komplett neu initialisieren.

Das Argument mit den Lokalitätseigenschaften und den Cache-Misses kann ich auch nicht nachvollziehen. Singletons sind Klassen wie jede andere Klasse auch. Ihre privaten Daten befinden sich räumlich lokal auf einem zusammenhängenden Speicherbereich. Die Referenz auf den Singleton kann sich im Falle einer Fassadenklasse auch innerhalb eines solchen Speicherbereichs befinden. Für einen "optimierenden Compiler" ist lose Kopplung als solches bereits ungünstig, weil es viele Optimierungen bereits ausschließt.

Dass ein Singleton die Skalierbarkeit hemmt stimmt auch nur teilweise und hängt von der Art des Singletons ab. Im Übrigen impliziert ein Singleton üblicherweise eine einzigartige Resource, die im Zweifel von mehreren Threads/Prozessen geshared wird. Aus dem Grund entscheidet man sich auch für ein Singleton -> wenn eine Resource nicht "klonbar" ist.

Das Argument über "False Sharing" betrifft Multiprocessing im Allgemeinen. Werden Resourcen von mehreren Threads gleichzeitig verwendet, so ergibt sich das automatisch. Aber es ist nirgends gesagt, dass innerhalb einer Cache Line (nur 32-64 Bytes Größe) sich gleichzeitig Daten befinden müssen, die regelmäßig von einem oder mehreren Threads beschrieben werden. Dieses Problem ist seeehr hypothetisch. Den direkten Zusammenhang zu Singletons kann ich hier nicht erkennen.
 
Zurück
Oben