Sunday, September 2, 2012

¿Cómo crear objetos?


La pregunta que da título a esta entrada, parece tener una respuesta trivial, y en efecto la tiene. No existe otra manera de crear objetos en Java u otros lenguajes similares que no sea a través del operador new del lenguaje. Pero la pregunta se hace interesante si nos detenemos a pensar en qué lugar del código debemos escribir esa creación de objetos, de suerte que un usuario de nuestras clases pueda usar objetos de ella.
En estos días estoy platicando sobre la creación de objetos en uno de los cursos que imparto y me resultó interesante compartir esas pláticas aquí. Siempre habrá alguien que se interese por estas cosas.
Java y otros lenguajes de OOP definen "constructores" para la creación de objetos, haciendo uso del mencionado operador y adoptando una sintaxis para ese fin. Todos sabemos que el constructor es una función ( que se note que no digo método) que lleva el mismo nombre de la clase, que no indica su tipo de retorno ni tan siquiera void (aunque por supuesto "retorna" un objeto creado. El Constructor, como cualquier otra función o método tiene en su firma la lista de parámetros de cualquier tipo, que normalmente recibe aorgumentos relacionados con el estado inicial que queremos para el objeto.
Java permite cosas como lo siguiente:
class Foo {
  int foo;
  int bar;
  Foo (int foo, int bar) {
    this.foo = foo;
    this.bar = bar;
  }
  void Foo(int foo,int bar) {
    this.foo = foo;
    this.bar = bar ;
  }
}
que imagino que nadie escribe (llamar a un método con el nombre de la clase), a no ser que sea para auto confundirse, confundir a los demás (peor aun) o para explicar que es algo que permite el lenguaje como es en este caso.
Java permite sobrecargar el constructor, de manera que podamos tener diferentes versiones para crear los objetos. Algunos lenguajes actuales incluyen la técnica de parámetros con nombres (named parameters) y se dan el lujo de no permitir la sobrecarga de constructores, pero eso es otra historia.
Cuando sobrecargamos a los constructores perseguimos el propósito de indicar a los usuarios de nuestra clase "diferentes" variantes de los objetos que ofrecemos. Por ejemplo, quien escribió la clase BigInteger en el core de Java decidió escribir el siguiente constructor para la clase BigInteger:
BigInteger(int bitLength, Random rnd)
para crear un positivo BigInteger "probablemente primo". Esa aseveración se expresa en la documentación o descripción del API de esa clase.
Dicho constructor establece que probablemente generará un número primo. ¿Cómo saber eso? Pues no hay otra forma que leyendo su documentación. ¿Podría lograrse algo más claro en la sintaxis de ese constructor para indicar que es muy probable que el BigInteger generado sea primo?.
Hay varias respuestas a esa pregunta. Una que quizá se le ocurra a alguien es crear una clase ProbablePrime que extienda a BigInteger y que el constructor de esa clase cree un objeto de clase BigInteger con el constructor anterior. Pero hay otra forma más sencilla y es a la que me quiero referir aquí y que muchos también conocen y es la de darle a la clase BigInteger un método estático de fábrica y bautizar a ese método con un nombre más nemotécnico.
¿Qué tal si esa clase ofreciera un método estático como el siguiente?

static BigInteger probablePrime(int bitLength, Random rnd)
¿Cuál línea del código siguiente sería más "limpia" si lo que quiero es generar un número primo?
BigInteger big1 = new BigInteger(10new Random());
BigInteger big2 = BigInteger.probablePrime(10new Random());
Por supuesto que la segunda línea expresa con claridad la intención del programador y Joshua Bloh en su libro "Effective Java" incluye ese precepto en uno de sus items: "Consider static factory methods instead of constructorswhen creating objects"
Esa NO es la única ventaja del uso de métodos estáticos de fábrica. Hay otras que el autor describe, como por supuesto hay algunas desventajas.
Al usar métodos estáticos de fábrica en lugar de constructores, no estamos obligados a crear un objeto brand new cada vez que invocamos el método. Es una ventaja que aprovechan las clases inmutables que permiten el uso de objetos compartidos. Por ejemplo, las clases wrappers de tipos primitivos usan esa técnica para compartir sus objetos inmutables. Sigue el código de uno de los métodos estáticos de fábrica de la clasejava.lang.Integer para encapsular un tipo primitivo.
static Integer valueOf(int i) {
  assert IntegerCache.high >=127;
  if( i >= IntegerCache.low && i <= IntegerCache.high)
    return IntegerCache.cache[ i + ( -IntegerCache.low) ];
  return new Integer(i);
}
Otra ventaja importante del uso de los métodos estáticos de fábrica es que facilitan el diseño de marcos de trabajo basados en interfaces al permitir que un método estático de fábrica retorne un objeto de clase derivada, permitiendo entonces que el marco de trabajo no tenga que hacer publica las implementaciones de sus interfaces y poder cambiar a voluntad esas implementaciones sin afectar el código del cliente. Claro que para lograr ésto, la clase, además de métodos estáticos de fábrica, tiene que incluir constructores públicos o protegidos para permitir que de ella se extienda. El uso entonces de los constructores o de los métodos estáticos de fábrica depende de la pericia y experiencia del programador.
El uso de métodos estáticos de fábrica también se aprovecha para diseñar clases no instanciables por el cliente (clases con constructores privados) que devuelven implementaciones propias del marco de trabajo. Es el caso por ejemplo de la clase java.util.Collectionsque incluye métodos estáticos de fábrica para obtener colecciones. Por ejemplo, el método estático de fábrica siguiente:
public static<T>  Set<T> singleton(T o)
{
  return new SingletonSet<>(o);
}
La clase SingletonSet "no es conocida por el cliente". Es una clase anidada privada de la clase java.util.Collections.
Cuando se usan métodos estáticos de fábrica, es necesario una buena selección de sus nombres. Una de las desventajas de esta técnica es que precisamente las firmas de los métodos estáticos de fábrica no se distinguen de otros métodos estáticos, algo que no sucede con los constructores. El bautizo de métodos es relevante. Robert Martin le dedica un capítulo a ese tópico en su libor "Celan Code"
Otras cosas interesantes con respecto a la creación de objetos
Hay otras cosas interesantes para resolver de manera más elegante algunos pequeños problemas cuando tenemos la necesidad de escribir una clase que requiere de muchos parámetros en su constructor y algunos de esos parámetros siempre se requieren y otros pueden tener valores opcionales. Al resolverlo por la vía "normal" se presenta el efecto "telescopio" en los constructores y existen algunas técnicas para escribir con mayor limpieza, pero ya este post se alargó demasiado y lo dejo para el siguiente.

2 comments:

Agustín Ramos said...

Una gran desventaja al usar métodos estáticos en Java, sean de fábrica o no, es que dificultan la creación de pruebas unitarias, ya que no es posible cambiar la implementación del proveedor del método estático mediante stubs o mocks y de esta manera, cualquier dependencia hacia un método estático, no se puede aislar.

Por otro lado, no veo el beneficio que los métodos estáticos de fábrica aportan a los marcos de trabajo; por el contrario, al ser métodos estáticos dificultan el cambio de implementaciones de la fábrica. De hecho según recuerdo originalmente Spring no tenía soporte para creación de objetos a partir de métodos estáticos.

Creo que en general es más flexible y "limpio" mantener fábricas en clases separadas de las clases de los objetos que se crean; sin embargo, dependiendo del contexto, en particular de la flexibilidad/extensibilidad esperada, lo más práctico puede sí puede crear un método de fábrica estático.

Saludos

B. J. Ferro said...

Son dos cosas diferentes: lo que aquí se discute es cuando un cliente "crea" un objeto de manera directa usando la clase que quiere instanciar, bien a través de sus constructores o a través de métodos estáticos de fábrica directamente encapsulados en la clase.
El otro contexto es el de un marco de trabajo como Spring que incorpora su propia fábrica de objetos en su core container y en este caso el cliente solicita a esa fábrica los objetos que necesita.