Costruttori Java vs metodi di fabbrica statici

1. Panoramica

I costruttori Java sono il meccanismo predefinito per ottenere istanze di classe completamente inizializzate. Dopo tutto, forniscono tutta l'infrastruttura richiesta per l'inserimento delle dipendenze, manualmente o automaticamente.

Anche così, in alcuni casi d'uso specifici, è preferibile ricorrere a metodi di fabbrica statici per ottenere lo stesso risultato.

In questo tutorial, evidenzieremo i pro ei contro dell'utilizzo di metodi factory statici rispetto ai semplici vecchi costruttori Java .

2. Vantaggi dei metodi di fabbrica statici rispetto ai costruttori

In un linguaggio orientato agli oggetti come Java, cosa potrebbe esserci di sbagliato nei costruttori? Nel complesso, niente. Anche così, l'Effective Java Item 1 del famoso Joshua Block afferma chiaramente:

"Considera metodi factory statici anziché costruttori"

Anche se questo non è un proiettile d'argento, ecco i motivi più convincenti che sostengono questo approccio:

  1. I costruttori non hanno nomi significativi , quindi sono sempre limitati alla convenzione di denominazione standard imposta dalla lingua. I metodi di fabbrica statica possono avere nomi significativi , quindi esprimere esplicitamente ciò che fanno
  2. I metodi di fabbrica statici possono restituire lo stesso tipo che implementa i metodi, un sottotipo e anche le primitive , quindi offrono una gamma più flessibile di tipi di restituzione
  3. I metodi di fabbrica statica possono incapsulare tutta la logica richiesta per la precostruzione di istanze completamente inizializzate , quindi possono essere utilizzati per spostare questa logica aggiuntiva fuori dai costruttori. Ciò impedisce ai costruttori di eseguire ulteriori attività, oltre alla semplice inizializzazione dei campi
  4. I metodi di fabbrica statici possono essere metodi con istanze controllate , con il pattern Singleton che è l'esempio più lampante di questa funzionalità

3. Metodi di fabbrica statici nel JDK

Ci sono molti esempi di metodi di fabbrica statici nel JDK che mostrano molti dei vantaggi sopra descritti. Esploriamo alcuni di loro.

3.1. La classe String

A causa del noto interning di String , è molto improbabile che utilizzeremo il costruttore della classe String per creare un nuovo oggetto String . Anche così, questo è perfettamente legale:

String value = new String("Baeldung");

In questo caso, il costruttore creerà un nuovo oggetto String , che è il comportamento previsto.

In alternativa, se vogliamo creare un nuovo oggetto String utilizzando un metodo factory statico , possiamo utilizzare alcune delle seguenti implementazioni del metodo valueOf () :

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

Esistono diverse implementazioni sovraccaricate di valueOf () . Ciascuno restituirà un nuovo oggetto String , a seconda del tipo di argomento passato al metodo (ad es. Int , long , boolean , char e così via).

Il nome esprime abbastanza chiaramente ciò che fa il metodo. Si attiene inoltre a uno standard consolidato nell'ecosistema Java per la denominazione dei metodi di fabbrica statici.

3.2. La classe opzionale

Un altro ottimo esempio di metodi factory statici nel JDK è la classe Optional . Questa classe implementa alcuni metodi factory con nomi piuttosto significativi , inclusi empty () , of () e ofNullable () :

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

3.3. La classe delle collezioni

Probabilmente l'esempio più rappresentativo di metodi factory statici nel JDK è la classe Collections . Questa è una classe non istanziabile che implementa solo metodi statici.

Molti di questi sono metodi di fabbrica che restituiscono anche raccolte, dopo aver applicato alla raccolta fornita un certo tipo di algoritmo.

Di seguito sono riportati alcuni esempi tipici dei metodi factory della classe:

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

Il numero di metodi factory statici nel JDK è molto ampio, quindi manterremo breve l'elenco degli esempi per brevità.

Tuttavia, gli esempi precedenti dovrebbero darci un'idea chiara di quanto siano onnipresenti i metodi factory statici in Java.

4. Metodi di fabbrica statici personalizzati

Naturalmente, possiamo implementare i nostri metodi di fabbrica statici. Ma quando vale davvero la pena farlo, invece di creare istanze di classe tramite semplici costruttori?

Vediamo un semplice esempio.

Consideriamo questa classe utente ingenua :

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 questo caso, non ci sono avvisi visibili per indicare che un metodo factory statico potrebbe essere migliore del costruttore standard.

E se vogliamo che tutte le istanze utente ottengano un valore predefinito per il campo del paese ?

Se inizializziamo il campo con un valore predefinito, dovremmo rifattorizzare anche il costruttore, rendendo così il design più rigido.

Possiamo invece usare un metodo factory statico:

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

Ecco come ottenere un'istanza utente con un valore predefinito assegnato al campo del paese :

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

5. Spostare la logica dai costruttori

La nostra classe Utente potrebbe rapidamente trasformarsi in un design imperfetto se decidessimo di implementare funzionalità che richiederebbero l'aggiunta di ulteriore logica al costruttore (i campanelli d'allarme dovrebbero suonare a quest'ora).

Supponiamo di voler fornire alla classe la capacità di registrare l'ora in cui viene creato ogni oggetto User .

Se mettessimo solo questa logica nel costruttore, violeremmo il principio di responsabilità unica . Finiremmo con un costruttore monolitico che fa molto di più che inizializzare i campi.

Possiamo mantenere il nostro design pulito con un metodo di fabbrica statico:

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

Ecco come creeremmo la nostra istanza utente migliorata :

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

6. Istanza controllata dall'istanza

Come mostrato sopra, possiamo incapsulare blocchi di logica in metodi factory statici prima di restituire oggetti User completamente inizializzati . E possiamo farlo senza inquinare il costruttore con la responsabilità di eseguire molteplici attività non correlate.

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.

Come al solito, tutti gli esempi di codice mostrati in questo articolo sono disponibili su GitHub.