Als nächstes Entwurfsmuster stelle ich das Singleton
oder das Einzelstück
vor.
Der Beispielcode kann in dem GitHub Repository seism0saurus/design-pattern-singelton eingesehen werden. Ich habe ein Package für den Quellcode ohne das Entwurfsmuster und ein Package für den Code mit dem Entwurfsmuster erstellt. Beide enthalten eine Example Klasse, deren Main Methode ausgeführt werden kann.
Zweck
Das Erzeugungsmuster stellt sicher, dass es von einer Klasse nur eine Instanz geben kann.
Um dieses Ziel zu erreichen, wird der Konstruktor versteckt und eine Methode verfügbar gemacht,
über die auf die Instanz der Klasse zugegriffen werden kann. Diese Methode heißt meistens getInstance()
.
Beispiel Datenbankverbindung
Es gibt ein paar Szenarien, in denen das Einzelstück erfolgreich angewendet wird. Eines davon ist JDBC. JDBC steht für Java Database Connectivity und übernimmt unter anderem den Aufbau einer Verbindung von JVM zur Datenbank. Diesen Verbindungsaufbau habe ich hier exemplarisch herausgepickt.
Eine Verbindung zur Datenbank aufzubauen und sich zu authentifizieren nimmt etwas Zeit in Anspruch. Daher macht es Sinn, diese Verbindung nur einmal aufzubauen, sobald sie benötigt wird und diese dann an allen Stellen des Programmes zu verwenden.
Vorher oder Antipattern
In einer rudimentären Implementierung habe ich einfach jedes Mal, wenn ich eine Query ausführe, eine neue DatabaseConnection instanziiert.
Die DatabaseConnection Klasse simuliert im Konstruktor das langsame Aufbauen einer Verbindung. Außerdem verfügt sie über eine Methode zum Ausführen von Queries.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class DatabaseConnection {
public DatabaseConnection(){
System.out.println("Connecting to database...");
//simulate slow connection initialization
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Connection established");
}
public void executeStatement(String statement){
System.out.println("Executing "+statement);
}
}
In der Beispielklasse wird einfach mehrmals eine Verbindung instantiiert und eine Query ausgeführt. Damit wir das Ergebnis mit dem der Singleton Lösung vergleichen können, wird die Zeit gestoppt.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Example {
public static void main(String[] args) {
long start = new Date().getTime();
DatabaseConnection mariadb = new DatabaseConnection();
mariadb.executeStatement("SELECT * FROM USERS");
mariadb = new DatabaseConnection();
mariadb.executeStatement("SELECT * FROM PRODUCTS");
mariadb = new DatabaseConnection();
mariadb.executeStatement("SELECT * FROM ADDRESSES");
mariadb = new DatabaseConnection();
mariadb.executeStatement("SELECT * FROM STOCK");
long end = new Date().getTime();
System.out.println("Duration: "+ (end-start) + " ms");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Connecting to database...
Connection established
Executing SELECT * FROM USERS
Connecting to database...
Connection established
Executing SELECT * FROM PRODUCTS
Connecting to database...
Connection established
Executing SELECT * FROM ADDRESSES
Connecting to database...
Connection established
Executing SELECT * FROM STOCK
Duration: 12009 ms
Durch die häufigen Verbindungen braucht das Programm über 12 Sekunden.
Nachher oder das Einzelstück Muster im Beispiel
Betrachten wir nun das DatabaseConnectionSingleton. Der beiden wichtigen Schritte sind:
- Den Konstruktor auf
private
setzen, sodass er von außen nicht mehr aufgerufen werden kann. - Eine
getInstance()
Methode schreiben, die die einzige Instanz der Klasse öffentlich zugänglich macht.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class DatabaseConnectionSingleton {
private static volatile DatabaseConnectionSingleton instance;
private DatabaseConnectionSingleton(){
System.out.println("Connecting to databse...");
//simulate slow connection initialization
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Connection established");
}
public static DatabaseConnectionSingleton getInstance(){
if (instance == null) {
synchronized(DatabaseConnectionSingleton.class) {
if (instance == null)
instance = new DatabaseConnectionSingleton();
}
}
return instance;
}
public void executeStatement(String statement){
System.out.println("Executing "+statement);
}
}
Der Aufbau der getInstance()
Methode ist ein double-checked locking oder eine doppelt überprüfte Sperrung.
Ohne synchonized
könnten bei mehreren Threads mehrere Singletons erzeugt werden, was wir ja verhindern wollen.
Würden wir die ganze Methode synchronized
schreiben, hätte es bei häufigem Aufruf Einfluss auf die Performance.
Tatsächlich ist auch das volatile
vor der instance
wichtig. Ohne dieses könnte es passieren, dass der Compiler
oder der Prozessor zur Laufzeit die Ausführungsreihenfolge anpasst und die Synchronisation zwischen den Threads nicht richtig funktioniert.
Details hierzu findest du in dem Artikel The “Double-Checked Locking is Broken” Declaration.
Alternativ zu der doppelt überprüften Sperre könnte man ein threadsicheres Singleton auch über einen Initialization on Demand Holder lösen.
Unser Singleton ist nun vor versehentlicher Mehrfachinstantiierunge geschützt. Was wir aber nicht verhindern, ist die Umgehung des Singleton Prinzips mittels Reflection, Cloning sowie Serialization und Deserialization. Es würde das Beispiel zu sehr aufblähen. Aber es ist gut, sich diese Stichwörter in Kombination mit dem Singleton zu merken.
Die Beispielklasse muss nun noch so angepasst werden, dass sie die getInstance()
Methode aufruft, statt den nicht mehr sichtbaren Konstruktor.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Example {
public static void main(String[] args) {
long start = new Date().getTime();
DatabaseConnectionSingleton mariadb = DatabaseConnectionSingleton.getInstance();
mariadb.executeStatement("SELECT * FROM USERS");
mariadb = DatabaseConnectionSingleton.getInstance();
mariadb.executeStatement("SELECT * FROM PRODUCTS");
mariadb = DatabaseConnectionSingleton.getInstance();
mariadb.executeStatement("SELECT * FROM ADDRESSES");
mariadb = DatabaseConnectionSingleton.getInstance();
mariadb.executeStatement("SELECT * FROM STOCK");
long end = new Date().getTime();
System.out.println("Duration: "+ (end-start) + " ms");
}
}
1
2
3
4
5
6
7
8
Connecting to databse...
Connection established
Executing SELECT * FROM USERS
Executing SELECT * FROM PRODUCTS
Executing SELECT * FROM ADDRESSES
Executing SELECT * FROM STOCK
Duration: 3008 ms
Wie wir an der Ausgabe in der Konsole sehen können, wird nur noch einmal eine Verbindung aufgebaut. Die anderen Codestellen nutzen die gleiche Instanz. Als schöner Nebeneffekt ist die Ausführungsgeschwindigkeit auf 3 Sekunden gesunken.
Vorteile
Es ist möglich eine Zugriffskontrolle zu implementieren und die Erzeugung des Objekts zu kontrollieren. Außerdem kann die Instanz zu dem Zeitpunkt erzeugt werden, zu dem sie benötigt wird.
Nachteile
Es besteht die Gefahr, dass ein Singleton zu einen Gottobjekt wird, wenn zu viel Verantwortlichkeiten in einer globalen Klasse gesammelt werden. Nach dem Single-Responsibility-Prinzip gilt dies als Antipattern.
Das Entwurfsmuster stellt außerdem eine Einschränkung dar und wird häufig eingesetzt, obwohl es gar nicht unbedingt nötig wäre.
Zusammenfassung
Das Einzelstück ermöglicht es, die Instantiierung und die Anzahl eines Objektes genau zu kontrollieren.
Im nächsten Artikel stelle ich euch dann das Builder Pattern
bzw. den Erbauer
vor. Ein Entwurfsmuster, dass ich sehr gerne verwende.
Bis bald,
seism0saurus