Constructores de Java frente a métodos de fábrica estáticos

1. Información general

Los constructores de Java son el mecanismo predeterminado para obtener instancias de clase completamente inicializadas. Después de todo, proporcionan toda la infraestructura necesaria para inyectar dependencias, ya sea de forma manual o automática.

Aun así, en algunos casos de uso específicos, es preferible recurrir a métodos de fábrica estáticos para lograr el mismo resultado.

En este tutorial, destacaremos los pros y los contras de usar métodos de fábrica estáticos frente a los antiguos constructores de Java .

2. Ventajas de los métodos de fábrica estáticos sobre los constructores

En un lenguaje orientado a objetos como Java, ¿qué podría estar mal con los constructores? En general, nada. Aun así, el artículo 1 de Java efectivo del famoso Joshua Block establece claramente:

"Considere métodos de fábrica estáticos en lugar de constructores"

Si bien esta no es una solución milagrosa, aquí están las razones más convincentes que sustentan este enfoque:

  1. Los constructores no tienen nombres significativos , por lo que siempre están restringidos a la convención de nomenclatura estándar impuesta por el lenguaje. Los métodos de fábrica estáticos pueden tener nombres significativos , por lo que transmiten explícitamente lo que hacen
  2. Los métodos de fábrica estáticos pueden devolver el mismo tipo que implementa el método o métodos, un subtipo y también primitivas , por lo que ofrecen un rango más flexible de tipos de retorno.
  3. Los métodos de fábrica estática pueden encapsular toda la lógica necesaria para preconstruir instancias totalmente inicializadas , por lo que se pueden usar para sacar esta lógica adicional de los constructores. Esto evita que los constructores realicen más tareas, además de inicializar campos.
  4. Los métodos de fábrica estáticos pueden ser métodos controlados de instancia , siendo el patrón Singleton el ejemplo más evidente de esta característica.

3. Métodos estáticos de fábrica en el JDK

Hay muchos ejemplos de métodos de fábrica estáticos en el JDK que muestran muchas de las ventajas descritas anteriormente. Exploremos algunos de ellos.

3.1. La clase String

Debido a la conocida práctica de String , es muy poco probable que usemos el constructor de la clase String para crear un nuevo objeto String . Aun así, esto es perfectamente legal:

String value = new String("Baeldung");

En este caso, el constructor creará un nuevo objeto String , que es el comportamiento esperado.

Alternativamente, si queremos crear un nuevo objeto String usando un método de fábrica estático , podemos usar algunas de las siguientes implementaciones del método valueOf () :

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

Hay varias implementaciones sobrecargadas de valueOf () . Cada uno devolverá un nuevo objeto String , dependiendo del tipo de argumento pasado al método (por ejemplo , int , long , boolean , char, etc.).

El nombre expresa con bastante claridad lo que hace el método. También se adhiere a un estándar bien establecido en el ecosistema Java para nombrar métodos de fábrica estáticos.

3.2. La clase opcional

Otro buen ejemplo de métodos de fábrica estáticos en el JDK es la clase Optional . Esta clase implementa algunos métodos de fábrica con nombres bastante significativos , incluidos empty () , of () y ofNullable () :

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

3.3. La clase de colecciones

Posiblemente el ejemplo más representativo de métodos de fábrica estáticos en el JDK es la clase Colecciones . Esta es una clase no instanciable que implementa solo métodos estáticos.

Muchos de estos son métodos de fábrica que también devuelven colecciones, después de aplicar a la colección proporcionada algún tipo de algoritmo.

A continuación, se muestran algunos ejemplos típicos de los métodos de fábrica de la clase:

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

El número de métodos de fábrica estáticos en el JDK es realmente extenso, por lo que mantendremos la lista de ejemplos breve en aras de la brevedad.

Sin embargo, los ejemplos anteriores deberían darnos una idea clara de cuán ubicuos son los métodos de fábrica estáticos en Java.

4. Métodos de fábrica estáticos personalizados

Por supuesto, podemos implementar nuestros propios métodos de fábrica estáticos. Pero, ¿cuándo realmente vale la pena hacerlo, en lugar de crear instancias de clase a través de constructores simples?

Veamos un ejemplo sencillo.

Consideremos esta clase de usuario 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 }

En este caso, no hay advertencias visibles que indiquen que un método de fábrica estático podría ser mejor que el constructor estándar.

¿Qué pasa si queremos que todas las instancias de usuario obtengan un valor predeterminado para el campo de país ?

Si inicializamos el campo con un valor predeterminado, también tendríamos que refactorizar el constructor, haciendo que el diseño sea más rígido.

En su lugar, podemos usar un método de fábrica estático:

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

Así es como obtendríamos una instancia de usuario con un valor predeterminado asignado al campo de país :

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

5. Sacar lógica de constructores

Nuestra clase de usuario podría descomponerse rápidamente en un diseño defectuoso si decidimos implementar características que requerirían agregar más lógica al constructor (las alarmas deberían estar sonando en este momento).

Supongamos que queremos proporcionar a la clase la capacidad de registrar el momento en el que se crea cada objeto Usuario .

Si ponemos esta lógica en el constructor, estaríamos rompiendo el principio de responsabilidad única . Terminaríamos con un constructor monolítico que hace mucho más que inicializar campos.

Podemos mantener nuestro diseño limpio con un método de fábrica estático:

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

Así es como crearíamos nuestra instancia de usuario mejorada :

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

6. Creación de instancias controlada por instancia

Como se muestra arriba, podemos encapsular fragmentos de lógica en métodos de fábrica estáticos antes de devolver objetos de usuario completamente inicializados . Y podemos hacer esto sin contaminar al constructor con la responsabilidad de realizar múltiples tareas no relacionadas.

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.

Como de costumbre, todos los ejemplos de código que se muestran en este artículo están disponibles en GitHub.