¿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
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:
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:
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?
¿Cuál línea del código siguiente sería más "limpia" si lo que quiero es generar un número primo?
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 clase
para encapsular un tipo primitivo.
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
que incluye métodos estáticos de fábrica para obtener colecciones. Por ejemplo, el método estático de fábrica siguiente:
La clase SingletonSet "no es conocida por el cliente". Es una clase anidada privada de la clase
.
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 "Clean 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.
- bferro's blog
- Inicie sesión o regístrese para enviar comentarios
¿Cómo crear objetos?
No es Celan Code, es Clean Code
Creacion de objeto depende del estado
Una tecnica a evaluar si debemos crear objetos estáticos es la necesidad de resguardar un estado por cada instancia o por (en caso de java) por cada maquia virtual. Otra seria incluso preguntarnos si requerimos guardar estado. Por ejemplo
en la operacion no necesitamos guardar ningun estado ya que es una operación "básica" que seria inutil hacer algo como:
para hacer la conversion del numero seria:
Aqui viene una pregunta: ¿Para que querriamos almacenar el numero en la instancia del objeto?, quizas seria util si en el contructor mandaramos
y tuvieramos metodos como
para saber los decimales que no entraron en el numero entero. Quizas tambien necesitemos metodos para redondear el numero. La cosa es que en este caso no solo se requiere realizar operacion, sino obtener mas datos como resultado de la operacion. Eso, creo yo que es un criterio importante como para definir si debemos usar
o no
"Buildea" un objeto, no lo "construyas"
En el post anterior (¿Cómo crear objetos?) nos referimos al uso de métodos estáticos de fábrica para complementar la creación de objetos por esa vía, en lugar de usar los constructores de la clase. Veamos ahora otros problemas que surgen con la creación de objetos con constructores y cómo pueden solucionarse.
Supongamos que en un sistema de control escolar, modelamos el concepto estudiante con la clase siguiente:
Se nos pueden ocurrir otros atributos, o que algunos de los atributos que hemos añadido pueden encapsularse en otras clases, pero el objetivo no es discutir sobre el buen diseño de los atributos. Lo que queremos destacar, es que de ese conjunto de atributos, algunos será siempre requeridos y otros serán opcionales, y que esa consideración debe tomarse en cuenta para el diseño de los constructores de la clase.
Supongamos (pienso que es una buena suposición) que los valores de los atributos id, nombre, fechaDeNacimiento, carrera y semestre siempre se requieren para crear un objeto de clase Estudiante y que el resto de los atributos pueden tomar valores opcionales. Noten que también he establecido que los atributos id, nombre y fechaDeNacimiento son constantes, por lo que estaremos obligados a inicialziarlos, bien en el momento de su declaración (que no hemos hecho) o en sus constructores que tenemos que hacer.
Es costumbre en un caso como éste, escribir varios constructores para aprovechar esos valores opcionales para algunos de los atributos y no tener que pasarlos como argumentos en el caso que esos valores sean los que se desean para el objeto creado.
Procedemos entonces a armar un telescopio "plegable" y escribimos los siguientes constructores para esa clase:
Llegamos entonces a lo que Joshua Bloh le llama el "telescoping ocnstructor", en ausencia de la técnica de parámetros con nombres. Podrían haberse escrito más constructores para considerar todas las posibles combinaciones de los parámetros opcionales, en lugar de tener que especificar valores opcionales en las llamadas a los constructores, pero con los que ya hemos escrito basta para indicar que seguramente habrán otras soluciones "más elegantes".
Salta a la vista, sobre todo para aquellos que ya conocen la técnica de contenedores que administran objetos y que usan inyección de dependencias, que una solución sería escribir setters para los atributos al estilo de Java Beans. El código siguiente realiza eso aunque no escribimos todos los setters:
Con el diseño anterior, creamos un estudiante de la siguiente forma:
Dos problemas se presentan al seguir la convención de Java Beans: no podemos lograr atributos inmutables (noten que suprimí el final) y el proceso de obtener un objeto de tipo Estudiante "consistente" ocupa más de una sentencia, por lo que entre sentencias tenemos un objeto inconsistente. Cuando trabajamos con un modelo de componentes orientado a contenedores, el contenedor no me "entrega" el objeto hasta que lo tenga totalmente consistente y el grado de consistencia lo establecemos nosotros.
¿Habrá(n) otra(s solución(es)?
Por supuesto, pero la dejamos para el siguiente post.
buildear... ouch
Pero pues de qué otra forma lo decimos, si "build" y "construct" se traduce a "construir"... fabricar? ensamblar?
En fin, el patrón builder es muy útil para estos casos que mencionan:
Buildea el objetos
Continúo con el post anterior("Buildea" un objeto, no lo "construyas") describiendo, como comenta Enrique, al builder.
![]()
El patrón de diseño Builder es uno de los patrones descritos en el libro Design Patterns: Elements of Reusable Object-Oriented Software. Es un patrón creacional que separa el proceso de construcción de un objeto complejo de su representación, de manera que el mismo proceso de construcción pueda utilizarse para diferentes representaciones.
La estructura del patrón es la siguiente:
Una variante de este patrón puede utilizarse para resolver problemas con la creación de objetos, como es el problema que hemos descrito del telescoping constructor. La idea es que la clase Estudiante se asocie "fuertemente" con un objeto colaborador, que se encargue del proceso de construcción de los objetos de clase Estudiante, y que el cliente haga uso directo de ese Builder para ordenar la creación de un estudiante, una vez que el Builder tenga todos los "ingredientes" necesarios para hacerlo.
Esa asociación fuerte (una relación de amistad en C++) se logra haciendo que la clase Builder quede anidada dentro de la clase Estudiante y que sea utilizada directamente por el cliente. El método de construcción del Builder tendrá que tener acceso a la privacidad de la clase Extudiante para lograr el objetivo.
El código entonces quedaría de la siguiente forma:
La creación de objetos de clase Estudiante se realizará como sigue:
¿Parámetros con nombre? Ahí lo dejo.
Nota al margen: Lo de Buildea es a propósito para generar ira.
Duda con Map y el patron Builder
Mmmm leyendo tu comentario (bferro) me surge una duda y tal vez es algo tonta o a lo mejor no, pero si me quedo con ella seguiré igual de nublado, bueno la duda es la siguiente en la interface Map la interface interna Map.Entry seguiría este patron de diseño Builder (obviamente en las clases concretas que lo implementan como HashMap) que mencionas o es algo distinto y cuando hablas de una "asociación fuerte" podria ser un "alto acoplamiento" o estoy en un error.
Alto acoplamiento
en efecto, entre la clase Estudiante y la clase EstudianteBuilder hay una gran acoplamiento. Es el caso siempre que se diseñan clases anidadas. Tu propósito al hacer eso es que la clase Outer y las clases que anida trabajen para un fin común y entonces es inevitable el acoplamiento entre ellas. Los objetos de clase EstudianteBuilder en este caso requieren conocer las implementación de la clase Estudiante. Lo puedes modelar como una clase estructurada en UML con dos partes: la clase Estudiante y la clase EstudianteBuilder.
Gracias
Gracias, muy comprensible y claro el ejemplo, algún libro que explique patrones de diseño así de claro como usted? He leido varios artículos de patrones de diseño sin mucho éxito por la falta de casos prácticos
falta de casos?
Jaj, los patrones de diseño surgieron de la repetición de código para resolver ciertos problemas (de ahí su nombre, patrones). Pero entiendo que se pueda dar el caso de que alguien lea acerca de los patrones y no entienda sus aplicaciones prácticas, sólo que me parece curioso.
Gracias por su comentario
Gracias por su comentario ezamudio, mi comentario lo hice porque la mayoría de los patrones de diseño los he abordado más desde la teoría y ganas de aprender que desde una real necesidad. Desde ahora intentaré verlo con otros ojos
El código bien hecho está lleno de patrones
El código de marcos de trabajo y marcos de aplicaciones está repleto de patrones de diseño. Si no son recurrentes entonces no son patrones. De hecho, hace ya unos años asesoré una tesis doctoral sobre minería de patrones en código legado de muchas aplicaciones y resultó muy interesante lo que se obtuvo. El código legado había sido escrito incluso antes del movimiento de patrones.
Un pequeño matiz bferro
Salta a la vista, sobre todo para aquellos que ya conocen la técnica de contenedores que administran objetos y que usan inyección de dependencias, que una solución sería escribir setters para los atributos al estilo de Java Beans.
- bferro
Entiendo a lo que te referías con los seters y los geters en cuanto a sus combinaciones, solo quiero decir que la inyección de dependencias si acepta constructores.
Y por cierto muy buenos comentarios bferro
Gracias señor bferro
Gracias señor bferro, por compartir y por responder
A proposito.
Aquí un comentario de bferro que es muy adecuado a la conversación:
Usando reflexion
Hay otra forma de crear objetos usando el api de reflexion pero no es algo que normalmente se requiere en el dia a dia. Diagamos cuando necesitamos algunas caracteristicas dinámicas en nuestro programa para determinar si alguna clase puede ejecutar cierto metodo y no sabemos si lo tiene, por ejemplo Groovy usaba activamente la reflexion para interceptar la ejecucion de métodos y en dado caso para modificar y actualizar las clases al vuelo, no se si sigue así pero almenos las primeras versiones asi funcionaba.
Un ejemplillo para crear una instancia de MiClase e invocar miMetodo:
Lo mejor es usar métodos de
Lo mejor es usar métodos de clase ( mal conocidos como métodos estáticos ) pues como ya se menciona se permite regresar instancias de subclases de forma transparente para el cliente.
Si tuviéramos una subclase de estudiante para una parte del sistema en la cual se quisiera imprimir el comprobante de inscripción se podría hacer algo como esto:
y
El builder es mejor usarlo cuando no se tiene toda la información en el momento y se necesita crear un objeto inmutable. Otro uso es para simular los parametros nombrados, pero parece demasiado trabajo solo para eso.
Finalmente como divertimento y también un poco para agregar legibilidad al código se pueden usar métodos que decoren la invocación. El proyecto Hamcrest hace algo similar a esto aunque no para crear objetos precisamente y sino el contexto de las pruebas unitarias aunque no se restringe a ese ámbito solamente. La idea es utilizar métodos como parámetros nombrados.
La implementación es absurdamente sencilla:
Y ya en ese tenor, se podrían poner nombres más interesantes no?
Parece ridículo y en alguna forma lo es. Pero con el advenimiento de la moda de los DSL's con el único fin de tener claridad en el código, vale la pena evaluar esta técnica para no tener que usar un nuevo lenguaje solo por que se puede.
Una cosa a recordar en este tipo de cuestiones es que la simpleza siempre transmite más directamente la idea.
Jo jo encontré esta forma
Jo jo encontré esta forma extra de dizque "truquear" los parámetros nombrados
El efecto es el intentado... se oye interesante aunque.. meh