Tabla de contenidos
- Introducción a la segunda edición
- Introducción de la primera edición
- Capítulo 1. Cuando los comentarios confunden
- Capítulo 2. El nombre de la cosa
- Capítulo 3. Acondiciona las condicionales
- Capítulo 4. Deja atrás lo primitivo
- Capítulo 5. Refactoriza a Enumerables
- Capítulo 6. Refactoriza de single return a return early
- Capítulo 6. Aplica Tell, Don’t Ask y la Ley de Demeter
- Capítulo 8. Dónde poner el conocimiento
- Lecturas recomendadas
- Notas
Introducción a la segunda edición
Hace ya unos cuantos años que escribí Refactor Cotidiano como una especie de guía sobre cuando y qué refactorizar en el trabajo diario como desarrolladora.
Cuando comencé a trabajar profesionalmente en el mundo de la programación, la idea de refactorizar no me resultó nunca extraña. Como persona que llevaba escribiendo blogs y otros textos desde años atrás, y viniendo del mundo educativo, estaba muy habituada a la idea de revisar y refinar todos los textos que escribía. Se convierte en algo natural, tanto para evitar en lo posible faltas de ortografía o sintaxis, como para asegurar que lo escrito refleja correctamente tu pensamiento y puede generar el efecto que pretendes cuando sea leído.
Por esa razón, mi estilo de programación se parece mucho al proceso que seguía y sigo al escribir. Expreso una idea en código y trato de refinarla en sucesivas revisiones, las cuales me permiten entender mejor lo que estoy haciendo y me abren oportunidades para otras mejoras.
A veces, resulta difícil encontrar la forma adecuada de expresar un concepto mediante código. En ese caso, intento hacerlo de forma basta, aunque funcional. Una vez que lo tengo delante, es cuando puedo examinarlo en busca de ciertos patrones que me dan las pistas de cómo modificarlo para que la expresividad y el diseño sean mejores.
Esto tiene un límite. Muchas veces ocurre que mi conocimiento actual del dominio en el que esté trabajando no es suficiente para expresarlo de la mejor manera. Y en esos casos, el código va así a producción: es funcional, hace lo que se supone que debe hacer, pero es manifiestamente mejorable. No pretendo seguir dando más vueltas hasta encontrar la versión perfecta. Esas mejoras llegarán en un futuro, cuando una nueva intervención nos lleve por esos mismos archivos, pero esta vez con un mejor conocimiento.
Acerca de este libro
Refactor cotidiano no es un catálogo de técnicas al estilo del Refactoring de M. Fowler, sino más bien una colección de ideas sobre temas o áreas en las que fijarse cuando estamos trabajando.
Por ejemplo, el libro empieza hablando de los comentarios y discute algunos de los problemas que pueden aparecer relacionados con ellos, cuando usarlos y cómo hacer para que sean innecesarios.
Acerca de la segunda edición
Refactor cotidiano tiene el honor de haber sido el libro de la primera edición de la PulpoCon (2019). Esta conferencia dedicada al desarrollo de software con orientación crafter se celebra en Vigo, en algún fin de semana de septiembre, y ya va por la tercera edición a la hora de escribir estas líneas.
El caso es que la conferencia adoptó la costumbre de regalar un libro exclusivo a las personas asistentes y cuando Rolando Caldas, su organizador, me comentó la idea me di cuenta de que una serie de artículos publicada en el blog podría convertirse fácilmente en ese libro.
Siempre tuve la sensación de que no era ninguna maravilla, pero por lo visto resultó útil a muchas personas, de lo que me alegro. El caso es que poco después de la celebración de la última conferencia me planteé revisar el libro.
Al releerlo me sorprendió que no había envejecido tan mal como pensaba, salvo por los ejemplos de código, que son de PHP y de una versión relativamente antigua. Pero, por otro lado, se me ocurrió que tal vez se podrían ampliar algunos capítulos, retirar las alusiones al blog, mejorar los ejemplos, tal vez introducir otros lenguajes, y convertir el libro en un manual un poco más moderno y más transversal.
Siendo un poco más sistemático, las áreas de mejora del libro:
- En algunos capítulos se remite a la lectora a páginas del blog para ampliar la explicación del porqué de algunos refactors. Esta explicación podría incorporarse en el capítulo para completarlos, aunque sea de forma resumida.
- El capítulo de enumerables es un poco específico de la situación de PHP en el momento de escribirlo. El lenguaje ahora tiene la suya propia, más similar a la de otros lenguajes. Diría que este capítulo necesita una reescritura más profunda que refleje la nueva realidad y que aproveche los puntos positivos de la propuesta original del libro.
- Se echa de menos el tema del refactor a colecciones, lo que merece posiblemente su propio capítulo.
- Otro asunto que tiene más que ver con el enfoque es: ¿por qué no estructurar el libro en torno a problemas frecuentes del código y cómo abordarlos mediante refactor? Algo similar a los code smells, pero quizá expresados de una manera menos taxonómica.
- Revisar el lenguaje y la expresión, aclarando algunos párrafos confusos.
Así que con estas ideas en la cabeza, empecé a trabajar, intercalando con otros proyectos. A medida que iba tomando forma, surgieron algunas ideas:
- Algunos cambios en la organización del índice
- Incluir un capítulo para sugerencias de lecturas y otros recursos
- Una pequeña introducción a cada capítulo, de estilo pretendidamente humorístico
- Notas a la segunda edición en cada capítulo, explicando los cambios introducidos en el capítulo cuando van más allá de la revisión de estilo o expresión.
Introducción de la primera edición
En The Talking Bit hemos escrito bastante sobre refactor, principalmente porque nos parece que es una de las mejores cosas que podemos hacer con nuestro código, sea nuevo o legacy. El refactor es una forma de mantenimiento del código que consiste en mejorar su expresividad a través de pequeños cambios que no alteran el comportamiento y que tampoco cambian sustancialmente la implementación.
Una reescritura, por el contrario, suele plantear un cambio brusco de implementación que podría incluso provocar cambios en el comportamiento. Este proceso busca reconstruir el software a partir de nuevos principios o ideas.
Por otro lado, el refactor puede hacerse de una manera continua e iterativa, interviniendo en el código siempre que se tenga ocasión. Por ejemplo: porque estamos revisándolo a fin de corregir un error o añadir una nueva característica.
Durante el proceso de lectura y análisis podemos encontrarnos con fragmentos de código que no expresan bien un concepto del dominio, que no se entienden fácilmente o que interfieren en la compresión de ese código. Ese momento es ideal para realizar pequeños refactors seguros que, acumulados a lo largo del tiempo, van haciendo que el código evolucione hacia un mejor diseño. Pero sobre todo, hacen que el código refleje cada vez mejor el conocimiento que tenemos.
Como ya hemos mencionado en otras ocasiones, el refactor trata principalmente sobre conocimiento y significado. Es decir, trata sobre que el código exprese cosas y, concretamente, que exprese de la mejor forma posible nuestro conocimiento sobre el dominio en el que trabajamos y cómo estamos resolviendo los problemas que nos plantea.
Por esa razón, se nos ha ocurrido que podría ser buena idea crear una especie de curso o guía para aprender sobre cómo hacer refactor cotidiano.
Refactor sin piedad
El refactor cotidiano es otro nombre para una práctica de Extreme Programming conocida como refactor sin piedad. Dicho en pocas palabras, consiste en refactorizar el código en cualquier momento en que sea posible. Contra lo que podría parecer no se trata de engolfarse dando vueltas al código hasta llegar a un código perfecto, sino de realizar retoques limitados para corregir lo que en ese momento podemos considerar como pequeñas imperfecciones. Por ejemplo:
- Un nombre no consigue expresar correctamente un concepto.
- Una expresión compleja podría descomponerse otras más pequeñas, más fáciles de entender y mantener.
- El cuerpo de un método o función es muy largo y puede ser dividido en secciones autónomas.
- Una expresión condicional es difícil de entender y se puede encapsular como función o método con un nombre expresivo, ocultando sus detalles.
- Una estructura condicional presenta demasiada anidación, de modo que extraemos sus partes para organizarlo según niveles de abstracción.
- Un fragmento de código está en lugar inadecuado y lo movemos a donde corresponde.
Con el tiempo, la acumulación de estos pequeños cambios irá mejorando la estructura, expresividad y mantenibilidad del código. Es muy posible que acabe revelando oportunidades de cambio de mayor calado que tal vez impliquen un rediseño.
En cualquier caso, al mejorar la situación del código, mejoran nuestras posibilidades de intervenir en él. Incorporar nuevas prestaciones será más rápido y seguro, lo que nos permite entregar con mayor frecuencia y predictibilidad.
Los momentos del refactor
En su charla Flows of refactoring, Martin Fowler explica qué es realmente el refactor y cuándo se tendría que hacer.
Fowler insiste en la idea de que si necesitas un momento especial para refactorizar es que no lo estás haciendo, sino, tal vez, reescribiendo o cambiando el diseño de tu software.
Existen tres momentos principales para el refactor:
- Durante la lectura de código
- Refactor preparatorio para introducir un cambio
- Refactor posterior a un cambio
Lectura de código, los momentos WTF
Pasamos la mayor parte del tiempo leyendo código. Cada vez que queremos introducir un cambio necesitamos leer el código existente para saber dónde es el mejor lugar para hacerlo o averiguar qué recursos ya están disponibles. También leemos el código simplemente para buscar algún tipo de conocimiento relacionado con el código mismo o el negocio.
En esos momentos podemos encontrarnos con fragmentos que nos hagan lanzar una interjección (WTF!), o que necesitamos leer dos o tres veces para entender lo que ocurre. Momentos en los que vemos algo que, por alguna razón no cuadra. No es que no funcione, es simplemente que hay algo descolocado, sucio, fuera de sitio, desentonando. En una palabra: desordenado.
Por lo general, estos momentos de extrañeza los genera la existencia de una distancia entre nuestro conocimiento del negocio y cómo se expresa en el propio código. Este desfase es lo que Ward Cunningham denominó deuda técnica.
La deuda técnica se paga refactorizando el software para que refleje el conocimiento del negocio de la mejor manera posible.
En cierto modo, es imposible evitar que el código tenga un cierto nivel de deuda técnica porque el conocimiento del negocio cambia mucho más rápidamente. De hecho, cuando introducimos cambios en el software y los llevamos a producción, el negocio ya está cambiando. En parte, gracias a las nuevas funcionalidades del software y al impacto que tienen.
La forma de prevenir el crecimiento de la deuda técnica es entregar de forma sostenida pequeños cambios y refactorizar constantemente.
Refactor preparatorio
Al introducir cambios en el código para incorporar nuevas funcionalidades o corregir errores suele ser necesario arreglar el código existente para hacer posibles esos mismos cambios. Una cita de Kent Beck lo expresa más o menos así:
For each desired change, make the change easy (warning: this may be hard), then make the easy change
Es decir:
Primero, haz que el cambio sea fácil (advertencia: puede ser difícil conseguirlo). Luego haz el cambio fácil.
Con frecuencia el código no está preparado para admitir ciertos cambios, por lo que debemos transformarlo primero a fin de que la nueva funcionalidad sea fácil de introducir.
Esto es lo que denominamos refactor preparatorio y debería hacerse siempre antes de introducir los nuevos cambios. En otras palabras: mejoramos la estructura del código asegurándonos de no cambiar su comportamiento. Mezclamos esos cambios y solo entonces procedemos a desarrollar la nueva funcionalidad.
Usando otra metáfora de Kent Beck: usamos dos sombreros: el de refactorizar y el de crear nueva funcionalidad. No podemos llevar los dos sombreros a la vez.
Refactor posterior a un cambio
Tras realizar los cambios necesarios volvemos a encasquetarnos el sombrero de refactor para limpiar el código que acabamos de introducir.
Es posible que nuestra nueva funcionalidad haya introducido algo de duplicación y esto podría revelar la existencia de un concepto más general que hasta entonces no habíamos descubierto. En otras ocasiones, el código nuevo incrementa la complejidad de algún área y debemos trabajar para simplificarla.
Al añadir código a un sistema de software, aumentamos su entropía o desorden. La entropía de software es un concepto introducido por Ivar Jacobson y otros en el libro Object-Oriented Software Engineering: A Use Case Driven Approach. La mejor forma de luchar contra un crecimiento desmesurado de la entropía, es aplicar un refactor continuado, particularmente tras introducir código nuevo.
Cómo refactorizar
La idea del refactor cotidiano es muy simple: Se trata de realizar pequeños cambios inocuos en nuestro código en cualquier momento que se nos presente la ocasión. Es lo que algunos autores denominan refactor oportunista. Nuestra propuesta concreta es que hagas un refactor muy pequeño cada vez que lo veas necesario, de modo que, en una primera fase:
- solo tocas un archivo.
- los cambios quedan recogidos en un único commit atómico, que contengan solo los cambios debidos a ese refactor.
En una segunda fase, a medida que ganas confianza:
- los cambios podrían afectar a varios archivos, pero el ámbito es limitado.
- los cambios quedan recogidos en un único commit atómico.
En una tercera fase:
- los cambios podrían suponer introducción de nuevas clases.
- de nuevo, los cambios quedarían recogidos en un único commit.
La guía
Esta guía se compone de una serie de capítulos en los que se exponen diversas orientaciones y principios que seguir a la hora de refactorizar. La idea es explicar ámbitos en los que podrías intervenir en los tres niveles indicados en el apartado anterior. En muchos casos los refactors propuestos, al menos en el primer nivel o fase no necesitarían tests porque podrían ejecutarse mediante herramientas del IDE.
Con el tiempo, es posible que esos pequeños refactors acumulado día tras día, mejoren la forma y calidad de tu código y te despejen caminos para mejorar su expresividad y arquitectura.
Así que, ¡happy tiding!
Capítulo 1. Cuando los comentarios confunden
En el que se trata de como gestionar los comentarios de un código, con especial atención a las situaciones en que el comentario más que ayudar nos confunde, así como criterios para decidir qué comentarios mantener y cuáles no.
Comentarios y documentación
Los lenguajes de programación incluyen la posibilidad de insertar comentarios en el código como forma de añadir documentación al mismo. Es decir: el objetivo de los comentarios es poner conocimiento cerca del lugar en el que puede ser necesario.
Los comentarios en el código parecen una buena idea y, probablemente, eran más útiles en otros tiempos, cuando la necesidad de economizar recursos y las limitaciones de los lenguajes de programación no nos permitían escribir un código lo bastante expresivo como para ser capaz de documentarse a sí mismo.
Así que, en este capítulo, intentaremos explicar qué comentarios nos sobran y por qué, y cuáles dejar.
Notas de la segunda edición
No hay grandes cambios en este capítulo. En general, se mantienen las mismas ideas que en la primera edición. Lo más significativo es que hemos añadido una sección con una heurística (las seis preguntas) que puede ser útil cuando tengas que decidir si añadir o mantener un comentario.
Por lo demás, este capítulo no es una diatriba general contra los comentarios o la documentación en el código, sino contra la documentación innecesaria, desactualizada o aquella que puede quedar fácilmente desactualizada.
Como norma general, la documentación debería estar lo más cerca posible del propio código. Esta podría ser una forma útil de organizar la documentación:
- El código expresa claramente qué hace y cómo.
- Los tests documentan cómo se usa el código, mediante ejemplos que se pueden ejecutar.
- Se añaden comentarios cuando clarifican el porqué de ciertas decisiones.
- El gestor de versiones documenta la historia del desarrollo.
- El archivo README explica la naturaleza del proyecto, su relación con otros proyectos en su caso, y documenta el proceso de instalación, uso en local, desarrollo y testing.
- Mejor aún si estos procesos están automatizados, mediante una herramienta tipo
make
o la equivalente para un ecosistema específico. Considérala una pipeline local. - El archivo README enlaza a documentos que puedan ser útiles, como how-to, configuración del IDE para el proyecto, o cualquier otro proceso que se considere oportuno, y se guardan en el mismo repositorio.
- Las decisiones más abstractas sobre el código, como pueden ser decisiones sobre diseño, convenciones de código, decisiones sobre tecnologías, etc., se pueden documentar mediante Architecture Decision Records (ADR), que se guardarán también en el repositorio.
- Los manuales de uso de la aplicación o el software pueden documentarse externamente, pero es conveniente enlazarlo en el README del proyecto, que actuaría como punto de entrada.
¿Por qué deberías eliminar comentarios?
Las principales razones para borrar comentarios son:
Suponen una dificultad añadida para leer el código. En muchos aspectos, los comentarios presentan una narrativa paralela a la del código y nuestro cerebro tiende a enfocarse en una de las dos. Si nos enfocamos en la de los comentarios, no estamos leyendo el código. Si nos enfocamos en la del código: ¿para qué queremos comentarios?
Los comentarios suponen una carga cognitiva. Incluso leyéndolos con el rabillo del ojo, los comentarios pueden suponer una carga cognitiva si, de algún modo, discrepan con lo que el código dice. Esto puede interrumpir tu flujo de lectura hasta que consigues aclarar si ese comentario tiene algún valor o no.
Pueden alargar innecesariamente un bloque de código. Idealmente, deberías poder leer un bloque de código en una sola pantalla. Los comentarios añaden líneas que podrían provocar que tengas que deslizar para ver todo el bloque. Son especialmente problemáticos los que están intercalados con las líneas de código.
Pueden mentir. Con el tiempo, si no se hace mantenimiento de los comentarios, estos acaban siendo mentirosos. Esto ocurre porque los cambios en el código no siempre son reflejados con cambios en los comentarios por lo que llegará un momento en que unos y otros no tengan nada que ver.
Refactor de comentarios
Básico
Simplemente, eliminamos los comentarios que no necesitamos. Es un refactor completamente seguro, ya que no afecta de ningún modo al código.
Reemplazar comentarios por mejores nombres
Eliminamos comentarios obvios, redundantes o innecesarios, cambiando el nombre de los símbolos que tratan de explicar.
Cambiar nombres es un refactor muy seguro, sobre todo con la ayuda de un buen IDE, que puede realizarlo automáticamente, y dentro de ámbitos seguros, como método o variables privadas.
Reemplazar comentarios por nuevas implementaciones
En algunos casos podríamos plantearnos mejorar el diseño de una parte del código porque al reflexionar sobre la necesidad de mantener un comentario nos damos cuenta de que es posible expresar la misma idea en el código.
Este tipo de refactor no encaja en la idea de esta serie sobre refactor cotidiano, pero plantea el modo en que los pequeños refactors del día a día van despejando el camino para refactors e incluso reescrituras de mayor alcance.
Comentarios redundantes
Los comentarios redundantes son aquellos que nos repiten lo que ya dice el código, por lo que podemos eliminarlos.
Por ejemplo:
En serio, ¿qué nos aporta este comentario que no esté ya expresado?
Los lenguajes fuertemente tipados, que soportan type hinting y/o return typing, nos ahorran toneladas de comentarios. Y de tests.
Los tipos de los parámetros y del objeto devuelto están explícitos en el código, por lo que es redundante que aparezcan como comentarios.
Excepciones: este tipo de comentarios tiene su razón de ser cuando no podemos hacer explícitos los tipos.
Puedes eliminar los comentarios redundantes poniendo mejores nombres. Por ejemplo, en este caso en que utilizamos un constructor secundario:
Con un nombre expresivo ya no necesitamos comentario:
O incluso más explícito:
Y podemos usar el objeto así, lo cual documenta perfectamente lo que está pasando:
Más excepciones: si lo que estamos desarrollando es una librería que pueda utilizarse en múltiples proyectos, incluso que no sean nuestros, los comentarios que describen lo que hace el código pueden ser necesarios.
Comentarios mentirosos
Los comentarios mentirosos son aquellos que dicen algo distinto que el código. Y, por tanto, deben desaparecer.
¿De dónde vienen los comentarios mentirosos? Sencillamente, ha ocurrido que los comentarios se han quedado olvidados, sin mantenimiento, mientras que el código ha evolucionado. Por eso, cuando los lees hoy es posible que digan cosas que ya no valen para nada.
Mi experiencia personal con este tipo de comentarios cuando entro a un código nuevo suele ser bastante negativa. Si el comentario y el código difieren te encuentras con el problema de decidir a cuál de los dos hacer caso. Lo cierto es que el código manda porque es lo que realmente se está ejecutando y lo que está produciendo resultados, pero la existencia del comentario genera esa inquietud: ¿por qué el comentario dice una cosa y el código hace otra?
Habitualmente, debería ser suficiente con comprobar si hay diferencias en la fecha en que se añadió el comentario y la fecha en la que se modificó el código. Si esta es posterior, es que tenemos un caso de comentario mentiroso por abandono y lo más adecuado sería borrarlo.
Este hecho debería bastar para que no añadas nuevos comentarios sin una buena razón. Tendemos a ignorar los comentarios triviales, de modo que cuando cambiamos el código nos despreocupamos de mantenerlos actualizados y acaban siendo mentirosos. Así que procuraremos dejar solo aquellos comentarios que nos importen realmente.
Si ya nos hemos librado de los comentarios redundantes, deberíamos contar solo con los que pueden aportar alguna información útil, así que nos toca examinarlos para asegurarnos de que no sean mentirosos. Y serán mentirosos si no nos cuentan lo mismo que cuenta el código.
Puede parecer un poco absurdo, pero al fin y al cabo los comentarios simplemente están ahí y no les prestamos mucha atención, salvo que sea la primera vez que nos movemos por cierto fragmento de código y tratamos de aprovechar cualquier información que nos parezca útil. Es entonces cuando descubrimos comentarios que pueden oscilar entre lo simplemente desactualizado y lo esperpéntico.
Así que, fuera con ellos. Algunos ejemplos:
To-dos olvidados. Las anotaciones To do seguramente hace meses que han dejado de tener sentido. Mienten en tanto que no tenemos ninguna referencia que les aporte significado.
¿De qué otro tipo de servicio estábamos hablando aquí hace tres meses? ¿Será que ya lo hemos cambiado?
Sería diferente si el comentario fuese mucho más preciso y detallado, de tal forma que indique con claridad el ámbito y plazo de la tarea pendiente. Algo así:
En ese caso, el comentario hace explícitos unos detalles que definen con precisión los motivos, acciones y plazos.
Comentarios olvidados. En algunos casos puede ocurrir que simplemente nos hayamos dejado comentarios olvidados. Por ejemplo, podríamos haber usado comentarios para definir las líneas básicas de un algoritmo, que es una técnica bien conocida, y ahí se habrían quedado. Todo ello también tiene que desaparecer:
Comentarios para estructurar código. Claro que puede que el algoritmo sea lo bastante complejo como para que necesitemos describir sus diferentes partes. En este caso, el mejor refactor es extraer esas partes a métodos privados con nombres descriptivos, en lugar de usar comentarios:
De este modo el código está estructurado y documentado.
Comentarios sobre valores válidos. Consideremos este código:
El comentario delimita los valores aceptables para un parámetro, pero no fuerza ninguno de ellos. Eso tenemos que hacerlo mediante una cláusula de guarda. ¿Hay una forma mejor de hacerlo?
Por supuesto: utilizar un enumerable.
Lo que permite eliminar el comentario, a la vez que tener una implementación más limpia y coherente:
Código comentado
En alguna parte he escuchado o leído algo así como “código comentado: código borrado”. El código comentado debería desaparecer. Lo más seguro es que ya nadie se acuerde de por qué estaba ese código ahí, para empezar, y por qué sigue aunque sea escondido en un comentario.
Si es necesario recuperarlo (spoiler: no lo será) siempre nos queda el control de versiones.
Excepciones: a veces se puede usar la técnica de comentar un código para desactivarlo temporalmente. En ese caso, deberíamos explicar esa decisión también en el mismo comentario. Mucho mejor que eso es utilizar alguna técnica de feature flag. Existen librerías en todos los lenguajes para gestionar feature flags, pero en muchos casos podemos introducer alguna variable que sea fácil de cambiar:
Comentarios que podríamos conservar… o no
Comentarios que explican decisiones
Los buenos comentarios deberían explicar por qué tomamos alguna decisión que no podemos expresar mediante el propio código y que, por su naturaleza, podríamos considerar como independientes de la implementación concreta que el código realiza. Es decir, no deberíamos escribir comentarios que expliquen cómo es el código, que es algo que ya podemos ver, sino que expliquen por qué es así.
Lo normal es que estos comentarios sean pocos, pero relevantes, lo cual los pone en una buena situación para realizar un mantenimiento activo de los mismos.
Obviamente, corremos el riesgo de que los comentarios se hagan obsoletos si olvidamos actualizarlos cuando sea necesario. Por eso la importancia de que no estén “acoplados” a la implementación en código.
Un ejemplo de comentario relevante podría ser este:
Este comentario es completamente independiente del código e indica una información importante que no podríamos expresar con él. Si en un momento dado cambia la legislación y debemos aplicar otra normativa, podemos cambiar el comentario.
Aunque, a decir verdad, podríamos llegar a expresarlo en código. A grandes rasgos:
Dudas razonables
Comentarios para el IDE
En aquellos lenguajes en los que el análisis estático por parte del IDE no pueda interpretar algunas cosas, añadir comentarios en forma de anotaciones puede suponer una ayuda para el IDE. En algunos casos, gracias a eso nos avisa de problemas potenciales antes de integrar los cambios.
No debería ser una práctica común, pero es un compromiso aceptable. Por ejemplo, en PHP era frecuente indicar el tipo de las propiedades de los objetos y otras variables con comentarios, ya que el lenguaje no permitía hacerlo en código.
Esto se introdujo en la versión 7.4:
En otros lenguajes, estas características estaban presentes desde mucho antes.
Las seis preguntas
El framework de las seis preguntas se utiliza en algunas disciplinas para determinar si una fuente proporciona información completa. Estas preguntas se pueden usar para decidir qué comentar en un código y qué no es necesario:
- ¿Cuándo se ha escrito el código?: Esta información la encontramos fácilmente en el sistema de control de versiones. No hay que añadirla como comentario. El único caso en que se me ocurre que podría ser útil es cuando mudamos un repositorio a un servidor diferente, ya que se puede perder la información.
- ¿Quién ha escrito el código?: Aplica lo mismo que en la pregunta anterior, es información que nos proporciona el sistema de control de versiones, de una forma mucho más precisa.
- ¿Dónde está el código?: Aplicado a la paquetización del código, básicamente es una información que o bien se declara de forma explícita, o bien el lenguaje se encarga de reportarnos en caso de errores. Por tanto, tampoco parece necesario establecerlo en un comentario.
- ¿Qué hace el código?: La respuesta corta es que el código ya dice lo que hace, pero con frecuencia eso no queda tan claro porque los nombres están mal escogidos o la estructura del código lleva a confusión. Eso podría llevarnos a plantear la necesidad de indicarlo en un comentario. Pero antes de ellos, lo apropiado sería reflexionar sobre cómo explicitar la intención de ese fragmento de código usando un buen nombre. Y, de todos modos, la mejor forma de documentar lo que hace un código es mediante un test.
- ¿Cómo hace el código lo que hace?: De nuevo, una vez que sabemos lo que hace un código, el cómo debería ser el código en sí. Ahora bien, hay algunos casos en los que es recomendable añadir comentarios. Uno de esos casos es el uso de algoritmos bien conocidos, que tienen nombre. En esa situación, es muy buena idea hacerlo explícito. Otro caso podría ser el de documentar distintos pasos en un algoritmo, aunque para ello suele ser mejor extraerlos a sus propios métodos.
- ¿Por qué hace el código lo que hace?: Finalmente, esta es una pregunta que solo podemos contestar nosotras: las personas responsables de ese conocimiento. Y esa explicación debe aparecer como comentario.
Resumen del capítulo
Los comentarios en el código tienen una utilidad limitada y, con frecuencia, se vuelven mentirosos y no resultan de ayuda para comprender lo que nuestro código hace, pudiendo incluso llevarnos a confusión si les hacemos caso.
Si introduces un comentario, debes responsabilizarte de su ciclo de vida: actualizarlo cuando cambie el código al que hace referencia. Borrarlo si ya no sirve de nada.
En lugar de usar comentarios es preferible trabajar en mejores nombres para los símbolos (variables, constantes, clases, métodos, funciones…), estructurar mejor el código en funciones o métodos que expresen su intención.
Si necesitamos documentar cómo funciona algo y cómo usarlo, es mucho mejor introducir tests, los cuales proporcionan una documentación viva.
Por otro lado, los comentarios que sí pueden permanecer suelen referirse a aspectos que no podemos expresar fácilmente con código, como puede ser explicar los motivos para hacer algo de una forma concreta. Aún así, debes asegurarte de mantenerlos al día.
Capítulo 2. El nombre de la cosa
En el que Adso y Guillermo… ejem, en el que discutimos sobre la necesidad de poner los nombres adecuados a las cosas del código, a fin de que se entienda qué demonios pasaba en aquel momento por nuestra cabeza, o la de la autora del código que tenemos que intervenir.
Probablemente en ningún lugar como el código los nombres configuran la realidad. Escribir código implica establecer decenas de nombres cada día, para identificar conceptos y procesos. Una mala elección de nombre puede condicionar nuestra forma de ver un problema de negocio. Un nombre ambiguo puede llevarnos a entrar en un callejón sin salida, ahora o en un futuro no muy lejano. Pero un nombre bien escogido puede ahorrarnos tiempo, dinero y dificultades.
Notas de la segunda edición
En este capítulo hemos cambiado los ejemplos para que los nombres originales sean mucho menos expresivos. De este modo, se entiende mejor la necesidad del refactor, y también se entiende mejor lo que hace la clase Calculator
que hemos usado como ejercicio.
Símbolos con nombres
Un trozo de código debería poder leerse como una especie de narrativa, en la cual cada palabra expresase de forma unívoca un significado. También de forma ubicua y coherente, es decir, que el mismo símbolo debería representar el mismo concepto en todas partes del código.
¿Cuándo refactorizar nombres?
La regla de oro es muy sencilla: cada vez que al leer una línea de código tenemos que pararnos a pensar qué está diciendo lo más probable sea que debamos cambiar algún nombre.
Este es un ejemplo de un código en el que nos encontramos con unos cuantos problemas de nombres, algunos son evidentes y otros no tanto:
Por supuesto, en este ejemplo hay algunos errores más aparte de los nombres. Pero hoy solo nos ocuparemos de estos. Vamos por partes.
Nombres demasiado genéricos
Los nombres demasiado genéricos requieren el esfuerzo de interpretar el caso concreto en que se están aplicando. Además, en un plano más práctico, resulta difícil localizar una aparición específica del mismo que tenga el significado deseado.
¿De dónde vienen los nombres demasiado genéricos? Normalmente, vienen de estadios iniciales del código, en los que probablemente bastaba con ese término genérico para designar un concepto. Con el tiempo, ese concepto evoluciona y se ramifica a medida que el conocimiento de negocio avanza, pero el código puede que no lo haya hecho al mismo ritmo, con lo que llega un momento en que este no es reflejo del conocimiento actual que tenemos del negocio.
Calculate… what? Exactamente, ¿qué estamos calculando aquí? El código no lo refleja. Podría ocurrir, por ejemplo, que $rate1
fuese algún tipo de descuento, $rate2
podría ser una comisión o impuestos y $val
parece claro que es algo así como el precio de tarifa de algún producto o servicio, sea lo que sea que vende esta empresa. Es muy posible que este método lo que haga sea calcular el precio final para el consumidor del producto. ¿Por qué no declararlo de forma explícita?
Vamos a revisar los distintos nombres que se están usando en el código para representar los conceptos que se manejan en esta calculadora de precios. Puesto que tenemos bastante claro que $val
es el precio del producto, podemos hacerlo explícito.
La clase Item
, que representa el producto o servicio que estamos vendiendo nos proporciona ese precio base e, igualmente, deberíamos hacerlo explícito.
Con estos cambios de nombres, debería haber quedado más claro qué es lo que está pasando.
Nombres ambiguos
Hay dos propiedades en la calculadora que tienen el mismo nombre $rate1
y $rate2
.
Técnicamente, son correctos, ya que $rate
nos sugiere un porcentaje o proporción, algo que podemos confirmar al leer los métodos en los que se aplican. Pero, ¿qué concepto de negocio representan?
Sabemos que se aplican dos, pero no sabemos qué representan. Uno de ellos se resta y el otro se suma. El que se resta, puede ser un descuento, mientras que el que se suma, podría tratarse de un impuesto o una comisión. Debería ser obvio que necesitamos clarificarlo.
Tras hablarlo con negocio, hemos llegado a la conclusión de $rate1
representa un descuento y $rate2
un impuesto. Lo adecuado, en este caso, será poner nombres explícitos.
Con este refactor ya hemos ganado mucho en expresividad, pero el término Rate se utiliza en varios nombres compuestos, por lo que no hemos terminado aún. El siguiente paso sería cambiar applyRate1
y applyRate2
por algo más descriptivo:
Aplicando este refactor hemos conseguido mucha más claridad y legibilidad. Incluso puede que hayamos hecho aflorar un bug. Si te fijas, se aplican los descuentos tras aplicar los impuestos. En muchos países, los impuestos se aplican sobre el precio final, por lo que deberíamos cambiar el orden de aplicación de los métodos applyTax
y applyDiscount
.
Esto es muy interesante porque nos demuestra como los nombres ambiguos puede llevarnos a errores de lógica. Al emplear el mismo término $rate
para referirnos a dos conceptos completamente distintos, hemos podido confundirnos y aplicar los descuentos antes de los impuestos, lo que podría llevar a problemas legales o a pérdidas económicas. Ciertamente, sería posible prevenirlo con un buen test, que nos habría indicado el error cometido, pero, y si no tenemos un test que cubra ese caso concreto, ¿cómo lo detectaríamos?
Pero es que, además, al eliminar la ambigüedad de los nombres, reducimos la dificultad y el tiempo que nos llevaría interpretar el código. No solo para nosotras, sino también para cualquier persona que pueda tener que trabajar con él en el futuro.
Nombres reutilizados en el mismo scope
Aunque ahora tenemos el código en un estado mucho mejor, todavía tenemos un aspecto que no está realmente bien. La variable $price
es actualizada constantemente y, no solo cambia de valor, sino que cambia de significado. En un momento dado, $price
es el precio base del producto, en otro es el precio con el descuento aplicado y en otro es el precio con el impuesto aplicado.
El principal inconveniente de esta forma de programar es que tiene un coste de cambio alto en el futuro. Si hubiese que introducir algún paso nuevo en el cálculo del precio final, habría que modificar todos los lugares en los que se actualiza la variable $price
. Además, el código es más difícil de comprender, ya que no es fácil saber en qué estado se encuentra $price
en cada momento.
Podrías argumentar que se trata de una variable temporal y de que, en último término, lo que buscamos es el resultado final de todas las transformaciones. Por ejemplo, podríamos hacer algo así, y ni siquiera necesitaríamos la variable $price
:
Bien, esta solución es interesante, pero a primera vista ya parece más difícil de leer. No solo eso, al igual que la solución inicial nos vamos a encontrar con problemas en caso de que en algún momento necesitemos introducir un cambio.
De entrada, sería más sencillo bautizar cada paso del cálculo con un nombre que refleje su significado. Por ejemplo:
Más allá de los nombres: cuando aparecen conceptos
Por otro lado, este tipo de patrones en los que vamos cambiando el valor de una variable con cálculos que la toman como argumento de entrada, nos debería sugerir la presencia de un concepto que estaría mejor representado con un objeto. Como podemos ver, todas las operaciones se realizan sobre un precio, por lo que podríamos encapsularlo en un objeto Price
. Y una vez que tenemos un objeto, lo más adecuado sería que éste se encargase de aplicar los descuentos y los impuestos.
Este refactor que acabamos de mostrar va más allá del alcance de este capítulo, pero nos ha servido para mostrar que cuando mejoramos los nombres, es muy fácil que afloren conceptos interesantes que no teníamos bien representandos con valores primitivos. En capítulos posteriores examinaremos cómo podemos introducir estos conceptos en nuestro código.
Tipo de palabra inadecuada
Los símbolos que, de algún modo, contradicen el concepto que representan son más difíciles de procesar, generalmente porque provocan una expectativa que no se cumple y, por tanto, debemos re-evaluar lo que estamos leyendo. Por ejemplo:
- Una acción debería representarse siempre mediante un verbo.
- Un concepto, mediante un sustantivo.
A su vez, nunca nos sobran los adjetivos para precisar el significado del sustantivo, por lo que los nombres compuestos nos ayudan a representar con mayor precisión las cosas.
Volvamos al ejemplo. Calculator
parece un buen nombre. PriceCalculator
sería aún mejor, ya que hace explícito el hecho de que calcula precios. Es un sustantivo, por lo que se deduce que es un actor que hace algo. Veámosla como interface:
Obviamente, este refactor es un poco más arriesgado. Vamos a tocar una interfaz pública, pero también es verdad que con los IDE modernos este tipo de cambios es razonablemente seguro.
finalPrice
es un sustantivo, pero en realidad ¿no representa una acción? ¿No sería mejor calculateFinalPrice
?
Por un lado, es cierto que parece más imperativo. Estamos diciendo algo así como: “Calculadora: calcula el precio final”. No deja lugar a dudas sobre lo que hace. En el lado negativo, resulta un nombre redundante, a la par que largo.
Pero antes… Volvamos un momento a la clase. PriceCalculator
, ¿es un actor o una acción? A veces tendemos a ver los objetos como representaciones de objetos del mundo real. Sin embargo, podemos representar acciones y otros conceptos con objetos en el código. Esta forma de verlo puede cambiar por completo nuestra manera de hacer las cosas.
Supongamos entonces, que consideramos que PriceCalculator
no es una cosa, sino una acción:
Tal y como está ahora, expresar ciertas cosas resulta extraño:
Pero podemos imaginarlo de otra forma mucho más fluida:
Lo que nos deja con esta interfaz:
Este cambio de nombre resulta interesante, pero también tenemos que valorarlo en su contexto. Que los objetos tengan nombres de acciones puede ser muy válido para representar casos de uso, pero no tanto para representar entidades de negocio. En este caso, si entendemos CalculatePrice
como una entidad de negocio, PriceCalculator
es un nombre mucho más adecuado.
Números mágicos
A veces no se trata estrictamente de refactorizar nombres, sino de bautizar elementos que están presentes en nuestro código en forma de valores abstractos que tienen un valor de negocio que no ha sido hecho explícito.
Poniéndoles un nombre, lo hacemos. Lo que antes era:
En la línea anterior, .21
es un número mágico. No sabemos qué significa, pero podemos intuir que se trata de un impuesto y deberíamos hacerlo explícito.
Después:
Convertir estos valores en constantes con nombre hace que su significado de negocio esté presente, sin tener que preocuparse de interpretarlo. Además, esto los hace reutilizables a lo largo de todo el código, lo que añade un plus de coherencia.
Así que, cada vez que encuentres uno de estos valores, hazte un favor y reemplázalo por una constante. Por ejemplo, los naturalmente ilegibles patrones de expresiones regulares:
O los patrones de formato para todo tipo de mensajes:
Nombres técnicos
Personalmente, me gustan poco los nombres técnicos formando parte de los nombres de variables, clases, interfaces, etc. De hecho, creo que en muchas ocasiones condicionan tanto el naming, que favorecen la creación de malos nombres.
Ya he hablado del problema de entender que los objetos en programación tienen que ser representaciones de objetos del mundo real. Esa forma de pensar nos lleva a ver todos los objetos como actores que hacen algo, cuando muchas veces son acciones.
En ocasiones, es verdad que tenemos que representar ciertas operaciones técnicas, que no todo va a ser negocio, pero eso no quiere decir que no hagamos las cosas de una manera elegante. Por ejemplo:
En cambio, en el dominio me choca ver cosas como:
En este ejemplo, el uso del verbo en pasado debería ser suficiente para entender de un vistazo que está hablando de un evento, que no es otra cosa que un mensaje que indica que algo interesante ha ocurrido.
Es cierto que incluir algunos apellidos técnicos a nuestros nombres puede ayudarnos a localizar cosas en el IDE. Pero hay que recordar que no programamos para un IDE.
Refactor de nombres
En general, gracias a las capacidades de refactor de los IDE o incluso del Buscar/Reemplazar en proyectos, realizar refactors de nombres es bastante seguro.
Variables locales en métodos y funciones. Cambiarlas no supone ningún problema, pues no afectan a nada que ocurra fuera de su ámbito.
Propiedades y métodos privados en clases. Tampoco suponen ningún problema al no afectar a nada externo a la clase.
Interfaces públicas. Aunque es más delicado, los IDE modernos deberían ayudarnos a realizarlos sin mayores problemas. La mayor dificultad me la he encontrado al cambiar nombres de clases, puesto que el IDE aunque localiza y cambia correctamente sus usos, no siempre identifica objetos relacionados, como los tests.
El coste de un mal nombre
Imaginemos un sistema de gestión de bibliotecas que, inicialmente, se creó para gestionar libros. Simplificando muchísimo, aquí tenemos un concepto clave del negocio:
Con el tiempo la biblioteca pasó a gestionar revistas. Las revistas tienen número, pero tal vez en su momento se pensó que no sería necesario desarrollar una especialización:
Y aquí comienza un desastre que solo se detecta mucho tiempo después y que puede suponer una sangría, quizá lenta pero constante, de tiempo, recursos y, en último término, dinero para los equipos y empresas.
La modificación de la clase Book
hizo que esta pasara a representar dos conceptos distintos, pero quizá se consideró que era una ambigüedad manejable: un compromiso aceptable.
Claro que la biblioteca siguió evolucionando y con el avance tecnológico comenzó a introducir nuevos tipos de objetos, como CD, DVD, libros electrónicos, y un largo etcétera. En este punto, el conocimiento que maneja negocio y su representación en el código se han alejado tanto que el código se ha convertido en una pesadilla: ¿cómo sabemos si Book
se refiere a un libro físico, a uno electrónico, a una película en DVD, a un juego en CD? Solo podemos saberlo examinando el contenido de cada objeto Book
. Es decir: el código nos está obligando a pararnos a pensar para entenderlo. Necesitamos refactorizar y reescribir.
Es cierto que, dejando aparte el contenido, todos los objetos culturales conservados en una biblioteca comparten ese carácter de objeto cultural o soporte de contenidos. CulturalObject
se nos antoja un nombre demasiado forzado, pero Media
resulta bastante manejable:
Media
representaría a los soportes de contenidos archivados en la biblioteca y que contendría propiedades como un número de registro (el id), la signatura topográfica (que nos comunica su ubicación física) y otros detalles relacionados con la actividad de archivo, préstamo, etcétera.
Pero esa clase tendría especializaciones que representan tipos de medios específicos, con sus propiedades y comportamientos propios.
Podríamos desarrollar más el conocimiento de negocio en el código, añadiendo interfaces. Por ejemplo, la gestión del préstamo:
Pero el resumen es que el hecho de no haber ido reflejando la evolución del conocimiento del negocio en el código nos lleva a tener un sobre-coste en forma de:
- El tiempo y recursos necesarios para actualizar el desarrollo a través de reescrituras.
- El tiempo y recursos necesarios para mantener el software cuando surgen problemas derivados de la mala representación del conocimiento.
- Las pérdidas por no ingresos debidos a la dificultad del software de adaptarse a las necesidades cambiantes del negocio.
Por esto, preocúpate por poner buenos nombres y mantenerlos al día. Va en ello tu salario.
Capítulo 3. Acondiciona las condicionales
En el que decidimos cómo hacer que las decisiones que el código toma sean más comprensibles y fáciles de mantener en el futuro, porque al fin y a la postre todo en esta vida es decidir. El caso es saber cuando tomar las decisiones y comprender bien sus condiciones y sus consecuencias.
Notas de la segunda edición
En este capítulo, como en otros, hemos mejorado los ejemplos y corregido algunas explicaciones para que sean más claras. Ahora la mayor parte de ejemplos tienen más sentido y ofrecen más contexto. Asímismo hemos incluido algunas propuestas nuevas.
La complejidad de decidir
Es bastante obvio que si hay algo que añade complejidad a un software es la toma de decisiones y, por tanto, las estructuras condicionales con las que la expresamos.
Estas estructuras pueden introducir dificultades de comprensión debido a varias razones:
- La complejidad de las expresiones evaluadas, sobre todo cuando se combinan mediante operadores lógicos tres o más condiciones, lo que las hace difíciles de procesar para nosotras.
-
La anidación de estructuras condicionales y la concatenación de condicionales mediante
else
, introduciendo múltiples flujos de ejecución. - El desequilibrio entre las ramas en las que una rama tiene unas pocas líneas frente a la otra que esconde su propia complejidad, lo que puede llevar a pasar por alto la rama corta y dificulta la lectura de la rama larga al introducir un nuevo nivel de indentación.
¿Cuándo refactorizar condicionales?
En general, como regla práctica, hay que refactorizar condicionales cuando su lectura no nos deja claro cuál es su significado. Esto se aplica en las dos partes de la estructura:
- La expresión condicional: qué define lo que tiene que pasar para que el flujo se dirija por una o por otra rama.
- Las ramas: las diferentes acciones que se deben ejecutar en caso de cumplirse o no la condición.
También podemos aplicar alguna de las reglas de object calisthenics:
Aplanar niveles de indentación: cuanto menos anidamiento en el código, más fácil de leer es porque indica que no estamos mezclando niveles de abstracción. El objetivo sería tener un solo nivel de indentación en cada método o función.
Eliminar else
: en muchos casos, es posible eliminar ramas alternativas, bien directamente, bien encapsulando toda la estructura en un método o función.
Vamos a ver como podemos proceder a refactorizar condicionales de una forma sistemática.
La rama corta primero
Si una estructura condicional nos lleva por una rama muy corta en caso de cumplirse y por una muy larga en el caso contrario, se recomienda que la rama corta sea la primera, para evitar que pase desapercibida. Por ejemplo, este fragmento tan feo:
Podría reescribirse así y ahora es más fácil ver la rama corta. Esto nos va a encaminar hacia otras refactorizaciones que veremos más adelante:
Return early
Si estamos dentro de una función o método y podemos hacer el retorno desde dentro de una rama es preferible hacerlo. Con eso podemos evitar la cláusula else
y hacer que el código vuelva al nivel de indentación anterior, lo que facilitará la lectura. Veámoslo aplicado al ejemplo anterior:
No hace falta mantener la variable temporal, pues podemos devolver directamente la respuesta obtenida:
Y, por último, podemos eliminar la cláusula else
y dejarlo así.
Aplicando el mismo principio, reducimos la complejidad aportada por la condicional final, retornando directamente y eliminando tanto else
como las variables temporales:
El contexto habitual de esta técnica es la de tratar casos particulares o que sean obvios en los primeros pasos del algoritmo, volviendo al flujo principal cuanto antes, de modo que solo recibe aquellos casos a los que se aplica realmente. Una variante de esta idea consiste en la introducción de las cláusulas de guarda, que veremos a continuación.
Cláusulas de guarda
En muchas ocasiones, cuando los datos tienen que ser validados antes de operar con ellos, podemos encapsular esas condiciones en forma de cláusulas de guarda. Estas cláusulas de guarda, también se conocen como aserciones, o precondiciones. Si los parámetros recibidos no las cumplen, el método o función falla, generalmente, lanzando excepciones.
Extraemos toda la estructura a un método privado:
La lógica bajo este tipo de cláusulas es que si no salta ninguna excepción, quiere decir que $parameter
ha superado todas las validaciones y lo puedes usar con confianza. La ventaja es que las reglas de validación definidas con estas técnicas resultan muy expresivas, ocultando los detalles técnicos en los métodos extraídos.
Una alternativa es usar una librería de aserciones, lo que nos permite hacer lo mismo de una forma aún más limpia y reutilizable. Si la aserción no se cumple, se tirará una excepción:
Una limitación de las aserciones que debemos tener en cuenta es que no sirven para control de flujo. Esto es, las aserciones fallan con una excepción, interrumpiendo la ejecución del programa, que así puede comunicar al módulo llamante una circunstancia que impide continuar. Por eso, las aserciones son ideales para validar precondiciones.
En el caso de necesitar una alternativa si el parámetro no cumple los requisitos, utilizaremos condicionales. Por ejemplo, si el parámetro excede los límites queremos que se ajuste al límite que ha superado, en vez de fallar:
Finalmente, ten en cuenta que el tipado ya es una guarda en sí misma, por lo que no necesitas verificar el tipo de un parámetro si ya lo has tipado correctamente.
Preferir condiciones afirmativas
Diversos estudios han mostrado que las frases afirmativas son más fáciles de entender que las negativas, por lo que siempre que sea posible deberíamos intentar convertir la condición en afirmativa ya sea invirtiéndola, ya sea encapsulándola de modo que se exprese de manera afirmativa. Con frecuencia, además, la particular sintaxis de la negación puede hacerlas poco visibles:
En uno de los ejemplos anteriores habíamos llegado a la siguiente construcción invirtiendo la condicional lo que resultó en una doble negación que puede hacerse difícil de leer:
Nosotros lo que queremos es devolver el método de pago en caso de tener uno seleccionado:
Una forma alternativa, si la condición es compleja o simplemente difícil de entender tal cual es encapsularla en un método:
Encapsula expresiones complejas en métodos o funciones
La idea es encapsular expresiones condicionales complejas en funciones o métodos, de modo que su nombre describa el significado de la expresión condicional, manteniendo ocultos los detalles escabrosos de la misma. Esto puede hacerse de forma global o por partes.
Justo en el apartado anterior hemos visto un ejemplo de esto mismo, haciendo explícito el significado de una expresión condicional difícil de leer.
Veamos otro caso en el mismo ejemplo: la extraña condicional que selecciona el método de pago en función del país de destino y el número de pedido. Posiblemente, algún apaño para resolver un problema concreto en los primeros pasos de la empresa.
Se podría encapsular la condición en un método que sea un poco más explicativo:
Y el método encapsulado sería algo así:
Encapsula ramas en métodos o funciones
Consiste en encapsular todo el bloque de código de cada rama de ejecución en su propio método, de modo que el nombre nos indique qué hace. Esto nos deja las ramas de la estructura condicional al mismo nivel y expresando lo que hacen de manera explícita y global. En los métodos extraídos podemos seguir aplicando refactors progresivos hasta que ya no sea necesario.
Este fragmento de código, que está bastante limpio, podría clarificarse un poco, encapsulando tanto las condiciones como la rama:
Veamos como:
De ese modo, la complejidad queda oculta en los métodos y el cuerpo principal se entiende fácilmente. Ya es cuestión nuestra si necesitamos seguir el refactor dentro de los métodos privados que acabamos de crear.
Equalize branches
Si hacemos esto en todas las ramas de una condicional o de un switch las dejaremos todas al mismo nivel, lo que facilita su lectura.
Reemplaza if…elseif
sucesivos con switch
En muchos casos, sucesiones de if
o if…else
quedarán mejor expresados mediante una estructura switch
. Por ejemplo, siguiendo con el ejemplo anterior, este método que hemos extraído:
Podría convertirse en algo así:
Sustituir if
por el operador ternario
A veces, un operador ternario puede ser más legible que una condicional:
Realmente las últimas líneas pueden expresarse en una sola y queda más claro:
El operador ternario tiene sus problemas, pero, en general, es una buena solución cuando queremos expresar un cálculo que se resuelve de dos maneras según una condición. Eso sí: nunca anides operadores ternarios porque su lectura entonces se complica enormemente.
De todos modos, este ejemplo concreto de código puede mejorar mucho. Si nos fijamos, el primer elemento encontrado ya nos basta como respuesta, por lo que se podría devolver inmediatamente. No necesitamos ninguna variable temporal, ni una doble condición. Este tipo de refactor lo veremos en el capítulo sobre return early.
Solo un if
por método
Christian Clausen en Five Lines of Code propone un refactor para condicionales que puede ser muy interesante. Con frecuencia, una estructura condicional indica que una función está haciendo varias cosas distintas. Por tanto, lo que propone es que cada método haga solo una cosa, y que, por tanto, solo tenga un if
que sería la primera línea. Si hay más de uno, separamos en métodos distintos.
Podría quedar más o menos así:
Resumen del capítulo
Las expresiones y estructuras condicionales pueden hacer que seguir el flujo de un código sea especialmente difícil, particularmente cuando están anidadas o son muy complejas. Mediante técnicas de extracción podemos simplificarlas, aplanarlas y hacerlas más expresivas.
Capítulo 4. Deja atrás lo primitivo
En el que se habla de que si estamos siguiendo un paradigma orientado a objetos, todo debería ser un objeto. Sí, todo.
Todos los lenguajes de programación vienen de serie con un conjunto de tipos de datos básicos que denominamos primitivos (y en algún caso escalares, cuando no son objetos): boolean, int, float, string…, que utilizamos para representar cosas y operar con ellas. La parte mala es que son tipos genéricos y, a veces, necesitaríamos algo con más significado.
Lo ideal sería poder crear nuestros propios tipos, aptos para el dominio en el que estemos trabajando e incluyendo sus propias restricciones y propiedades. Además, podrían encapsular las operaciones que les sean necesarias. ¿Te suena el concepto? Estamos hablando de value objects.
Los value objects son objetos que representan algún concepto importante en el dominio. Ya hemos hablado un montón de veces de ellos en el blog, por lo que simplemente haré un resumen. Puedes encontrar más detalles y ejemplos en este artículo de Dani Tomé.
En resumen, los value objects:
- Representan conceptos importantes o interesantes del dominio, entendido como el dominio de conocimiento que toca el código que estamos implementando o estudiando.
- Siempre son creados consistentes, de modo que si obtienes una instancia puedes tener la seguridad de que es válida. De otro modo, no se crean y se lanza una excepción.
- Los objetos nos interesan por su valor, no por su identidad, por lo que tienen que tener alguna forma de chequear esa igualdad.
- Son inmutables, o sea, su valor no puede cambiar durante su ciclo de vida. En caso de que tengan métodos mutators, estos devolverán una nueva instancia de la clase con el valor modificado.
- Encapsulan comportamientos. Los buenos value objects atraen y encapsulan comportamientos que pueden ser utilizados por el resto del código.
Los value objects pueden ser genéricos y reutilizables, como Money
, o muy específicos de un dominio.
Una aclaración que me gustaría hacer es value object es uno de los bloques de construcción en Domain Driven Design, pero el patrón de encapsular valores primitivos en objetos lo podemos (y debemos) aplicar a
Refactorizar a value objects
Refactorizar a value objects puede ser una tarea de bastante calado, ya que implica crear nuevas clases y utilizarlas en diversos puntos del código. Ahora bien, este proceso puede hacerse de forma bastante gradual. Ten en cuenta que:
- Los value objects no tienen dependencias, para crearlos solo necesitas tipos escalares o bien otros value objects.
- Los value objects se pueden instanciar allí donde los necesites, son newables.
- Normalmente, tendrás métodos para convertir los value objects a escalares, de modo que puedas utilizar sus valores con código que no puedes modificar.
Los value objects aportan varias ventajas:
- Al encapsular su validación tendrás objetos con valores adecuados que puedes usar libremente sin necesidad de validar constantemente.
- Aportarán significado a tu código, siempre sabrás cuando una variable es un precio, un email, una edad, lo que necesites.
- Te permiten abstraerte de cuestiones como formato, precisión, etc.
Propiedades múltiples para un solo concepto
Veamos un objeto típico de cualquier negocio: Customer
que da lugar a varios ejemplos clásicos de value object. Un cliente siempre suele tener un nombre, que acostumbra a ser una combinación de nombre de pila y uno o más apellidos. También tiene una dirección, que es una combinación de unos cuantos datos.
El constructor de nuestro Customer
podría ser muy complicado, y eso que no hemos incluido todos los campos:
Solemos decir que las cosas que cambian juntas deben ir juntas, pero eso también implica que las cosas que no cambian juntas deberían estar separadas. En el constructor van todos los detalles mezclados y se hace muy difícil de manejar.
Yo no sé a ti pero a mí esto me pide un builder:
Builder que podríamos usar así:
Gracias a usar el builder podemos ver que hay, al menos, dos conceptos: el nombre del cliente y su dirección. De hecho, en la dirección tendríamos también dos conceptos: la localidad y las señas dentro de esa localidad.
Vamos por partes:
Value object simple
Parece que no, pero manejamos mucha lógica en algo tan simple como un nombre. Veamos por ejemplo:
- En España usamos nombres con dos apellidos, pero en muchos otros países se suele usar un nombre con un único apellido.
- A veces necesitamos usar partes del nombre por separado, como sería el nombre de pila (“Estimada Susana”, “Sr. Pérez”). Otras veces queremos combinarlo de diferentes formas, como podría ser poner el apellido primero, lo que es útil para listados.
- Y, ¿qué pasa si queremos introducir nueva información relacionada con el nombre? Por ejemplo, el tratamiento (Sr./Sra., Estimado/Estimada, etc.).
El nombre del cliente se puede convertir fácilmente a un value object, lo que retirará cualquier lógica de la “gestión” del nombre de la clase Customer
, contribuyendo al Single Responsibility Principle y proporcionándonos un comportamiento reutilizable.
Así que podemos crear un value object sencillo:
La validación debe hacerse en el constructor, de modo que solo se puedan instanciar PersonName
correctos. Supongamos que nuestras reglas son:
- Name y FirstSurname son obligatorios y no pueden ser un string vacío.
- LastSurname es opcional.
Al incluir la validación tendremos el siguiente código:
Más adelante volveremos sobre este objeto. Ahora vamos a definir varios value objects. De momento, solo me voy a concentrar en los constructores, sin añadir ningún comportamiento, ni siquiera el método equals
ya que quiere centrarme en cómo movernos de usar escalares a estos objetos.
value object Compuesto
Para crear el VO Address
haremos algo parecido y crearemos una clase Address
para representar las direcciones de los clientes.
Sin embargo, hemos dicho que podríamos crear un value object en torno al concepto de localidad o código postal, que incluiría el código postal y la ciudad. Obviamente, esto dependerá de nuestro dominio. En algunos casos no nos hará falta esa granularidad porque simplemente queremos disponer de una dirección postal de nuestros clientes para enviar comunicaciones. Pero en otros casos puede ocurrir que nuestro negocio tenga aspectos que dependan de ese concepto, como un servicio cuya tarifa sea función de la ubicación.
La verdad es que podríamos hilar más fino y declarar un VO PostalCode
:
De modo que Locality
quedaría así:
En este caso, no consideramos que PostalCode
sea una dependencia de Locality
. Estamos hablando de tipos de datos por lo que estos objetos son newables, que es una forma de decir que se instancian a medida que se necesitan.
En fin. Volviendo a nuestro problema original de crear un objeto Address
podríamos adoptar este enfoque:
Puesto que Locality
es un VO no es necesario validarla. Además, aquí no necesitamos saber cómo se construye por lo que nos daría igual si hemos optado por un diseño u otro de la clase, ya que la vamos a recibir construida y Address
puede confiar en que funcionará como es debido.
Siempre que un objeto requiere muchos parámetros en su construcción puede ser interesante plantearse si tenemos buenas razones para organizarlos en forma de value objects, aplicando el principio de co-variación: si cambian juntos, deberían ir juntos. En este caso, $street
, $streetNumber
y $floor
pueden ir juntos, en forma de StreetAddress
porque entre los tres componen un concepto útil.
De este modo, Address se hace más simple y ni siquiera tiene que ocuparse de validar nada:
En resumidas cuentas, a medida que reflexionamos sobre los conceptos del dominio podemos percibir la necesidad de trasladar esa reflexión al código de una forma más articulada y precisa. Pero como hemos señalado antes todo depende de las necesidades de nuestro dominio. Lo cierto es que, como veremos a lo largo del artículo, cuanto más articulado tengamos el dominio, vamos a tener más capacidad de maniobra y muchísima más coherencia.
Introduciendo los value objects
Volvamos a Customer
. De momento, el hecho de introducir una serie de value objects no afecta para nada al código que tengamos, por lo que podríamos estar creando cada uno de los VO, haciendo commits, mezclando en master y desplegando sin afectar de ningún modo a la funcionalidad existente. Simplemente, hemos añadido clases a nuestra base de código y ahí están: esperando a ser utilizadas.
En este caso, tener a CustomerBuilder
nos viene muy bien pues encapsula la compleja construcción de Customer, aislándola del resto del código. Podremos refactorizar Customer
sin afectar a nadie. Empezaremos por el nombre:
Como podemos ver, para empezar el constructor ya es más simple. Además, el método fullName
puede delegarse al disponible en el objeto PersonName
, que se puede ocupar cómodamente de cualquier variante o formato particular que necesitemos a lo largo de la aplicación.
Es por esto que decimos que los value objects “atraen” comportamientos, ya que cualquier cosa que las clases usuarias necesiten puede encapsularse en el propio VO. Si necesitásemos el nombre en un formato apto para listas podríamos hacer lo siguiente:
Como tenemos un Builder que encapsula la construcción de Customer
, lo que hacemos es modificar esa construcción de acuerdo al nuevo diseño:
Fíjate que he dejado el método withName
tal y como estaba. De esta forma, no cambio la interfaz pública de CustomerBuilder
, como tampoco cambia la de Customer
salvo en el constructor, y el código que lo usa no se enterará del cambio. En otras palabras, el ejemplo anterior funcionará exactamente igual:
Por supuesto, haríamos lo mismo con el VO Address. Así queda Customer:
El método full
en Address
lo resuelvo mediante un type casting a string de sus componentes, que es una manera sencilla de disponer de su valor en un formato escalar estándar:
En este caso necesitaremos:
Y también:
Así como:
Del mismo modo que antes, modificaremos CustomerBuilder
para utilizar los nuevos objetos:
Y ya está, hemos hecho este cambio sin tener que tocar en ningún lugar más del código. Obviamente, tener un Builder de Customer nos ha facilitado muchos las cosas, lo que nos dice que es bueno tener un único lugar de instanciación de los objetos.
Beneficios
El beneficio más evidente es que las clases importantes del dominio como Customer
, quedan mucho más compactas. Hemos podido reducir ocho propiedades a dos, que son conceptos relevantes dentro de Customer
.
Por otro lado, todo lo que tiene que ver con ellos, Customer
se lo delega. Dicho de otro modo, Customer
no tiene que saber cómo se da formato a un nombre o a una dirección. Simplemente, cuando se lo piden entrega el nombre o la dirección formateados. Asimismo, cualquier otro objeto que usase PersonName
o Address
, lo hará de la misma manera.
Otra cosa interesante es que los cambios que necesitemos en el comportamiento de estas propiedades pueden aplicarse sin tocar el código de la clase, modificando los value objects, con lo cual el nuevo comportamiento se extenderá a todas las partes de la aplicación que lo utilicen.
Introduciendo nuevas features a través de los value objects
Vamos a ver cómo la nueva situación en la que nos deja el refactor nos facilita la vida en el futuro. Imaginemos que tenemos que añadir una nueva feature en la aplicación.
Es importante tratar bien a los clientes, por lo que nos han pedido incluir una propiedad de género que permita personalizar el tratamiento que utilizamos en las comunicaciones.
¿A quién pertenecería esa propiedad en nuestro modelo? En el diseño inicial tendríamos que ponerla en Customer
, pero ahora podríamos hacerlo en PersonName
. Aún mejor: no toquemos nada, o casi nada, de lo que hay.
Supongamos que Customer
tiene un método treatment
al que recurrimos para montar emails o cartas:
Primero queremos un nuevo value object, que será Gender:
Este tipo de value object será un enumerable. Representa conceptos que tienen un número limitado (numerable) de valores. El siguiente paso es hacer que la clase PersonName
implemente una interfaz PersonNameInterface
:
Y hacemos que Customer la utilice. Es un cambio pequeño, que nos permitirá usar implementaciones alternativas:
Ahora, crearemos un tipo de PersonName
que sepa algo acerca del género del nombre:
Y ahora, el último cambio, en el Builder usaremos la nueva implementación:
Esta modificación ya tiene algo más de calado, pero sigue siendo razonablemente segura. Si te fijas, solo estamos modificando comportamientos del Builder y ahí podemos ser un poco menos estrictos. Teniendo muchísimo rigor, podríamos crear un decorador de CustomerBuilder
que crease un Customer
cuyo PersonNameInterface
fuese un GenderMarkerPersonName, pero ya estaríamos entrando quizá en la sobre-ingeniería.
Fíjate que ahora Customer
es capaz de usar el tratamiento adecuado para cada cliente y realmente no lo hemos tenido que tocar. De hecho, hemos añadido la feature
añadiendo bastante código, pero con un mínimo riesgo de afectación al resto de la aplicación.
Haciendo balance
Veamos algunos números. Empezamos con dos clases: Customer
y CustomerBuilder
. ¿Sabes cuántas tenemos ahora? Nada menos que nueve, además de una interfaz. La parte buena es que hemos afectado muy poco al código restante, tan solo en el último paso, cuando hemos tenido que introducir una nueva feature
. Pero aquí ya no estamos hablando de refactor.
Sin embargo, nuestro dominio tiene ahora muchísima flexibilidad y capacidad de cambio.
Sería bastante fácil, por ejemplo, dar soporte a los múltiples formatos de dirección postal que se usan en todo el mundo, de modo que nuestro negocio está mejor preparado para expandirse internacionalmente, puesto que solo tendríamos que introducir una interfaz y nuevos formatos a medida que los necesitemos, sin tener que cambiar el core
del dominio. Puede sonar exagerado, pero estos pequeños detalles pueden ser un dolor de cabeza enorme si seguimos el modelo con el que empezamos. Algo tan pequeño puede ser la diferencia entre un código y un negocio que escale fácilmente o no.
Capítulo 5. Refactoriza a Enumerables
Que trata sobre cómo gestionar aquellos tipos de datos que tienen valores limitados, algo que solemos encontrar en prácticamente todos los dominios. Pero también algo que queremos mejorar respecto a la edición anterior.
En este capítulo profundizaremos en una idea que ya apuntamos en un capítulo anterior: refactorizar introduciendo enumerables. Los enumerables son tipos de datos que cuentan con un número finito de valores posibles.
Notas de la segunda edición
Esta segunda edición incluye un plot-twist respecto a la primera en la que recomendábamos usar enumerables. Pero los enumerables son un poco quedarse a mitad de camino si estamos trabajando orientados a objetos.
El problema con los enumerables es que siempre vamos a tener que estar mirando su estado cuando queramos añadirles comportamiento. Esto es, una instancia de un enumerable puede tener cualquiera de los valores posibles de ese concepto. Si quiero añadir un comportamiento que dependa de ese valor, tendré que preguntarle al enumerable cada vez para asegurarme que sea el correcto. Si tengo muchos valores y muchas variantes de comportamiento, el objeto será complejo y difícil de mantener.
Este cambio ha sido muy influenciado por mi relectura del libro Five lines of code, de Christian Clausen, pero, de hecho, tiene todo el sentido del mundo desde la perspectiva de orientación a objetos.
Dejo aquí el capítulo original de la primera edición, con algún retoque, y añado una gran addenda para explicar cómo refactorizar a objetos.
Enumérame otra vez
Supongamos el típico problema de representar los estados de alguna entidad o proceso. Habitualmente usamos un string o un número para ello. La primera opción ayuda a que el valor sea legible por humanos, mientras que la representación numérica puede ser más compacta aunque más difícil de entender.
En cualquier caso, el número de estados es limitado y lo podemos contar. El problema es garantizar la consistencia de los valores que utilicemos para representarlos, incluso entre distintos sistemas.
Y aquí es dónde pueden ayudarnos los Enumerables.
Los Enumerables se modelan como Value Objects. Esto quiere decir que un objeto representa un valor y se encarga de mantener su consistencia, disfrutando de todas las ventajas que señalamos en el capítulo anterior.
En la práctica, además, podemos hacer que los Enumerables nos permitan una representación semántica en el código, aunque internamente transporten valores abstractos, como códigos numéricos, necesarios para la persistencia en base de datos, por ejemplo.
De escalar a enumerable
Empecemos con un caso más o menos típico. Tenemos una entidad con una propiedad que puede tener dos valores, como ‘activo’ y ‘cancelado’. Inicialmente, la modelamos con un string, que es como se va a guardar en base de datos, y confiamos en que lo sabremos manejar sin mayores problemas en el código. ¿Qué podría salir mal?
Para empezar, cuando tenemos una variable o propiedad de tipo string, tenemos infinitos valores potenciales de esa variable o propiedad y tan solo queremos usar dos de ellos. Así que, cuando necesitemos usarlo, tendremos que asegurarnos de que solo consideraremos esos dos strings concretos. En otras palabras: tendremos que validarlos cada vez.
Habitualmente también haremos alguna normalización, como poner el string en minúsculas o mayúsculas, para simplificar el proceso de comparación y asegurarnos una representación coherente.
Pero esto debería hacerse cada vez que vamos a utilizar esta variable o propiedad, o al menos, siempre que sepamos que su origen no es confiable, como el input de usuario o una petición a la API o cualquier fuente de datos que no esté en un estado conocido.
En lugar de esto, deberíamos usar un Value Object, como vimos en el capítulo anterior. Realmente, lo único que hace un poco especiales a los Enumerables es el hecho de que el número de valores posibles es limitado lo que nos permite usar algunas técnicas interesantes.
Nuestra primera iteración es simple, definimos una clase Status
que contiene un valor string.
Personalmente, me gusta añadir un método __toString
a los VO para poder hacer el type cast si lo necesito.
Tenemos que definir cuáles son los valores aceptables para este VO, lo cual podemos hacer mediante constantes de clase. Definiremos una para cada valor válido y una extra que será un simple array que los agrupa, lo que nos facilitará la validación.
En el ejemplo he puesto los valores aceptados en español y los nombres de las constantes en inglés, que es como haremos referencia a ellos en el código por cuestiones de lenguaje de dominio. Esta diferencia podría darse cuando, por ejemplo, necesitamos interactuar con un sistema legacy en el que esos valores están representados en español y sería más costoso cambiarlo que adaptarnos.
En cualquier caso, lo que importa es que vamos a tener una representación en código de esa propiedad y los valores concretos son detalles de implementación que en unos casos podremos elegir y en otros no.
Por otro lado, está el tema de las constantes públicas. Es una cuestión de conveniencia, ya que nos puede permitir acceder a los valores estándar en momentos en los que no podemos usar objetos mediante llamada estática.
Nuestro siguiente paso debería ser implementar la validación que nos garantice que podemos instanciar solo valores correctos.
Como se puede ver, es muy sencillo, ya que simplemente comprobamos si el valor aportado está en la lista de valores admitidos. Pero, como es un string, podríamos tener algún problema en caso de que nos pasen el dato con alguna mayúscula. En estos casos, no está de más, realizar una normalización básica. Tampoco se trata de arreglar el input externo, pero sí de prevenir alguno de los errores habituales.
Para terminar con lo básico, necesitamos un método para comprobar la igualdad, así como un método para obtener su valor escalar si fuese preciso.
Y, con esto, ya tenemos un Enumerable.
Bonus points
Hay algunas cosas interesantes que podemos hacer con los enumerables, a fin de que resulten más cómodos y útiles.
Por ejemplo, podemos querer tener named constructors que hagan más explícita la forma de creación.
Puesto que son pocos valores, podríamos permitirnos tener named constructors para crear directamente instancias con un valor determinado:
Con esto, podemos hacer privado el constructor standard, usando así la clase:
Enumerables y cambios de estado
Supongamos que una cierta propiedad de una entidad se puede modelar con un enumerable de n elementos con la característica de que solo puede cambiar en una cierta secuencia.
Con frecuencia nos encontramos que esta gestión de estados la realiza la entidad. Sin embargo, podemos delegar en el enumerable buena parte de este comportamiento.
A veces esta secuencia es lineal, indicando que la entidad pasa, a lo largo de su ciclo de vida, por los diferentes estados en un orden prefijado. Por ejemplo, un contrato puede pasar por los estados pre-signed, signed, extended and finalized, pero siempre lo hará en ese orden, por lo que es necesario comprobar que no es posible asignar a un contrato un estado nuevo que sea “incompatible” con el actual.
Otras veces, el orden de la secuencia puede variar, pero solo se puede pasar de unos estados determinados a otros. Por ejemplo, un post de un blog, puede pasar de draft a ready to review, pero no directamente a published, mientras que desde ready to review puede volver a draft, si el revisor no lo encuentra adecuado, o avanzar a published si está listo para ver la luz.
Como hemos dicho, este tipo de reglas de negocio pueden encapsularse en el propio enumerable simplificando así el código de la entidad. Hay muchas formas de hacer esto y en casos complejos necesitaremos hacer uso de otros patrones.
Veamos el ejemplo lineal. Supongamos un ContractStatus
que admite tres estados que se suceden en una única secuencia. Podemos tener un método en el Enumerable para avanzar un paso el estado:
Este ejemplo nos permite hacer avanzar el estado de un objeto contrato de este modo. Recuerda que al ser un Value Object el método nos devuelve una nueva instancia de ContractStatus
.
Otra situación interesante se produce cuando necesitamos reasignar el estado del contrato de forma directa. Por ejemplo, debido a errores o tal vez por necesidades de sincronización entre distintos sistemas. En esos caso, podríamos tener (o no) reglas de negocio que permitan ciertos cambios y prohíban otros.
Para nuestro ejemplo vamos a imaginar que un contrato puede volver atrás un paso (de signed a pre-signed y de finalized a signed) o avanzar un paso, como en el método forward
.
Esta implementación es bastante tosca, pero creo que representa con claridad la intención. El método changeTo
nos permite pasarle un nuevo ContractStatus
y nos lo devuelve si el cambio es válido o bien lanza una excepción.
En esencia, el método changeTo
valida que el estado se pueda cambiar teniendo en cuenta el estado actual. La idea de fondo es aplicar el principio Tell, don't ask
, de modo que no le preguntemos al contrato por su estado, ni a ContractStatus
por su valor, si no que le decimos que cambie a un nuevo estado si es posible. En caso de fallo, ya tomaremos nosotros las medidas necesarias.
Enumerables como traductores
¿Y qué ocurre si tenemos que interactuar con distintos sistemas que representan el mismo significado con distintos valores? Podría ocurrir que uno de los sistemas lo hiciese con enteros de modo que necesitamos alguna traducción.
Un enfoque pragmático, cuando las combinaciones de valores/versiones son reducidas, sería incorporar esa capacidad al propio Enumerable, mediante un named constructor específico y un método para obtener esa versión del valor.
Añadiendo rigor al enumerable
Aunque la solución que acabamos de ver resulta práctica en ciertos casos, lo cierto es que no es precisamente rigurosa al mezclar responsabilidades.
Nos hace falta algún tipo de traductor:
Plot-twist: no uses enumerables y abraza el polimorfismo
Si bien usar enumerables es una mejora sustancial con respecto a usar primitivos, la programación orientada a objetos nos ofrece algo mejor: el polimorfismo. Recordemos: el polimorfismo es la propiedad que nos permite enviar el mismo mensaje a distintos objetos a fin de que cada uno actúe como le corresponda.
Tomemos el ejemplo anterior:
ContractStatus
se convierte en una interfaz, que será implementada por las clases: Presigned
, Signed
y Finalized
.
Aquí tenemos un ejemplo de una de esas clases.
Necesitaremos una factoría para instanciar los objetos adecuados si, por ejemplo, cargamos la información desde una base de datos, o
Capítulo 6. Refactoriza de single return a return early
En el que se trata un problema que viene del principio de los tiempos de la programación, cuando teníamos cosas como GOTO, números de línea y direcciones de memoria arbitrarias a las que saltar. Pero aún nos quedan algunos hábitos.
En el blog ya hemos hablado del patrón clásico Single Exit Point y cómo acabó derivando en single return. También algún momento de esta guía de refactor hemos hablado también del return early. Ahora vamos a retomarlos conjuntamente porque seguramente nos los encontraremos más de una vez.
Notas de la segunda edición
Este capítulo cambia de lugar porque me he dado cuenta de que es un refactor relativamente menor y rompe por la mitad los dos capítulos dedicados a la distribución de responsabilidades.
Hasta este punto del libro estamos hablando de refactorings motivados por problemas o defectos del código que podemos identificar incluso visualmente, sin preocuparnos mucho de qué hace el código relacionado. Pero los capítulos siguientes ya requieren que pensemos en cuales son los papeles que juegan los distintos objetos en el código.
Lo primero será saber de qué estamos hablando:
Single return
Se trata de que en cada método o función solo tengamos un único return
, a pesar de que el código pueda tener diversos caminos que nos permitirían finalizar en otros momentos.
Obviamente, si el método solo tiene un camino posible tendrá un solo return
.
Si el método tiene dos caminos, caben dos posibilidades:
Uno de los flujos se separa del principal, hace alguna cosa y vuelve de forma natural al tronco para terminar lo que tenga que hacer.
Uno de los flujos se separa para resolver la tarea de una manera alternativa, por lo que podría devolver el resultado una vez obtenido. Sin embargo, si se sigue el patrón single return, hay que forzar que el flujo vuelva al principal antes de retornar.
Si el método tiene más de dos caminos se dará una combinación de las posibilidades anteriores, es decir, algunas ramas volverán de forma natural al flujo principal y otras podrían retornar por su cuenta.
En principio, la ventaja del Single Return es poder controlar con facilidad que se devuelve el tipo de respuesta correcta, algo que sería más difícil si tenemos muchos lugares con return
. Pero la verdad es que explicitando return types es algo de lo que ni siquiera tendríamos que preocuparnos.
En cambio, el mayor problema que tiene Single Return es que fuerza la anidación de condicionales y el uso de else
hasta extremos exagerados, lo que provoca que el código sea especialmente difícil de leer. Lo peor es que eso no se justifica por necesidades del algoritmo, sino por la gestión del flujo para conseguir que solo se pueda retornar en un punto.
El origen de esta práctica podría ser una mala interpretación del patrón Single Exit Point de Djkstra, un patrón que era útil en lenguajes que permitían que las llamadas a subrutinas y sus retornos pudieran hacerse a líneas arbitrarias, con la famosa sentencia GOTO
. El objetivo de este patrón era asegurar que se entrase a una subrutina en su primera línea y se volviese siempre a la línea siguiente a la llamada.
Early return
El patrón early return consiste en salir de una función o método en cuanto sea posible, bien porque se ha detectado un problema (fail fast), bien porque se detecta un caso especial que se maneja fuera del algoritmo general o por otro motivo.
Dentro de este patrón encajan cosas como las cláusulas de guarda, que validan los parámetros recibidos y lanzan una excepción si no son correctos. También se encuentran aquellos casos particulares que necesitan un tratamiento especial, pero que es breve o inmediato.
De este modo, al final nos queda el algoritmo principal ocupando el primer nivel de indentación y sin elementos que nos distraigan.
El mayor inconveniente es la posible inconsistencia que pueda darse en los diferentes returns en cuanto al tipo o formato de los datos, algo que se puede controlar fácilmente forzando un return type.
Por otra parte, ganamos en legibilidad, ya que mantenemos bajo control el anidamiento de condicionales y los niveles de indentación. Además, al tratar primero los casos especiales podemos centrar la atención en el algoritmo principal de ese método.
Ejemplo básico
Hace un par de años comencé a practicar un ejercicio para estudiar algoritmos y estructuras de datos, reproduciéndolos en PHP usando metodología TDD. El código visto ahora está un poco pobre, pero me viene bien porque he encontrado varios ejemplos de single return y otros puntos de mejora.
En primer lugar, vamos a ver un caso en el que podemos refactorizar un single return muy evidente, pero también uno que no lo es tanto:
El primer paso es invertir la condicional, para ver la rama más corta en primer lugar. Aquí se ve claramente que cada una de las ramas implica un cálculo diferente de la misma variable, que es lo que se va a devolver al final. El else se introduce porque no queremos que el flujo pase por el bloque grande si $source
tiene un único elemento o ninguno, ya que no tendríamos necesidad de ordenarlo.
Por esa razón, podríamos simplemente finalizar y retornar el valor de $source
como que ya está ordenado. Al hacer esto, también podemos eliminar el uso de la variable temporal $sorted
que es innecesaria.
Con este arreglo el código ya mejora mucho su legibilidad gracias a que despejamos el terreno tratando el caso especial y dejando el algoritmo principal limpio.
Pero vamos a ir un paso más allá. El bucle for
contiene una forma velada de single return en forma de estructura if...else
que voy a intentar explicar.
El algoritmo quicksort se basa en hacer pivotar los elementos de la lista en torno a su mediana, es decir, al valor que estaría exactamente en la posición central de la lista ordenada. Para ello, se calcula la mediana de forma aproximada y se van comparando los números para colocarlos en la mitad que les toca: bien por debajo o bien por encima de la mediana.
Para eso se compara cada número con el valor mediano para ver sucesivamente si es igual, menor o mayor, con lo que se añade a la sublista correspondiente y se van ordenando esas sublistas de forma recursiva.
En este caso las cláusulas else
tienden a hacer más difícil la lectura y, aunque la semántica es correcta, podemos hacerlo un poco más claro.
Como ya sabrás, podemos forzar la salida de un bucle con continue
:
Y, aunque en este caso concreto no es especialmente necesario, esta disposición hace que la lectura del bucle sea más cómoda. Incluso es más fácil reordenarlo y que exprese mejor lo que hace:
Otro ejemplo
En este caso es un Binary Search Tree, en el que se nota que no tenía muy claro el concepto de return early o, al menos, no lo había aplicado hasta sus últimas consecuencias, por lo que el código no mejora apenas:
Empecemos mejorando el método insert
:
Que podría quedar así:
Al método insertNew
le sobra indentación:
Empezamos aplicando el return early una vez:
Y una segunda vez:
Otro lugar que necesita un arreglo es el método findParent
. Aquí hemos usado return early, pero no habíamos sabido aprovecharlo:
Al hacerlo, nos queda un código más limpio:
Todos estos refactors se pueden hacer sin riesgo con las herramientas de Intentions (comando + return) de PHPStorm que nos ofrece la inversión de if/else (flip), y la separación de flujos (split workflows) cuando son posibles. En todo caso, estas clases estaban cubiertas por tests y estos siguen pasando sin ningún problema.
Finalmente, arreglamos findNode
, que estaba así:
Y quedará así:
Capítulo 6. Aplica Tell, Don’t Ask y la Ley de Demeter
En el que tratamos sobre la redistribución de responsabilidades. Porque en un sistema orientado a objetos la pregunta no es: ¿cómo hay que hacer esto?, sino: ¿quién debería estar haciendo esto? Y, por tanto, lo que buscamos es la forma de mover las responsabilidades y comportamientos a los objetos a los que pertenecen naturalmente.
Hasta ahora, hemos trabajado refactors muy orientados a mejorar la expresividad del código y a la organización de unidades de código. En este capítulo vamos a trabajar en cómo mejorar las relaciones entre objetos.
Los principios de diseño nos proporcionan criterios útiles tanto para guiarnos en el desarrollo como para evaluar código existente en el que tenemos que intervenir. Vamos a centrarnos en dos principios que son bastante fáciles de aplicar y que mejorarán la inteligibilidad y la posibilidad de testear nuestro código. Se trata de Tell, Don’t Ask y la Ley de Demeter.
Primero haremos un repaso y luego los veremos en acción.
Tell, don’t ask
La traducción de este enunciado a español sería algo así como “Ordena, no preguntes”. La idea de fondo de este principio es que cuando queremos modificar un objeto basándose su propio estado, no es buena idea preguntarle por su estado (ask), hacer el cálculo y cambiar su estado si fuera preciso. En su lugar, lo propio sería encapsular ese proceso en un método del propio objeto y decirle (tell) que lo realice él mismo.
Dicho en otras palabras: cada objeto debe ser responsable de su estado.
Veamos un ejemplo bastante absurdo, pero que lo deja claro.
Supongamos que tenemos una clase Square
que representa un cuadrado y queremos poder calcular su área.
Si aplicamos el principio Tell, Don’t Ask, el cálculo del área estaría en la clase Square
:
Mejor, ¿no? Veamos por qué.
En el dominio de las figuras geométricas planas, el área o superficie es una propiedad que tienen todas ellas y que, a su vez, depende de otras que son su base y su altura, que en el caso del cuadrado coinciden. La función para determinar el área ocupada por una figura plana depende de cada figura específica.
Posiblemente, estés de acuerdo en que al modelar este comportamiento lo pondríamos en la clase de cada figura desde el primer momento, lo que seguramente nos llevaría a una interfaz.
La primera razón es que toda la información necesaria para calcular el área está en la clase, por lo que tiene todo el sentido del mundo que el conocimiento preciso para calcularla también esté allí. Se aplica el principio de Cohesión y el principio de Encapsulamiento, manteniendo juntos los datos y las funciones que procesan esos datos.
Una segunda razón es más pragmática: si el conocimiento para calcular el área está en otro lugar, como un servicio, cada vez que necesitemos incorporar una nueva clase al sistema, tendremos que modificar el servicio para añadirle ese conocimiento, rompiendo el principio Abierto/Cerrado.
En tercer lugar, el testing se simplifica. Es fácil hacer tests de las clases que representan las figuras. Por otro lado, el testing de las otras clases que las utilizan también se simplifica. Normalmente, esas clases usarán el cálculo del área como una utilidad para llevar a cabo sus propias responsabilidades que es lo que queremos saber si hacen correctamente.
Siguiendo el principio Tell, Don’t Ask movemos responsabilidades a los objetos a los que pertenecen.
Ley de Demeter
La Ley de Demeter1 también se conoce como Principio de mínimo conocimiento y, más o menos, dice que un objeto no debería conocer la organización interna de los otros objetos con los que colabora.
Siguiendo la Ley de Demeter, como veremos, un método de una clase solo puede usar los objetos a los que conoce. Estos son:
- La propia clase, de la que puede usar todos sus métodos.
- Objetos que son propiedades de esa clase.
- Objetos creados en el mismo método que los usa.
- Objetos pasados como parámetros a ese método.
La finalidad de la ley de Demeter es evitar el acoplamiento estrecho entre objetos. Si un método usa un objeto, contenido en otro objeto que ha recibido o creado, implica un conocimiento que va más allá de la interfaz pública del objeto intermedio.
En el ejemplo, el método calculatePrice
obtiene el descuento aplicable llamando a un método de Product
, que devuelve otro objeto al cual le preguntamos sobre el descuento.
¿Qué objeto es este y cuál es su interfaz? Podemos suponer que se trata de un objeto Promotion
, pero eso es algo que sabemos nosotros, no el código. Este conocimiento excesivo nos dice que estamos ante una violación de la Ley de Demeter.
Puedes ponerlo así, pero sigue siendo el mismo problema:
Una justificación que se aduce en ocasiones es que puesto que sabes qué clase de objeto devuelve el objeto intermedio, entonces puedes saber su interfaz. Sin embargo, lo que conseguimos es una dependencia oculta de entre dos objetos que no tienen una relación directa. Esto es:
El objeto A usa el objeto B para obtener y usar el objeto C.
El objeto A depende del objeto C, pero no hay nada en A que nos diga que existe esa dependencia.
Es posible aplicar varias soluciones. La más adecuada depende de varios factores.
Encapsular en nuevos métodos
En algunos casos, se trataría de aplicar el principio Tell, Don’t Ask. Esto es. A veces, la responsabilidad de ofrecernos una cierta respuesta encajaría en el objeto intermedio, por lo que podríamos encapsular la cadena de llamadas. Veámoslo con un ejemplo similar:
En este caso, resulta razonable pensar que la estructura del precio de un producto es algo propio del producto, y los usuarios del objeto no tienen por qué conocerla. En un primer paso, aplicamos la Ley de Demeter haciendo que el objeto Price
sea el que obtiene el precio base, sin que la calculadora tenga que saber de dónde se obtiene.
En ese caso, Product
utiliza su colaborador Family
para obtener el valor que devolver en basePrice
.
En el segundo paso, aplicamos Tell, Don't Ask
, ya que realmente estamos pidiéndole cosas a Product
que puede hacer por sí mismo.
Reasignación de responsabilidades
El primer ejemplo sobre descuentos es un poco más delicado que el que acabamos de ver:
No está tan claro que las posibles promociones formen parte de la estructura de precios de un producto. Las promociones son seguramente una responsabilidad de Marketing y los productos y precios son de Ventas. Puesto que no queremos tener dos razones para cambiar Producto, lo lógico es que las promociones estén en otra parte.
Dicho de otro modo, no tiene mucho sentido que un producto conozca cuáles son las promociones que se le aplican en el contexto de una campaña de marketing, que son puntuales y limitadas en el tiempo, mientras que la estructura de precio es algo permanente.
Tiene más sentido que la responsabilidad de las promociones esté en otro lugar. Podría ser algo así:
La clase que contiene el método calculatePrice
tendría un colaborador que le proporciona los descuentos disponibles para un producto.
En resumidas cuentas, el código que incumple la Ley de Demeter tratando con objetos que no conoce directamente puede estar revelando problemas más profundos en el diseño y que hay que solucionar. Estos problemas se manifiestan en un acoplamiento fuerte entre objetos que tienen una relación escasa entre sí.
Capítulo 8. Dónde poner el conocimiento
En el que se continúan ideas expuestas en el Capítulo 6 acerca de cómo reasignar responsabilidades en el código.
En anteriores capítulos hemos propuesto refactorizar código a Value Objects o aplicar principios como la ley de Demeter o Tell, don’t ask para mover código a un lugar más adecuado. En este capítulo vamos a analizarlo desde un punto de vista más general.
Solemos decir que refactorizar tiene que ver con el conocimiento y el significado. Fundamentalmente, porque lo que hacemos es aportar significado al código con el objetivo de que este represente de una manera fiel y dinámica el conocimiento cambiante que tenemos del negocio del que nos ocupamos.
En el código de una aplicación tenemos objetos que representan alguna de estas cosas del negocio:
- Conceptos, ya sea en forma de entidades o de value objects. Las entidades representan conceptos que nos interesan por su identidad y tienen un ciclo de vida. Los value objects representan conceptos que nos interesan por su valor.
- Relaciones entre esos conceptos, que suelen representarse en forma de agregados y que están definidas por las reglas de negocio.
- Procesos que hacen interactuar los conceptos conforme a reglas de negocio también.
Uno de los problemas que tenemos que resolver al escribir código y al refactorizarlo es dónde poner el conocimiento y, más exactamente, las reglas de negocio.
Si hay algo que caracteriza al legacy es que el conocimiento sobre las reglas de negocio suele estar disperso a lo largo y ancho del código, en los lugares más imprevisibles y representado de las formas más dispares. El efecto de refactorizar este código es, esperamos, llegar a trasladar ese conocimiento al lugar donde mejor nos puede servir.
Principios básicos
Principio de abstracción. Benjamin Pierce formuló el principio de abstracción en su libro Types and programming languages:
Each significant piece of functionality in a program should be implemented in just one place in the source code. Where similar functions are carried out by distinct pieces of code, it is generally beneficial to combine them into one by abstracting out the varying parts.
DRY. Por su parte, Andy Hunt y David Thomas, en The Pragmatic Programmer, presentan una versión de este mismo principio que posiblemente te sonará más: Don’t Repeat Yourself:
Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.
En esencia, la idea que nos interesa recalcar es que cada regla de negocio estará representada en un único lugar y esa representación será la de referencia para todo el código.
Los principios que hemos enunciado se centran en el carácter único de la representación, pero no nos dicen dónde debe residir la misma. Lo cierto es que es un tema complejo, pues es algo que puede admitir varias interpretaciones y puede depender del estado de nuestro conocimiento actual del negocio.
Buscando dónde guardar el conocimiento
En los objetos a los que pertenece
El patrón GRASP Information Expert nos viene perfecto aquí. Este patrón nos dice que un objeto debería ser la fuente de verdad de todo lo que tiene que ver consigo mismo. Expresándolo en otras palabras, quiere decir que un objeto debe ser capaz de realizar todos los comportamientos que le sean propios, dentro del contexto de nuestra aplicación. Para ello no debería necesitar exponer sus propiedades internas o estado.
Por tanto, cuando preguntamos a un objeto sobre su estado y realizamos acciones basadas en la respuesta, lo suyo debería ser encapsular esas acciones en forma de comportamientos del objeto. Para ello, podemos seguir el principio Tell, don’t ask. Esto es, en lugar de obtener información de un objeto para operar con ella y tomar una decisión sobre ese objeto, le pedimos que lo haga él mismo y nos entregue un resultado si es adecuado para el contexto.
En ese sentido, los value objects y entidades, de los que hemos hablado tantas veces, son lugares ideales para encapsular conocimiento. Veamos un ejemplo:
Supongamos que en nuestro negocio estamos interesados en ofrecer ciertos productos o ventajas a usuarios cuya cuenta de correo pertenezca a ciertos dominios, por ejemplo perteneciente a su cuenta corporativa. El correo electrónico es, pues, un concepto importante del negocio y lo representamos mediante un value object:
En un determinado servicio verificamos que el dominio del correo electrónico del usuario se encuentra dentro de la lista de dominios beneficiados de este tratamiento especial.
El problema aquí es que el servicio tiene que ocuparse de obtener el dominio de la dirección de correo, cosa que no tendría que ser de su incumbencia. La clase Email
nos está pidiendo a gritos convertirse en la experta en calcular la parte del dominio del correo:
Nos basta con esto para hacer más expresivo nuestro servicio, pero aún podemos hacerlo mejor:
Reglas de negocio como Specification
El ejemplo anterior es una primera aproximación a cómo mover el conocimiento. En este caso no se trata tanto de la regla de negocio como de un requisito para poder implementarla.
Podríamos decir que la regla de negocio implica distintos conocimientos. En términos de negocio nuestro ejemplo se enunciaría como “todos los clientes cuyo dominio de correo esté incluido en la lista tienen derecho a tal ventaja cuando realicen tal acción”. Técnicamente, implica saber sobre usuarios y sus emails, y saber extraer su dominio de correo para saber si está incluido en tal lista.
Desde el punto de vista del negocio la regla relaciona clientes, seleccionados por una característica, con una ventaja que les vamos a otorgar.
Ese conocimiento se puede encapsular en una Specification, que no es más que un objeto que puede decidir si otro objeto cumple una serie de condiciones. El objeto en que estamos interesadas es Customer
y para el contexto de esta regla, nos interesa poder preguntarle por su dominio de correo.
Ahora el conocimiento de la regla de negocio se encuentra en un solo lugar y lo puedes reutilizar allí donde lo necesites2
No solo eso, sino que incluso nos permite escribir mejor el servicio al expresar las relaciones correctas: en este caso la regla de negocio se basa en una propiedad de los clientes y no de los pedidos, aunque luego se aplique el resultado a los pedidos o al cálculo de su importe.
Sobre el patrón Specification puedes encontrar más información en este artículo
Lecturas recomendadas
Donde se exponen algunos libros, artículos y vídeos que pueden ser un buen recurso para aprender de autores más cualificados y prestigiosos que el de este libro, cosa que tampoco es tan difícil de encontrar.
Libros recomendados
Five Lines of Code, de Christian Clausen. Soy consciente de que el prólogo por el inefable Robert C. Martin puede echar para atrás a más de una, pero el libro de Clausen merece una lectura. El autor propone una metodología de refactoring sencilla y progresiva. Parte de una serie de reglas, como la que da título al libro entre otras, para llevarnos de la mano aplicando distintos refactors seguros, por lo que no sería necesario disponer de tests. Con esto, nos ayuda a entender cuál es problema de ese código, por qué y cómo arreglarlo. Para ello nos propone un par de proyectos en TypeScript, lenguaje que toma elementos de otros muchos lenguajes, por lo que no es muy difícil realizar los ejercicios propuestos y llevarlos a tus propios proyectos.
Refactoring, de Martin Fowler y Kent Beck. Se trata de un libro de referencia. Es difícil de leer de un tirón porque buena parte de su contenido es un catálogo de técnicas, pero el capítulo sobre code smells es posiblemente el más iluminador de todos. Existen dos ediciones de este libro. En la primera, el refactoring era un concepto todavía en exploración y hay algunos apéndices en los que se trata esa cuestión. En la segunda edición, los ejemplos están en Javascript, lo que posiblemente los hace más accesibles.
99 bottles of OOP, de Sandi Metz, no es estrictamente un libro sobre refactoring porque su objetivo es enseñar diseño orientado a objetos. Sin embargo, plantea el proceso comenzando por un ejemplo procedural y nos guía en el camino de convertirlo en un programa orientado a objetos, paso a paso y sin romper la funcionalidad. Creo que el término que mejor encaja para eso es refactoring. Además, tienes una edición del libro para PHP, Ruby y JS. Sandi Metz es una de mis autoras favoritas, así que recomiendo a ciegas cualquier cosa que publique, y sus charlas son fantásticas.
Blogs y otros recursos online
Refactoring.guru es un sitio web que, en parte, reproduce muchos elementos de los libros de M. Fowler, algo que en su día les llevó a ciertos problemas relacionados con la propiedad intelectual. El caso es que esta web nos presenta un catálogo de refactorings y code smells muy apañado, con numerosos ejemplos, explicaciones, esquemas e ideas. La verdad es que mola bastante.
The talking bit. El origen de este libro es mi blog personal, que está bastante lleno de contenido técnico relacionado sobre todo con testing, diseño de software y, por supuesto, refactoring. Y está en español, que de recursos en inglés estamos surtidas.
Vídeos
Workflows of refactoring es la charla de Martin Fowler en la que explica cuando se hace el refactoring.
The real secret to refactoring siempre merece la pena seguir el material de David Farley.
Refactoring práctico The Talking Bit tiene un canal en Youtube, en el que voy poniendo, cuando me siento suficientemente inspirado y tengo silencio en casa, vídeos en los que explico procesos de refactoring, TDD, etc. Son un apoyo al material publicado en el blog porque en algún momento pensé que los artículos escritos no conseguían transmitir el proceso sobre el terreno. Como punto característico, no se trata precisamente de shorts, prácticamente son sesiones de live coding (a veces corto algún trocito o edito algún error), por lo que tienen una duración larga. Básicamente, soy yo haciendo ejercicios y pensando en voz alta.
Refactoring con calisthenics Esta es otra serie de vídeos en las que aplico las reglas de calistenia de Jeff Way como forma de dirigir el refactor de un código. En concreto de un ejemplo adaptado por Emily Bache, a partir de otro puesto por Fowler en el libro de Refactoring.
Notas
1El nombre viene del proyecto donde se usó por primera vez.↩
2Una objeción que se puede poner a este código es que instanciamos la Specification. Normalmente, lo mejor sería inyectar en el servicio una factoría de Specification para pedirle las que necesitemos y que sea la factoría la que gestione sus posibles dependencias.↩