Constructeurs Java vs méthodes d'usine statiques

1. Vue d'ensemble

Les constructeurs Java sont le mécanisme par défaut pour obtenir des instances de classe entièrement initialisées. Après tout, ils fournissent toute l'infrastructure requise pour l'injection de dépendances, manuellement ou automatiquement.

Même ainsi, dans quelques cas d'utilisation spécifiques, il est préférable de recourir à des méthodes d'usine statiques pour obtenir le même résultat.

Dans ce didacticiel, nous allons mettre en évidence les avantages et les inconvénients de l'utilisation de méthodes de fabrique statiques par rapport aux anciens constructeurs Java .

2. Avantages des méthodes statiques d'usine par rapport aux constructeurs

Dans un langage orienté objet comme Java, qu'est-ce qui ne va pas avec les constructeurs? Dans l'ensemble, rien. Même ainsi, l'élément 1 Java efficace du célèbre Joshua Block indique clairement:

"Considérez les méthodes de fabrique statiques au lieu des constructeurs"

Bien que ce ne soit pas une solution miracle, voici les raisons les plus convaincantes qui soutiennent cette approche:

  1. Les constructeurs n'ont pas de noms significatifs , ils sont donc toujours limités à la convention de dénomination standard imposée par le langage. Les méthodes de fabrique statiques peuvent avoir des noms significatifs , transmettant ainsi explicitement ce qu'elles font
  2. Les méthodes de fabrique statiques peuvent retourner le même type qui implémente la ou les méthodes, un sous-type et également des primitives , de sorte qu'elles offrent une gamme plus flexible de types de retour
  3. Les méthodes de fabrique statique peuvent encapsuler toute la logique requise pour la préconstruction d'instances entièrement initialisées , afin qu'elles puissent être utilisées pour déplacer cette logique supplémentaire hors des constructeurs. Cela empêche les constructeurs d'effectuer d'autres tâches, autres que la simple initialisation des champs
  4. Les méthodes de fabrique statiques peuvent être des méthodes à instanciation contrôlée , le modèle Singleton étant l'exemple le plus flagrant de cette fonctionnalité

3. Méthodes de fabrique statique dans le JDK

Il existe de nombreux exemples de méthodes d'usine statiques dans le JDK qui présentent de nombreux avantages décrits ci-dessus. Explorons certains d'entre eux.

3.1. La classe String

En raison de l' internement de String bien connu , il est très peu probable que nous utilisions le constructeur de classe String pour créer un nouvel objet String . Même ainsi, c'est parfaitement légal:

String value = new String("Baeldung");

Dans ce cas, le constructeur créera un nouvel objet String , qui est le comportement attendu.

Sinon, si nous voulons créer un nouvel objet String à l' aide d'une méthode de fabrique statique , nous pouvons utiliser certaines des implémentations suivantes de la méthode valueOf () :

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

Il existe plusieurs implémentations surchargées de valueOf () . Chacun retournera un nouvel objet String , en fonction du type d'argument passé à la méthode (par exemple int , long , boolean , char, etc.).

Le nom exprime assez clairement ce que fait la méthode. Il s'en tient également à une norme bien établie dans l'écosystème Java pour nommer les méthodes d'usine statiques.

3.2. La classe facultative

Un autre exemple intéressant de méthodes de fabrique statiques dans le JDK est la classe Optional . Cette classe implémente quelques méthodes d'usine avec des noms assez significatifs , y compris empty () , of () et ofNullable () :

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

3.3. La classe Collections

L' exemple le plus représentatif de méthodes de fabrique statiques dans le JDK est probablement la classe Collections . Il s'agit d'une classe non instanciable qui implémente uniquement des méthodes statiques.

Beaucoup d'entre elles sont des méthodes d'usine qui retournent également des collections, après avoir appliqué à la collection fournie un type d'algorithme.

Voici quelques exemples typiques des méthodes de fabrique de la classe:

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

Le nombre de méthodes de fabrique statiques dans le JDK est vraiment important, nous allons donc garder la liste d'exemples courte par souci de concision.

Néanmoins, les exemples ci-dessus devraient nous donner une idée claire de l'omniprésence des méthodes de fabrique statique en Java.

4. Méthodes d'usine statique personnalisées

Bien sûr, nous pouvons implémenter nos propres méthodes d'usine statiques. Mais quand cela vaut-il vraiment la peine de le faire, au lieu de créer des instances de classe via des constructeurs simples?

Voyons un exemple simple.

Considérons cette classe utilisateur naïve :

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 }

Dans ce cas, il n'y a aucun avertissement visible pour indiquer qu'une méthode de fabrique statique pourrait être meilleure que le constructeur standard.

Que faire si nous voulons que toutes les instances User obtiennent une valeur par défaut pour le champ country ?

Si nous initialisons le champ avec une valeur par défaut, nous devrons également refactoriser le constructeur, rendant ainsi la conception plus rigide.

Nous pouvons utiliser une méthode de fabrique statique à la place:

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

Voici comment nous obtiendrions une instance User avec une valeur par défaut attribuée au champ country :

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

5. Déplacer la logique hors des constructeurs

Notre classe User pourrait rapidement se transformer en une conception défectueuse si nous décidons d'implémenter des fonctionnalités qui nécessiteraient d'ajouter plus de logique au constructeur (les sonnettes d'alarme devraient sonner à ce moment-là).

Supposons que nous souhaitons fournir à la classe la possibilité de consigner l'heure à laquelle chaque objet User est créé.

Si nous mettons simplement cette logique dans le constructeur, nous briserions le principe de responsabilité unique . On se retrouverait avec un constructeur monolithique qui fait bien plus qu'initialiser des champs.

Nous pouvons garder notre conception propre avec une méthode d'usine statique:

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); } } 

Voici comment nous créerions notre instance utilisateur améliorée :

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

6. Instanciation contrôlée par instance

Comme indiqué ci-dessus, nous pouvons encapsuler des morceaux de logique dans des méthodes de fabrique statiques avant de renvoyer des objets User entièrement initialisés . Et nous pouvons le faire sans polluer le constructeur avec la responsabilité d'effectuer de multiples tâches indépendantes.

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.

Comme d'habitude, tous les exemples de code présentés dans cet article sont disponibles à l'adresse over sur GitHub.