Java-Konstruktoren gegen statische Factory-Methoden

1. Übersicht

Java-Konstruktoren sind der Standardmechanismus zum Abrufen vollständig initialisierter Klasseninstanzen. Schließlich stellen sie die gesamte Infrastruktur bereit, die zum manuellen oder automatischen Einfügen von Abhängigkeiten erforderlich ist.

Trotzdem ist es in einigen speziellen Anwendungsfällen vorzuziehen, auf statische Factory-Methoden zurückzugreifen, um das gleiche Ergebnis zu erzielen.

In diesem Tutorial werden die Vor- und Nachteile der Verwendung statischer Factory-Methoden im Vergleich zu einfachen alten Java-Konstruktoren hervorgehoben .

2. Vorteile statischer Fabrikmethoden gegenüber Konstruktoren

Was könnte in einer objektorientierten Sprache wie Java mit Konstruktoren falsch sein? Insgesamt nichts. Trotzdem heißt es in dem effektiven Java-Punkt 1 des berühmten Joshua Block eindeutig:

"Betrachten Sie statische Factory-Methoden anstelle von Konstruktoren"

Dies ist zwar keine Silberkugel, aber hier sind die überzeugendsten Gründe, die diesen Ansatz unterstützen:

  1. Konstruktoren haben keine aussagekräftigen Namen , daher sind sie immer auf die von der Sprache auferlegte Standardbenennungskonvention beschränkt. Statische Factory-Methoden können aussagekräftige Namen haben und somit explizit vermitteln, was sie tun
  2. Statische Factory-Methoden können denselben Typ zurückgeben, der die Methode (n), einen Subtyp und auch Grundelemente implementiert , sodass sie einen flexibleren Bereich von Rückgabetypen bieten
  3. Statische Factory-Methoden können die gesamte Logik kapseln, die zum Vorkonstruieren vollständig initialisierter Instanzen erforderlich ist , sodass diese zusätzliche Logik aus Konstruktoren entfernt werden kann. Dies verhindert, dass Konstruktoren weitere Aufgaben ausführen, als nur Felder zu initialisieren
  4. Statische Factory-Methoden können Methoden mit kontrollierter Instanz sein , wobei das Singleton-Muster das eklatanteste Beispiel für diese Funktion ist

3. Statische Factory-Methoden im JDK

Es gibt viele Beispiele für statische Factory-Methoden im JDK, die viele der oben beschriebenen Vorteile aufzeigen. Lassen Sie uns einige von ihnen erkunden.

3.1. Die String- Klasse

Aufgrund der bekannten String- Internierung ist es sehr unwahrscheinlich, dass wir den String- Klassenkonstruktor verwenden, um ein neues String- Objekt zu erstellen . Trotzdem ist dies völlig legal:

String value = new String("Baeldung");

In diesem Fall erstellt der Konstruktor ein neues String- Objekt. Dies entspricht dem erwarteten Verhalten.

Wenn Sie alternativ ein neues String- Objekt mit einer statischen Factory-Methode erstellen möchten , können Sie einige der folgenden Implementierungen der valueOf () -Methode verwenden:

String value1 = String.valueOf(1); String value2 = String.valueOf(1.0L); String value3 = String.valueOf(true); String value4 = String.valueOf('a'); 

Es gibt mehrere überladene Implementierungen von valueOf () . Jeder gibt ein neues String- Objekt zurück, abhängig vom Typ des an die Methode übergebenen Arguments (z. B. int , long , boolean , char usw.).

Der Name drückt ziemlich deutlich aus, was die Methode tut. Es hält sich auch an einen im Java-Ökosystem etablierten Standard für die Benennung statischer Factory-Methoden.

3.2. Die optionale Klasse

Ein weiteres gutes Beispiel für statische Factory-Methoden im JDK ist die Option- Klasse. Diese Klasse implementiert einige Factory-Methoden mit ziemlich aussagekräftigen Namen , einschließlich empty () , of () und ofNullable () :

Optional value1 = Optional.empty(); Optional value2 = Optional.of("Baeldung"); Optional value3 = Optional.ofNullable(null);

3.3. Die Kollektionen Klasse

Durchaus möglich das repräsentativste Beispiel für statische Factory - Methoden in der JDK ist die Kollektionen Klasse. Dies ist eine nicht instanziierbare Klasse, die nur statische Methoden implementiert.

Viele davon sind Factory-Methoden, die auch Sammlungen zurückgeben, nachdem auf die bereitgestellte Sammlung eine Art Algorithmus angewendet wurde.

Hier sind einige typische Beispiele für die Factory-Methoden der Klasse:

Collection syncedCollection = Collections.synchronizedCollection(originalCollection); Set syncedSet = Collections.synchronizedSet(new HashSet()); List unmodifiableList = Collections.unmodifiableList(originalList); Map unmodifiableMap = Collections.unmodifiableMap(originalMap); 

Die Anzahl der statischen Factory-Methoden im JDK ist sehr umfangreich, daher werden wir die Liste der Beispiele der Kürze halber kurz halten.

Die obigen Beispiele sollten uns jedoch eine klare Vorstellung davon geben, wie allgegenwärtig statische Factory-Methoden in Java sind.

4. Benutzerdefinierte statische Factory-Methoden

Natürlich können wir unsere eigenen statischen Factory-Methoden implementieren. Aber wann lohnt es sich wirklich, dies zu tun, anstatt Klasseninstanzen über einfache Konstruktoren zu erstellen?

Sehen wir uns ein einfaches Beispiel an.

Betrachten wir diese naive Benutzerklasse :

public class User { private final String name; private final String email; private final String country; public User(String name, String email, String country) { this.name = name; this.email = email; this.country = country; } // standard getters / toString }

In diesem Fall gibt es keine sichtbaren Warnungen, die darauf hinweisen, dass eine statische Factory-Methode besser sein könnte als der Standardkonstruktor.

Was passiert , wenn wir , dass alle wollen , dass die Benutzerinstanzen einen Standardwert für das bekommen Land Feld?

Wenn wir das Feld mit einem Standardwert initialisieren, müssten wir auch den Konstruktor umgestalten, wodurch das Design starrer wird.

Wir können stattdessen eine statische Factory-Methode verwenden:

public static User createWithDefaultCountry(String name, String email) { return new User(name, email, "Argentina"); }

So erhalten Sie eine Benutzerinstanz mit einem Standardwert, der dem Feld " Land" zugewiesen ist:

User user = User.createWithDefaultCountry("John", "[email protected]");

5. Verschieben der Logik aus Konstruktoren

Unsere Benutzerklasse könnte schnell zu einem fehlerhaften Design werden, wenn wir uns für die Implementierung von Funktionen entscheiden, die das Hinzufügen weiterer Logik zum Konstruktor erfordern würden (Alarmglocken sollten zu diesem Zeitpunkt ertönen).

Angenommen, wir möchten der Klasse die Möglichkeit geben, den Zeitpunkt zu protokollieren, zu dem jedes Benutzerobjekt erstellt wird.

If we just put this logic into the constructor, we'd be breaking the Single Responsibility Principle. We would end up with a monolithic constructor that does a lot more than initialize fields.

We can keep our design clean with a static factory method:

public class User { private static final Logger LOGGER = Logger.getLogger(User.class.getName()); private final String name; private final String email; private final String country; // standard constructors / getters public static User createWithLoggedInstantiationTime( String name, String email, String country) { LOGGER.log(Level.INFO, "Creating User instance at : {0}", LocalTime.now()); return new User(name, email, country); } } 

Here's how we'd create our improved User instance:

User user = User.createWithLoggedInstantiationTime("John", "[email protected]", "Argentina");

6. Instance-Controlled Instantiation

As shown above, we can encapsulate chunks of logic into static factory methods before returning fully-initialized User objects. And we can do this without polluting the constructor with the responsibility of performing multiple, unrelated tasks.

For instance, suppose we want to make our User class a Singleton. We can achieve this by implementing an instance-controlled static factory method:

public class User { private static volatile User instance = null; // other fields / standard constructors / getters public static User getSingletonInstance(String name, String email, String country) { if (instance == null) { synchronized (User.class) { if (instance == null) { instance = new User(name, email, country); } } } return instance; } } 

The implementation of the getSingletonInstance() method is thread-safe, with a small performance penalty, due to the synchronized block.

In this case, we used lazy initialization to demonstrate the implementation of an instance-controlled static factory method.

It's worth mentioning, however, that the best way to implement a Singleton is with a Java enum type, as it's both serialization-safe and thread-safe. For the full details on how to implement Singletons using different approaches, please check this article.

As expected, getting a User object with this method looks very similar to the previous examples:

User user = User.getSingletonInstance("John", "[email protected]", "Argentina");

7. Conclusion

In this article, we explored a few use cases where static factory methods can be a better alternative to using plain Java constructors.

Moreover, this refactoring pattern is so tightly rooted to a typical workflow that most IDEs will do it for us.

Of course, Apache NetBeans, IntelliJ IDEA, and Eclipse will perform the refactoring in slightly different ways, so please make sure first to check your IDE documentation.

As with many other refactoring patterns, we should use static factory methods with due caution, and only when it's worth the trade-off between producing more flexible and clean designs and the cost of having to implement additional methods.

Wie üblich sind alle in diesem Artikel gezeigten Codebeispiele auf GitHub verfügbar.