viernes, 20 de julio de 2007

Paso V: ¿Conoce usted qué significa desarrollar con Objetos?

Antes de proseguir, quiero dejar claro que en este paradigma si un objeto no tiene cierta responsabilidad es porque la tiene otro, no existe alternativa. Por lo tanto, si le saco la responsabilidad a las llamadas de calcular su costo, se la tengo que dar a otro objeto, no hay alternativa. La pregunta en cuestión es ¿cuál es ese objeto? Nuevamente debemos recurrir a una analogía. Pensemos por un momento como sería una oficina encargada de facturar llamadas telefónicas, que no posea ningún tipo de computadora para automatizar el procedimiento. ¿Qué personas compondrían esta oficina?, ¿cuáles serían sus responsabilidades? Seguramente habría una persona encargada tipificar una llamada para luego determinar su costo en base a una tabla de precios. Estos son los objetos que necesitamos en nuestro modelo, uno para representar a la persona que tipifica y factura (¿o quizá dos?) y otro para representar la tabla de precios.

El primero puede ser instancia de la clase “Facturador”, sabrá responder cuál es el costo de una llamada dada una tabla de precios. La tabla de precios será otro objeto que indicará el costo por pulso de cada tipo de llamada. Juntos, nos permitirán obtener el costo de una llamada y por lo tanto de todas.

Unos párrafos atrás comenté que la solución no estaba “a la vista”. Esto se debe a que los objetos que necesitábamos ni siquiera estaban mencionados en el enunciado del problema; encontrarlos requiere cierto poder de análisis, capacidad de abstracción para crear analogías y porque no, un pizca de imaginación (¿será por esto que algunos ver la programación como un arte?). Lo interesante de este caso es que ustedes pueden, a partir de ahora, hacer analogías similares para resolver los problemas que se les presentan, utilizar la misma técnica y ver que resultados obtienen. Les aseguro que serán muy positivos.

Sin embargo suena un poco raro que un facturador solo responda el costo de una llamada, su nombre parece indicar otra cosa. En realidad lo que estoy esperando de dicha persona (objeto) es una factura para un cliente. Nuevamente nos encontramos con un caso interesante de este problema, empezamos a hablar de factura algo que no había surgido hasta ahora pero que se comenta en el enunciado. Recuerden la máxima de este paradigma: “Todo ente de la realidad que se está modelando debe tener un objeto que lo represente”; por lo tanto si existe una factura en la realidad, una factura debemos tener en nuestro modelo, o sea, en nuestro programa. ¿Se les había ocurrido tener un objeto para representar la factura? De todas las entrevistas que tomé con este problema, nadie, pero nadie modelo una factura.

¿Por qué es importante tener una factura? La respuesta trivial es: ¡porque existe en la realidad! Pero si ustedes son pragmatistas y quieren una respuesta útil, verán que existen varios motivos, como tener que reimprimir una factura sin volver a calcularla, querer consultar cuanto se facturó (sumar todas las facturas), etc. Aún más interesante es que la factura me permita determinar los motivos por los cuales se llegó a los valores que posee, por lo tanto la factura no solo tendría los ítems facturados sino también podría, de alguna manera, permitirnos llegar a entender por qué se calcularon (importante al momento atender reclamos de los clientes, ¿no lo creen así?). En definitiva, necesitamos también una clase factura que será luego la que por pedido del ejercicio, se deberá imprimir en pantalla.

¿Qué sucede con la clase Cliente de la solución original?, ¿es una buena abstracción? Nuevamente debo responder que no. Posee varios errores respecto al ente que debería representar de la realidad. ¿Un cliente en la realidad, conoce todas las llamadas que hizo?, ¿ustedes como usuarios de teléfono, se acuerdan?, claramente no. No es la responsabilidad de un cliente conocer todas las llamadas que hizo. Menos aún porque si fuese así podría mentirle a la telefónica para tener que pagar menos, ¿no les parece? Este último argumento es válido para demostrar que un cliente ¡no puede tener la responsabilidad de imprimir su factura!, ¡sería una locura! Fíjense como el problema que estamos viendo en este caso no tiene que ver con cuestiones computacionales, no es un problema de performance, acoplamiento, encapsulamiento de datos, etc., es un problema de interpretación de la realidad, es un problema semántico, relacionado al significado que le otorgamos a los objetos, al lenguaje que construimos con nuestro modelo. Este último punto es el que marca la diferencia entre saber programar y saber programar con objetos. La diferencia radica en que programar con objetos implica entender el problema que estamos modelando, entender su significado y entender que por cada ente de este problema debe haber un objeto en nuestro modelo. No puede suceder que exista un objeto en nuestro modelo que represente un cliente y que dicho objeto no tenga nada que ver con un cliente real. ¿Se imaginan la reacción de un experto del dominio si le decimos que el encargado de facturar en nuestro programa es un cliente? No se deben crear objetos por cuestiones computacionales o de conveniencia organizativa, se deben crear si existen en la realidad que estamos modelando.

Ya estamos llegando al final, no se desanime. La verdad es que se necesitan unos cuantos objetos más, como el número de teléfono, alguien que se encargue de tipificar si una llamada es local, nacional o internacional a partir del número de origen y destino, etc., pero creo que a esta altura el objetivo del artículo está cumplido, que usted pueda responderse con sinceridad, sin necedad, si sabe o no desarrollar con objetos correctamente. ¡No se culpe sino es así!, es totalmente comprensible puesto que hay muy pocos buenos libros del tema, los cursos que abordan este problema generalmente lo hacen desde una perspectiva tecnológica con el objetivo de enseñar un lenguaje de programación, y sobre todo, es necesario cambiar la manera de pensar respecto de qué es un programa y cómo se debe crear un programa cuando se trabaja con objetos.

Lo importante es que usted recuerde por lo menos estas frases y trate de aplicarlas:

  • Un programa es un modelo de un problema de la realidad
  • Cuando se utiliza el paradigma de objetos para crear dicho modelo, se cuenta únicamente con objetos que representan entes de la realidad y mensajes que se envían entre sí para llevar adelante una tarea.
  • Es muy importante descubrir qué objetos existen en la realidad y otorgar correctamente las responsabilidades a estos. Hacerlo permite reutilizar una solución.
  • No todos los objetos están a la vista, hay que usar la imaginación para descubrir aquellos que existen implícitamente en la realidad. En otras palabras, hacer explicito lo implícito.
  • Muchos de los objetos que no están a la vista, viven en la ambigüedad del lenguaje natural que usamos los seres humanos para comunicarnos y el la información contextual de una conversación. Es muy importante entrenar nuestra capacidad para encontrar estos objetos.
  • La herencia o subclasificación es una herramienta poderosa pero pésimamente usada; en algunos casos dificulta el reuso.

Espero que hayan disfrutado de este artículo, por lo menos yo lo hice escribiéndolo. Si alguna vez quieren ver la solución completa del ejercicio vayan a la materia de Programación Orientada a Objetos que damos en la Facultad de Ciencias Exactas de la UBA o preguntenme!! jaja.

13 comentarios:

Anónimo dijo...

Me acabo de enterar que cuando entré a Mercap no sabía nada de objetos... jajaja... No es que hoy sepa mucho mas, pero por lo menos en el modelo que pensé ahora estaba el Facturador (o Tasador como lo llamé yo) :P
Con tu permiso, quizás te robe algunas frases para la materia que damos en la UTN (paradigmas de programación).
La verdad que muy bueno el artículo(o la saga de articulos)! Keep goin'!!
Jorge S.

Hernan Wilkinson dijo...

Roba lo que quieras!! es gratis!! jaja.
Gracias por el comentario Jorge!

Saludos,
Hernan.

Abel dijo...

Realmente excelente la sucesión de posts acerca del desarrollo con objetos. Es muy bueno poder repasar estos conceptos (que con las circunstancias del trabajo diario a veces vamos olvidando, o se nos van diluyendo) y profundizar en los aspectos que uno desconoce.

Muy interesante también la forma de exposición, haciendo que uno se choque con el problema en primera instancia para luego explicar cuál es el error.

Gaboto dijo...

Muy bueno, (sigo leyendo desordenadamente jeje)
Lo que podria ser interesante tambien es tener el tipo de llamada como un objeto.
O sea, en lugar de tener conjuntos, tener un objeto que tipifique las llamadas, entonces sepa responder al tipo de una llamada. No se que ventajas o desventajas podria tener esta solucion. Luego en la tabla de costos se consulta el costo de ese tipo de llamadas...algo asi.
Que opinas?

Hernan Wilkinson dijo...

Está interesante tu propuesta... no me parece mal, habría que entender bien cual sería la responsabilidad de este "tipo" de llamada... si es solo para identificar algo, alcanza con los conjuntos, si tiene más responsabilidad no me parecería mal, pero de entrada no veo que otra responsabilidad tendría...

Sebastian dijo...

Hola Hernan, muy buen tema pero me quedan dudas al respecto que me gustaria debatir.
Si yo modelo la realidad al extremo, entonces voy a tener roles (Facturador) que llevan a cabo una tarea, manipulando siempre objetos. Pero si justamente éstos Roles son los que saben como llevar a cabo una tarea dado un objeto, éstos objetos van a resultar siendo objetos anémicos con getters.
En el ejemplo del sistema de Facturación, el Rol Facturador en realidad lo cumple el servicio de Dominio (ServicioFacturacion). Siendo la factura el aggregate root, porque no dejar que el objeto Factura tenga un metodo "charge(PriceTable priceTable, Collection calls)" y que la Factura sea quien carga a si misma las llamadas y usa la tabla de precios para determinar los costos.
Ya para eso voy a tener que obtener de la llamada su origen y destino y voy a estar violando la Ley de Demeter.
Si voy al extramo de la realidad, siempre voy a tener a los Roles haciendo cosas y a los objetos de negocio anemicos, usandolos nomas de contexto.
Por que la llamada no puede retornar un mensaje diciendo si es local o internacional o nacional? La llamada tambien seria un objeto anemico como lo describis en el ejemplo, con getters.
Por ahi estoy equivocado, por eso me gustaria que me aclaren el panorama porque tengo un menjunje de Domain Driven Design con el ejemplo que diste.

Hernan Wilkinson dijo...

Que tal Sebástian, gracias por tu comentario. Te comento algunas cosas.
1) La factura debería ser el resultado de hacer el proceso de facturación, por lo tanto no está bien darle dicha responsabilidad a la factura. Una vez generada la factura no se la debería poder modificar.
2) Los objetos "roles" (como los llamaste) no son los únicos que hacen cosas, además esos objetos también son objetos de negocio, no entiendo por qué haces la separación
3) No debería pasar lo que comentas de tener objetos que solo responden a mensajes del tipo getter y setter, si sucede eso algo estamos haciendo mal. Me da la sensación que por ahí tenés una confusión entre otorgar la responsabilidad a los objetos correctos con que representa cada uno de ellos. Un objeto que representa una llamada puede tener responsabilidades como saber si el origen y destino son del mismo pais o localidad, etc. pero eso no implica que sepa responder mensajes relacionados a la facturación propiamente dicha.
Espero haber sido claro, sino seguí preguntando!

Sebastian dijo...

Gracias por las respuestas, te sigo preguntando :)

Corregime si me equivoco, pero mi facturador basicamente quedaría asi:

precio=tablaCostos.precioPara(llamada.duracion,
servicioLocalidades.localidadPara(llamada.nroOrigen()),
servicioLocalidades.localidadPara(llamada.nroDest()));

pero en este caso de la llamada estaria solo obtiendo sus valores a traves de sus getters.

No seria mejor que la llamada conozca a la tablaDeCostos y me devuelva su precio:

llamada.calcPrecioSegun(TablaCostos)

y como la llamada internamente puede conocer las Localidades origen y destino, en la clase Llamada hacer simplemente:

return tablaCostos.precioPara(this.duracion, this.locOrigen, this.locDestino)

y asi evitar los getters y modelar el comportamiento en la entidad y no en el servicio (Facturador)

La Llamada conoce a la tabla de costos porque yo se la paso, pero no la referencia como dependencia suya.

Saludos.

Hernan Wilkinson dijo...

Que tal Sebastian,
aunque la llamada conozca momentaneamente la tabla de costos, estas creando un acoplamiento innecesario. De hecho, estás haciendo que una llamada sepa responder el mensaje calcPrecioSegun que tiene que ver con una responsabilidad que no le corresponde a la llamada. Pensa esto, si queres llevar tu abstracción de llamada a otro lugar (otro sistema, etc), también tenes que llevarte la tabla de precios, el método de calcPrecioSegun, etc. cuando por ahí no lo necesitas. Por ejemplo, si estás haciendo un sistema que categoriza las llamadas para hacer campañas de marketing, que necesidad hay de que la llamada responda ese mensaje o esté acoplada a la tabla de precios? La respuesta es ninguna y eso demuestra que esa funcionalidad no es esencial a la llamada y por lo tanto no debería estar dentro de sus responsabilidades.
Espero haber sido claro, cualqueir cosa seguí preguntando, no hay drama! todo lo contrario.

Un abrazo,
Hernan.

Sebastian dijo...

Hernán,

Estaba leyendo -TDD by example- de Kent Beck y plantea el ejemplo prograsivo del libro en el dominio del dinero y sus monedas.
Basicamente se le pide a un "banco" que conoce las tasas de cambio, que calcule una expresion como la siguiente a dolares:

$AR 5 + u$s 10

Por un lado la clase -Money- representa un valor y su moneda, y por el otro lado la clase Banco a quien se le otorga la responsabilidad de traducir una expresion a una moneda determinada.

class Bank
Money reduce(Expression exp, String currency)

pero su implementacion no hace nada, solo delega a la expresion (Money) la responsabilidad de traducir su valor a otra moneda, por lo que la clase Money conoce al banco:

Money reduce(Bank bank, String toCurrency)

e internamente en su implementación, se le pide al banco la tasa de cambio de la moneda de "Money" a la moneda "toCurrency"... entonces, por la clase Money conoce momentaneamente al Banco.

En que difiere este caso con lo que veniamos hablando? No es lo mismo que la llamada conozca momentaneamente a la TablaDeCostos para resolver un problema especifico, que la clase Money conozca al banco para resolver otro problema especifico? Me da la sensación de que es lo mismo.

Saludos!

Hernan Wilkinson dijo...

Que tal Sebastian, el ejemplo de Beck es similar como decís, pero eso no significa que la solución sea buena. De hecho no me parece buena. Para aquellos que conocen cómo se determina el precio de una moneda respecto de otra, saben que este proceso no depende de un banco sino de un mercado en un punto del tiempo determinado, con lo cual es mucho más complicada la solución que la que provee Beck.
Más allá de eso, ¿por qué querés darle esa responsabilidad una cantidad de dinero? ¿No te parece suficiente las responsabilidades que una cantidad de dinero tiene como para agregarle esa responsabilidad? ¿por qué te parece malo tener un objeto que se encarge de convertir monedas? De esa manera además de la ventaja de desacoplamiento que ya te comenté, al estar reificado esa acción en un objeto podrías fácilmente luego guardar el resultado de la conversión y quién se encargo de hacerla (para conocer luego en que mercado y a que hora).
Como siempre, en el diseño hay que tomar decisiones y hacer trade-offs. Personalmente prefiero objetos chicos, bien funcionales y poco acoplados. La solución de Beck no rompe ningúna "regla" de diseño gravemente, pero tiene más acoplamiento y es menos cohesiva... tomá la que más te guste :-)

Diego Campodonico dijo...

Que tal Hernan? Perdón por postear en algo tan viejo, pero es un tema interesante el de la asignación de responsabilidades a objetos.
Con respecto a lo que dice Sebastián en cuanto a modelar la realidad al extremo, estoy de acuerdo. El tema es que hay objetos que existen físicamente en la realidad y otros que no. Por ejemplo, si tengo un objeto que representa "3 metros" y otro que representa "5 metros", parece no haber nada de malo en hacer "3 metros" + "5 metros", sin embargo en la realidad no existen físicamente, y por lo tanto no pueden sumarse por sí solos. Si seguimos la analogía tendríamos que tener un "SumadorDeMedidas" al cual le pasamos los "3 metros " y los "5 metros".
Y así cada objeto inanimado de la realidad no tendría comportamiento, porque en la realidad no lo tiene.
Creo que a esto se refería Sebastián.

aldo dijo...

Hola Hernan,
Excelente los post sobre desarrollar en objetos!
En este último, nombras que hay muy pocos buenos libros sobre el tema.
Me podrás indicar algunos de esos?
Muchas gracias!
Saludos