Esta semana me he dado cuenta de la diferencia existente entre un constructor feo y uno bonito. Es curioso lo fácil que es hacerlo bien y lo poquito que cuesta, así que voy a compartir mis apreciaciones con vosotros, a ver qué opináis.
Durante mi carrera profesional he hecho muchas cosas feas, así que no me echéis en cara si encontráis que he hecho algo diferente de lo que diga :D ¡Esto consiste en ir mejorando día a día!
Actualización 2012/01/24: Por petición de _YeBeNeS_, añado ejemplos en java.
Lanzando excepciones
Tener un constructor que lanza excepciones es algo que ya resulta algo feo. Un constructor debería preparar el objeto para ser utilizado, no realizar una tarea. Dado que no realiza tareas, no debería lanzar ninguna excepción.
Cuando digo que no lanza excepciones no significa que las capture: digo que no las lanza porque no lo necesita. Las actividades que haga serán tan simples que no pueden fallar: inicializaciones y punto. Nada de operaciones. Las cosas complejas las dejamos para los métodos.
No hay nada más feo que un constructor que lanza excepciones. Es lo último que te esperas y es lo último que deseas que otros se encuentren.
Cuando el constructor tiene excepciones, éste sería el código mínimo para instanciarlo:
public class Example {
public Example() throws exception {
}
}
// [...]
try {
Example example = new Example();
} catch (Exception e) {
}
Null pointer exception
Si un constructor no realiza operaciones, no puede darse el caso de acceder a un puntero inválido. El constructor sólo debería hacer asignaciones, nada más.
Constructores con parámetros
Hay ocasiones en las que un objeto requiere de un parámetro para poder funcionar. En estos casos, está bien requerirlo en el constructor.
Cuando el número de parámetros es muy alto (es decir, DOS), entonces deberíamos plantearnos hacerlo de otra manera. Si el constructor requiere tantos parámetros, es posible que esté violando el principio de única responsabilidad.
Otra costumbre que tomé y que he visto es la de crear un constructor con N parámetros, de manera que permita inicializar cada uno de sus atributos privados. La experiencia me ha demostrado que es una costumbre bastante mala... en casi todos los casos.
En el caso de Java, es una mala costumbre lo mires como lo mires. ¿Cuál es el orden de los parámetros? Si tengo 3 parámetros... ¿Tengo que hacer las 3 combinaciones de parámetros únicos, al menos 2 de 2 argumentos y otro de 3? ¿Y si, de pronto, necesito un cuarto atributo privado? ¿Cuántas combinaciones tengo que hacer?
En otros lenguajes, como Python, en el que los argumentos son nombrados, todo lo dicho no le afecta. Sin embargo, en este caso tendremos otro problema: ¿Y si ya no necesito uno de los argumentos o si necesito cambiarle el nombre?
A ver... ante el código siguiente:
public class Example {
private int value1;
private int value2;
private String juntaLaTrocola;
private String gamusino;
public Example() {}
public Example(int value1){}
public Example(int a, int b) {}
public Example(String juntaLaTrocola) {}
public Example(String gamusino, String juntaLaTrocola() {}
}
Se me ocurren muchas preguntas:
- En el tercer constructor, ¿va primero el value1 o el value2?
- ¿Por qué no puedo construir un objeto sólo con "gamusino"?
- ¿Por qué no puedo combinar valores numéricos y cadenas? ¿Es porque el programador se cansó (como en este caso)? ¿Es porque no debo? ¿Es porque se añadieron después?
Además, si heredas de la clase, ¡¡¡estás obligado a sobreescribir todos estos métodos!!!
¿Ejemplos? JDialog tiene 16 constructores distintos.
Constructores fatigados
No hace mucho que yo mismo implementé un constructor que parseaba un archivo XML. Me pareció algo horrible, pero no encontraba otra manera mejor de hacerlo. ¿Alguien le ve lógica a esto? Yo, ahora, no.
Si tenemos un constructor que hace tanto trabajo, ¿cómo podemos mejorar nuestro código? No podemos usar el patrón un método a una clase, porque tendremos que el constructor de la nueva clase hará, de nuevo, todo el trabajo. En caso contrario, ¿por qué tenemos dos clases? bastaría con sustituir la primera por la segunda.
No es algo tan raro de encontrar: la propia API de Java tiene métodos que lanzan excepciones o realizan mucho trabajo (NOTA: buscando ejemplos no he encontrado ninguno que lance excepciones, pero sé que los he visto; ¿habrá sido en clases de terceros?):
RMIConnectorServer(JMXServiceURL url, Map<String,?> environment, RMIServerImpl rmiServerImpl, MBeanServer mbeanServer); JDialog(Window owner, String title, Dialog.ModalityType modalityType, GraphicsConfiguration gc);
La solución: Fábricas
En ocasiones necesito dos constructores porque hay distintas maneras de usar el objeto. Veamos un ejemplo: Tengo una clase que se conecta a base de datos y que se puede utilizar de dos maneras diferentes: Con una base de datos real o con una base de datos en memoria.
Aquí tengo un problema. Uno de los constructores necesitará un argumento y el otro ninguno. No es lógico utilizar otro sistema para construir el objeto... ¿O sí?
Tras haberlo hecho así numerosas veces, he descubierto que hay otra manera mucho más chula: Utilizando un método que me fabrique el objeto. De esta manera puedo diferenciar entre la forma de construirlo de una manera y la otra. Es decir: tendré el método createInMemory y createInFile. Opcionalmente podré transformar el constructor en privado para evitar su uso de forma indebida (aunque no soy muy partidario de esto).
Con esta simple transformación consigo que el constructor no lance excepciones y, además, tengo una manera elegante de ir construyendo el objeto con más de una instrucción, mucho más verboso cuando tenga que cambiarlo.
Otra ventaja: Si necesito añadir nuevos "constructores", como por ejemplo una base de datos remota, bastará con ofrecer más fábricas: createRemote.
Un ejemplo:
public class DataAccess {
public DataAccess() {}
public static DataAccess createInMemory() throws SQLException{
DataAccess result = new DataAccess();
result.setConnectionString("in memory");
result.buildDatabase();
return result;
}
public static DataAccess createMySql() throws SQLException{
DataAccess result = new DataAccess();
result.setConnectionString("mysql");
if (!result.hasDatabase())
result.buildDatabase();
return result;
}
}
Fábricas abstractas
Considero de suma importancia hacer nuestro código de manera que pueda crecer, sin limitarlo desde su creación. El uso de estas fábricas permite que en el futuro puedan transformarse fácilmente en Fábricas Abstractas, dando mayor funcionalidad con unos cambios muy pequeños.
Pruebas
Cuando se utiliza un constructor complejo, la realización de pruebas se ve obstaculizada. En cambio, cuando el constructor es simple y lo que se complican son las operaciones, resulta más sencillo de probar, ya que puedes construir el objeto en el setUp con la completa seguridad de que no va a fallar. Sin embargo, cuando se utilizan constructores complejos, no hay manera de probarlo.
Si una operación (un método) lanza una excepción, la operación te está avisando de algo. Si es el constructor el que la lanza, no puedes estar seguro de qué operación se estaba realizando en ese momento. Esto dificulta terriblemente las pruebas y complica cualquier intento de mockear el objeto.
Conclusión
Ya que nuestras clases tienden a ser complejas, no ensuciemos el código desde el principio y tratemos de usar constructores sencillos. Cuando es el constructor el que hace el trabajo, ¿qué les queda a los métodos? Deleguemos este trabajo.
Muy interesante la entrada y no tengo nada que contradecir, lo único que añadir un par de ejemplos la hubiera completado :)
ResponderSuprimirUn saludo, Alberto.
Ya los tienes.
ResponderSuprimir¡Gracias por la petición!
Buenas! :)
ResponderSuprimirLos artículos técnicos que vienes escribiendo me están pareciendo geniales, sigue así!
Un apunte con respecto a este, el tema del constructor "privado". Ten en cuenta que si lo haces público a la misma vez que provees una factoría te estás encontrando con la misma toma de decisiones que cuándo haces más de uno, es decir, ¿qué sentido tiene poder instanciarlo si es una factoría? Y si lo tiene, ¿debe entonces proveerse por factoría? Al dejarlo privado estamos dejando clara la intención del programador a la hora de usar ese código, eso es lo que debe predominar, la intención y la falta de ambigüedad en el uso ;)
Abrazo!
Hola!!
SuprimirEstoy totalmente de acuerdo contigo. Sin embargo, no quise hacer esa apreciación a propósito.
Nunca se sabe cuándo te vas a encontrar en la necesidad de usarlo. Aunque en un principio todo pinta que va a ser poco útil dejar como público el constructor, es probable que exista una situación "especial".
Por ejemplo: Podríamos dar un constructor normal y una fábrica que devuelve su "configuración más habitual". En este caso sólo sería un "script" que te permite ahorrarte pasos cada vez que llamas, pero no quitas la flexibilidad de hacer lo que te dé la gana :D
Desde otro punto de vista, en el mismo ejemplo, si es la configuración más habitual, podrían establecerse con los valores por defecto (convención frente a configuración) y así nos ahorramos la fábrica, pero es sólo un ejemplo XD
De todas maneras, muy buena apreciación ;)
¡Un saludo!
Una observación. Cuando hablas de parsear un XML para la instanciación del objeto por parte del constructor... cierto que no tiene sentido, pero no es tan descabellado que lo haga una factoría. A fin de cuentas y de forma muy muy muy resumida es lo que hace Spring (entre otras muchas cosas) o frameworks similares.
ResponderSuprimirLa Inyección de Dependencias y la Inversión del Control favorecen mucho este tipo de simplicidad en el código dejando los constructores mucho más limpios y las clases más sencillas.
Genial por el artículo y el blog.
Como bien dices, debería hacerlo la fábrica (lo siento, "factoría" me resulta muy... "espaninglish"). Es justamente lo que propongo en el artículo y lo que, cuando lo necesité, no se me ocurrió.
ResponderSuprimirLa ID e IC, al favorecer la simplicidad del código, favorecen también que las pruebas resulten mucho más sencillas de realizar.
¡Gracias por el comentario!