Prólogo
El software se está comiendo el mundo. Esta famosa frase de Marc
Andreessen es una de mis citas favoritas, casi un meme que revela el
estado actual de las cosas. Nunca antes en la historia de la humanidad ha
habido tanto software, ni se ha encargado de tantas cosas. Para los que
vivimos en ciudades, no hay casi nada a nuestro alrededor que no esté
gestionado por software. Cada año se delega más control en los artefactos
de software. Y con la llegada explosiva y disruptiva de la inteligencia
artificial, esta tendencia no hace sino acentuarse: las IA con las que
interactuamos también son software.
Soy de la generación que fue programadora antes que usuaria de
software. Empecé a programar a los 16 años, con pequeños programas. A
los 18, empecé a trabajar en sistemas más grandes y me enfrenté a la
motivación básica de este libro, la razón por la que es tan bienvenido e
incluso necesario: el software, ese software del que dependen cada vez
más cosas, está escrito por humanos. Es código. Y la calidad de ese código
se refleja directamente en la calidad del software, su mantenibilidad, vida
útil, coste, rendimiento...
Con aquellos primeros sistemas, programados por equipos muy pequeños
de dos o tres personas, aprendí por las malas por qué es ventajoso tener
un código limpio. Y ojalá hubiera tenido un libro como éste para guiarme
en aquella época, porque me habría ahorrado mucho tiempo.
Esto no significa que la relevancia del libro se quedara atrás en aquellos
tiempos prehistóricos de la informática, ni que su público sean
programadores novatos a los que hay que enseñar lo básico. Todo lo
contrario.
En este libro encontrarás, al estilo de las recetas de un libro de cocina,
formas sencillas de evitar un sinfín de trampas y errores o escollos
comunes en el código. En muchos casos, la receta en sí no es la parte
valiosa de cada capítulo, sino que plantea y discute un tema concreto,
proporcionando una base para que pensemos en cómo resolver esa
cuestión en nuestro código y evaluemos la limpieza de nuestra solución.
El estilo de Contieri es extremadamente sencillo y directo, tan claro como
nos gustaría que fuera nuestro código. Hay ejemplos de código en cada
"receta" para eliminar cualquier duda que podamos tener sobre las
situaciones para aplicarlas correctamente.
Puede parecer que la limpieza y claridad del código son problemas y
responsabilidades únicamente de los programadores. Pero en realidad,
los problemas con el código empiezan mucho antes, en la fase de diseño, y
se extienden a la asignación de recursos, las políticas de desarrollo, la
gestión de proyectos y equipos, las fases de mantenimiento y evolución.....
Creo que la mayoría de los profesionales de la industria del software
pueden beneficiarse de este libro porque ilustra muy bien y explica un
gran número de problemas comunes del código, el material del que está
hecho el software... el software que se está comiendo el mundo.
Es tentador pensar que escribir código es cosa del pasado y que la
inteligencia artificial generativa y los grandes modelos de lenguaje se
encargarán de producir código sin intervención humana. Basándome en
los ejemplos que veo a diario, esto todavía no es posible. La cantidad de
"alucinaciones" (errores básicos, problemas de interpretación,
vulnerabilidades y problemas de mantenimiento) que presenta el código
escrito por las IAs lo dificulta. Sin embargo, estamos claramente en una
etapa de transición. Y durante esta etapa, prosperarán los centauros
tecnológicos, con programadores experimentados que supervisen,
corrijan y mejoren el código producido por los sistemas automatizados.
Mientras estos ojos humanos tengan que leer y mantener el código, es
esencial que sea código limpio, como enseña este libro.
Carlos E. Ferro
Lic. en Ciencias de la computación
Ingeniero de Software Senior en Quorum Software
Buenos Aires
20 de junio de 2023
Prefacio
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y
comentarios: translation-feedback@[Link]
El código está en todas partes, desde el desarrollo web a los contratos
inteligentes, los sistemas integrados, las cadenas de bloques, el sistema de
software de a bordo de James Webb, los robots quirúrgicos y muchos
otros ámbitos. El software se está apoderando efectivamente del mundo, y
actualmente asistimos al auge de las herramientas profesionales de
generación de código de inteligencia artificial. Eso significa que el código
limpio es más importante que nunca. Mientras sigues trabajando en bases
de código propietario o de código abierto cada vez más grandes, el código
limpio es la forma de mantenerlo fresco y listo para evolucionar.
A quién va dirigido este libro
Este libro te ayuda a identificar los problemas más comunes en una base
de código y pone de relieve las consecuencias de estos problemas, y en
última instancia te ayuda a evitarlos con recetas fáciles de seguir. Es un
valioso recurso que puede ayudar enormemente a programadores,
revisores de código, arquitectos y estudiantes a mejorar sus habilidades
con el código y los sistemas existentes.
Cómo está organizado este libro
Este libro consta de 25 capítulos. Cada capítulo comienza con algunos
principios y fundamentos que muestran las ventajas del código limpio,
sus consecuencias y sus inconvenientes cuando se aplica incorrectamente.
El primer capítulo habla de la única regla rectora del código limpio:
mapea las entidades del mundo real 1:1 con tu diseño. Esta regla sirve de
base de la que pueden derivarse todos los demás principios.
Dentro de cada capítulo, encontrarás varias recetas organizadas
temáticamente con herramientas y consejos para cambiar tu código. El
propósito de cada receta es ayudarte a realizar cambios positivos y
mejoras en tu situación actual. Junto con las recetas y los ejemplos,
también se te presentarán varios principios, heurísticos y reglas de diseño
de software. Las recetas tienen ejemplos de código en varios lenguajes de
programación, ya que el código limpio no es una propiedad de uno solo de
ellos. Muchos libros de refactorización se basan en un único lenguaje y los
autores actualizan las nuevas ediciones utilizando el lenguaje
de programación de moda. Este libro es agnóstico en cuanto a lenguajes
de programación, y la mayoría de las recetas se aplican a muchos
lenguajes (excepto cuando se indica lo contrario).
Debes leer el código como pseudocódigo, aunque la mayor parte se
ejecute tal cual. Cuando tengo que decidir entre legibilidad y rendimiento,
siempre elijo la legibilidad. Proporciono definiciones de terminología
común a lo largo del libro, pero también puedes encontrarlas todas en
el Glosario de Términos del libro.
Qué necesitas para utilizar este libro
Para ejecutar los ejemplos de código, necesitarás un entorno de trabajo
como las cajas de arena de O'Reilly o Replit. Te animo a que traduzcas los
ejemplos de código a tu lenguaje de programación favorito. Hoy en día
puedes hacerlo gratis con generadores de código de inteligencia artificial.
He utilizado herramientas como GitHub Copilot, OpenAI Codex, Bard,
ChatGPT y muchas más para ayudarme a escribir los ejemplos de código
de este libro. Utilizar estas herramientas me ha permitido usar más de 25
lenguajes diferentes en este libro, aunque no sea un experto en muchos
de ellos.
Accede a este libro en formato digital
Este Libro de cocina de código limpio ofrece acceso gratuito a una edición
en línea, siempre disponible y en la que se pueden realizar búsquedas,
en [Link]
Convenciones utilizadas en este libro
En este libro se utilizan las siguientes convenciones tipográficas:
Cursiva
Indica nuevos términos, URL, direcciones de correo electrónico,
nombres de archivo y extensiones de archivo.
Constant width
Se utiliza en los listados de programas, así como dentro de los
párrafos para referirse a elementos del programa como nombres de
variables o funciones, bases de datos, tipos de datos, variables de
entorno, sentencias y palabras clave.
Constant width bold
Muestra comandos u otros textos que deben ser tecleados
literalmente por el usuario.
Constant width italic
Muestra el texto que debe sustituirse por valores proporcionados
por el usuario o por valores determinados por el contexto.
CONSEJO
Este elemento significa un consejo o sugerencia.
NOTA
Este elemento significa una nota general.
ADVERTENCIA
Este elemento indica una advertencia o precaución.
Utilizar ejemplos de código
El material complementario (ejemplos de código, ejercicios, etc.) se puede
descargar en [Link]
Si tienes una pregunta técnica o un problema al utilizar los ejemplos de
código, envía un correo electrónico a bookquestions@[Link].
Este libro está aquí para ayudarte a hacer tu trabajo. En general, si se
ofrece código de ejemplo con este libro, puedes utilizarlo en tus
programas y documentación. No es necesario que te pongas en contacto
con nosotros para pedirnos permiso, a menos que estés reproduciendo
una parte importante del código. Por ejemplo, escribir un programa que
utilice varios trozos de código de este libro no requiere permiso. Vender o
distribuir ejemplos de los libros de O'Reilly sí requiere permiso.
Responder a una pregunta citando este libro y el código de ejemplo no
requiere permiso. Incorporar una cantidad significativa de código de
ejemplo de este libro en la documentación de tu producto sí
requiere permiso.
Agradecemos la atribución, pero en general no la exigimos. Una
atribución suele incluir el título, el autor, la editorial y el ISBN. Por
ejemplo "Clean Code Cookbook por Maximiliano Contieri (O'Reilly).
Copyright 2023 Maximiliano Contieri".
Si crees que el uso que haces de los ejemplos de código no se ajusta al uso
legítimo o al permiso concedido anteriormente, no dudes en ponerte en
contacto con nosotros en permissions@[Link].
Aprendizaje en línea O'Reilly
NOTA
Durante más de 40 años, O'Reilly Media ha proporcionado formación
tecnológica y empresarial, conocimientos y perspectivas para ayudar a las
empresas a alcanzar el éxito.
Nuestra red única de expertos e innovadores comparten sus
conocimientos y experiencia a través de libros, artículos y nuestra
plataforma de aprendizaje online. La plataforma de aprendizaje en línea
de O'Reilly te ofrece acceso bajo demanda a cursos de formación en
directo, rutas de aprendizaje en profundidad, entornos de codificación
interactivos y una amplia colección de textos y vídeos de O'Reilly y de más
de 200 editoriales. Para más información, visita [Link]
Cómo contactar con nosotros
Dirige tus comentarios y preguntas sobre este libro a la editorial:
O'Reilly Media, Inc.
1005 Gravenstein Highway Norte
Sebastopol, CA 95472
800-889-8969 (en Estados Unidos o Canadá)
707-829-7019 (internacional o local)
707-829-0104 (fax)
support@[Link]
[Link]
Tenemos una página web para este libro, donde se enumeran erratas,
ejemplos y cualquier información adicional. Puedes acceder a esta página
en [Link]
Para obtener noticias e información sobre nuestros libros y cursos,
visita [Link]
Encuéntranos en LinkedIn: [Link]
Síguenos en Twitter: [Link]
Míranos en YouTube: [Link]
Agradecimientos
Este libro está dedicado a mi mujer, María Virginia, que siempre me ha
querido y apoyado, y a mis queridas hijas, Malena y Miranda, y a mis
padres, Juan Carlos y Alicia.
Tengo una gran deuda de gratitud con Máximo Prieto y Hernán Wilkinson
por sus valiosas ideas y conocimientos, que han contribuido en gran
medida a las ideas presentadas en este libro. También estoy agradecido a
mis colegas de Ingeniería de Software por compartir sus ideas y a mis
compañeros profesores de Ciencias Exactas de la Universidad de Buenos
Aires por compartir sus conocimientos y experiencia a lo largo de los
años.
Por último, me gustaría expresar mi gratitud a los revisores técnicos
Luben Alexandrov, Daniel Moka y Carlos E. Ferro y a mi editora Sara
Hunter, cuya orientación y consejos han mejorado enormemente este
libro.
Capítulo 1. Código limpio
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y
comentarios: translation-feedback@[Link]
Cuando Martin Fowler definió la refactorización en su libro Refactoring:
Improving the Design of Existing Code, mostró los puntos fuertes y las
ventajas, así como las razones que hay detrás de la refactorización.
Afortunadamente, después de más de dos décadas, la mayoría de los
desarrolladores conocen el significado de la refactorización y los olores de
código. Los desarrolladores se enfrentan a diario a la deuda tecnológica y
la refactorización se ha convertido en una parte esencial del desarrollo de
software. En su libro fundacional, Fowler utilizó las refactorizaciones
para abordar los olores de código. Este libro recorre algunos de ellos en
forma de recetas semánticas para mejorar tus soluciones.
1.1 ¿Qué es el olor a código?
Un olor a código es síntoma de un problema. La gente tiende a pensar que
la presencia de olores de código es una prueba de que toda la entidad
necesita ser desmontada y reconstruida. Éste no es el espíritu de la
definición original. Los olores de código son simplemente indicadores de
oportunidades de mejora. Un olor a código no te dice necesariamente qué
va mal; te está diciendo que prestes especial atención.
Las recetas de este libro ofrecen algunas soluciones a esos síntomas. Como
en cualquier libro de cocina, las recetas son opcionales y los olores del
código son directrices y heurísticas, no reglas rígidas. Antes de aplicar
cualquier receta a ciegas, debes comprender los problemas y evaluar el
coste y los beneficios de tu propio diseño y código. Un buen diseño implica
equilibrar las directrices con consideraciones prácticas y contextuales.
1.2 ¿Qué es la refactorización?
Volviendo al libro de Martin Fowler, da dos definiciones
complementarias:
Refactorización (sustantivo): cambio realizado en la estructura interna del
software para hacerlo más fácil de entender y más barato de modificar sin
cambiar su comportamiento observable.
Refactorizar (verbo): reestructurar el software aplicando una serie de
refactorizaciones sin cambiar su comportamiento observable.
Las refactorizaciones fueron inventadas por William Opdyke en su tesis
doctoral de 1992, "Refactoring Object-Oriented Frameworks", y se
popularizaron después del libro de Fowler. Han evolucionado desde la
definición de Fowler. La mayoría de los IDE modernos admiten
refactorizaciones automáticas. Éstas son seguras y realizan cambios
estructurales sin modificar el comportamiento del sistema. Este libro tiene
muchas recetas con refactorizaciones automáticas y seguras, y además
también incluye refactorizaciones semánticas. Los refactorizadores
semánticos no son seguros, ya que pueden cambiar parte del
comportamiento del sistema. Debes aplicar las recetas con
refactorizaciones semánticas con cuidado, ya que pueden romper tu
software. Indicaré si una receta tiene refactorización semántica en las
recetas aplicables. Si tienes una buena cobertura de código de
comportamiento, puedes estar seguro de que no romperás escenarios
empresariales importantes. No debes aplicar recetas de refactorización al
mismo tiempo que corriges un defecto o desarrollas una nueva
característica.
La mayoría de las organizaciones modernas tienen suites de cobertura de
pruebas sólidas en sus conductos de integración continua/entrega
continua. Consulta Software Engineering at Google de Titus Winters et
al. (O'Reilly 2020) para averiguar si tienes estas suites de cobertura de
pruebas.
1.3 ¿Qué es una receta?
Utilizo el término receta a la ligera. Una receta es un conjunto de
instrucciones para crear o modificar algo. Las recetas de este libro
funcionan mejor si entiendes el espíritu de la receta para poder aplicarla
con tus propios sabores. Otros libros de recetas de esta serie son más
concretos, con soluciones paso a paso. Para utilizar las recetas de este
libro, tendrás que traducirlas a tu lenguaje de programación y diseñar la
solución. La receta es un vehículo para enseñarte a comprender un
problema, identificar las consecuencias y mejorar tu código.
1.4 ¿Por qué Código Limpio?
El código limpio es fácil de leer, comprender y mantener. Está bien
estructurado, es conciso y utiliza nombres significativos para variables,
funciones y clases. También sigue las buenas prácticas y los patrones de
diseño y favorece la legibilidad y el comportamiento por encima del
rendimiento y los detalles de implementación.
El código limpio es muy importante en todos los sistemas en evolución en
los que se realizan cambios a diario. Sigue siendo especialmente relevante
en determinados entornos en los que no es posible implementar
actualizaciones con la rapidez deseada. Esto incluye sistemas embebidos,
sondas espaciales, contratos inteligentes, aplicaciones móviles y muchas
otras aplicaciones.
Los libros clásicos de refactorización, los sitios web y los IDE se centran en
refactorizaciones que no cambian el comportamiento del sistema. Este
libro tiene algunas recetas para escenarios así, como los
renombramientos seguros. Pero también encontrarás varias recetas
relacionadas con refactorizaciones semánticas en las que cambias la
forma de resolver algunos problemas. Tendrás que entender el código, los
problemas y la receta para hacer los cambios apropiados.
1.5 Legibilidad, rendimiento o ambos
Este libro trata sobre código limpio. Algunas de sus recetas no son las más
eficaces. Elijo la legibilidad sobre el rendimiento cuando está en
conflicto. Por ejemplo, he dedicado un capítulo entero(Capítulo 16) a la
optimización prematura para resolver problemas de rendimiento sin
pruebas suficientes.
Para las soluciones de rendimiento de misión crítica, la mejor estrategia
es escribir código limpio, cubrirlo con pruebas y luego mejorar los cuellos
de botella utilizando las reglas de Pareto. El principio de Pareto aplicado
al software establece que si solucionas el 20% de los cuellos de botella
críticos, mejorarás el rendimiento de tu software en un 80%. Si mejoras el
20% del mal rendimiento, es probable que aumente la velocidad del
sistema en un 80%.
Este método te disuade de hacer cambios prematuros de optimización que
carezcan de escenarios basados en pruebas, porque eso dará lugar a
pocas mejoras y dañará el código limpio.
1.6 Tipos de software
La mayoría de las recetas de este libro están dirigidas a sistemas backend
con reglas de negocio complejas. El simulador que vas a construir a partir
del Capítulo 2 es perfecto para ello. Dado que las recetas son agnósticas al
dominio, también puedes utilizar la mayoría de ellas para el desarrollo
del frontend, bases de datos, sistemas embebidos, blockchains y muchos
otros escenarios. También hay recetas específicas con ejemplos de código
para UX, frontend, contratos inteligentes y otros dominios específicos
(consulta la Receta 22.7, "Ocultar errores de bajo nivel a los usuarios
finales", por ejemplo).
1.7 Código generado por máquina
¿Necesitas código limpio ahora que hay muchas herramientas disponibles
para el código generado por ordenador? La respuesta en 2023 es: sí. Más
que nunca. Hay muchos asistentes comerciales de codificación de
software. Sin embargo, (todavía) no tienen el control total; son copilotos y
ayudantes, y los humanos siguen siendo los que toman las decisiones de
diseño.
En el momento de escribir este libro, la mayoría de las herramientas
comerciales y de inteligencia artificial escriben soluciones anémicas y
algoritmos estándar. Pero son increíblemente útiles cuando no recuerdas
cómo se hace una pequeña función y son prácticas para traducir entre
lenguajes de programación. Las he utilizado mucho mientras escribía este
libro. No domino los más de 25 lenguajes que he utilizado en las recetas.
He traducido y probado varios fragmentos de código en distintos idiomas
con muchas herramientas de ayuda. Te invito a que tú también utilices
todas las herramientas disponibles para traducir algunas de las recetas de
este libro a tu idioma favorito. Las herramientas están aquí para
quedarse, y los futuros desarrolladores serán centauros tecnológicos:
mitad humanos, mitad máquinas.
1.8 Consideraciones sobre los nombres
a lo largo del libro
A lo largo del libro, utilizo indistintamente los siguientes términos :
Métodos/funciones/procedimientos
Atributos/variables de instancia/propiedades
Protocolo/comportamiento/interfaz
Argumentos/colaboradores/parámetros
Funciones anónimas/cierres/lambdas
Las diferencias entre ellas son sutiles y a veces dependen del idioma.
Añado una nota cuando es necesario para aclarar el uso.
1.9 Patrones de diseño
Este libro asume que el lector tiene una comprensión básica de los
conceptos de diseño orientado a objetos. Algunas de las recetas de este
libro se basan en patrones de diseño populares, incluidos los descritos en
el libro de patrones de diseño "Gang of Four". Otras recetas presentan
patrones menos conocidos, como el objeto nulo y el objeto método.
Además, este libro incluye explicaciones y orientaciones sobre cómo
sustituir patrones que ahora se consideran antipatrones, como el
patrón singleton en la Receta 17.2, "Sustitución de Singletons".
1.10 Paradigmas del lenguaje de
programación
Según David Farley
La obsesión de nuestra industria por los lenguajes y las herramientas ha
perjudicado a nuestra profesión. Esto no significa que no se puedan
conseguir avances en el diseño de lenguajes, pero la mayor parte del
trabajo en diseño de lenguajes parece concentrarse en cosas equivocadas,
como avances sintácticos en lugar de estructurales.
Los conceptos de código limpio que se presentan en este libro pueden
aplicarse a diversos paradigmas de programación. Muchas de estas ideas
tienen sus raíces en la programación estructurada y la programación
funcional, mientras que otras proceden del mundo orientado a objetos.
Estos conceptos pueden ayudarte a escribir un código más elegante y
eficiente en cualquier paradigma.
Utilizaré la mayoría de las recetas en lenguajes orientados a objetos y
construiré un simulador (denominado MAPPER) utilizando objetos como
metáforas de entidades del mundo real. Hago referencia a MAPPER con
frecuencia a lo largo del libro. Muchas recetas te llevarán a pensar en
código declarativo y de comportamiento (véase el Capítulo 6, "Código
declarativo") en lugar de código de implementación.
1.11 Objetos frente a clases
La mayoría de las recetas de este libro hablan de objetos y no de clases
(aunque hay un capítulo entero sobre clasificación: véase el Capítulo 19,
"Jerarquías"). Por ejemplo, la Receta 3.2 se titula "Identificar la esencia de
tus objetos" en lugar de "Identificar la esencia de tu clase". Este libro habla
de utilizar objetos para mapear objetos del mundo real.
La forma de crear estos objetos es accidental; puedes utilizar la
clasificación, la creación de prototipos, las fábricas, la clonación, etc. El
Capítulo 2 trata de la importancia de mapear tus objetos y del imperativo
de modelar cosas que puedas ver en el mundo real. Para crear objetos,
muchos lenguajes utilizan clases, que son artefactos y no son evidentes
en el mundo real. Las necesitas si utilizas un lenguaje de clasificación.
Pero no son el objetivo principal de las recetas.
1.12 Modificabilidad
El código limpio no consiste sólo en garantizar que tu software funcione
correctamente, sino también en facilitar su mantenimiento y
evolución. De nuevo, según Modern Software Engineering de Dave
Farley, debéis ser expertos en aprender y hacer que el software esté
preparado para el cambio. Éste es un gran reto para la industria
tecnológica, y espero que este libro te ayude a mantenerte al día de estos
avances.
Capítulo 2. Establecimiento de los
axiomas
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y
comentarios: translation-feedback@[Link]
2.0 Introducción
He aquí una definición común de software:
Las instrucciones que ejecuta un ordenador, en contraposición al
dispositivo físico en el que se ejecutan (el "hardware").
El software se define por su opuesto: todo lo que no es hardware. Ésta no
es exactamente una buena definición de lo que es realmente el software.
He aquí otra definición popular:
Software, instrucciones que indican a un ordenador lo que debe hacer. El
software comprende todo el conjunto de programas, procedimientos y
rutinas asociados al funcionamiento de un sistema informático. El término
se acuñó para diferenciar estas instrucciones del hardware, es decir, los
componentes físicos de un sistema informático. Un conjunto de
instrucciones que dirige el hardware de un ordenador para que realice una
tarea se denomina programa, o programa de software.
Hace muchas décadas, los desarrolladores de software se dieron cuenta
de que el software es mucho más que instrucciones. A lo largo de este
libro, pensarás en el comportamiento del sistema y llegarás a darte cuenta
de que el objetivo principal del software es:
Imitar algo que ocurre en una realidad posible.
Esta idea vuelve a los orígenes de los lenguajes de programación
modernos como Simula.
SIMULA
Simula fue el primer lenguaje de programación orientado a objetos que
incorporó la clasificación. Su nombre indicaba claramente que el objetivo de la
construcción de software era crear un simulador. Esto sigue siendo así en la
mayoría de las aplicaciones informáticas actuales.
En ciencia, se construyen simuladores para comprender el pasado y
prever el futuro. Desde los tiempos de Platón, los seres humanos han
intentado construir buenos modelos de la realidad. Puedes definir el
software como la construcción de un simulador con el acrónimo MAPPER:
Modelo: Parcial Abstracto y Programable Explicación de la Realidad
Este acrónimo aparecerá con frecuencia a lo largo del libro. Vamos a
sumergirnos en lo que constituye MAPPER.
2.1 ¿Por qué es un modelo?
Un modelo es el resultado de ver un determinado aspecto de la realidad a
través de una lente y una perspectiva específicas, utilizando un
paradigma concreto. No es la verdad última e inmutable, sino la
comprensión más exacta que tienes en ese momento basada en tus
conocimientos actuales. El objetivo de un modelo de software, como el de
cualquier otro modelo, es predecir el comportamiento del mundo real.
MODELO
Un modelo explica el tema que describe utilizando conceptos intuitivos o
metáforas. El objetivo final de un modelo es la comprensión de cómo funciona
algo. Según Peter Naur, "Programar es construir teoría y modelos".
2.2 ¿Por qué es abstracto?
El modelo surge de la suma de las partes. Y no puedes entenderlo
completamente observando los componentes aislados. El modelo se basa
en contratos y comportamientos, y éstos no detallan necesariamente
cómo debes hacer que sucedan las cosas.
2.3 ¿Por qué es programable?
Debes ejecutar tu modelo en un simulador que reproduzca las
condiciones deseadas, que podría ser un modelo Turing (como los
ordenadores comerciales modernos), un ordenador cuántico
(ordenadores del futuro) o cualquier otro tipo de simulador capaz de
seguir el ritmo de la evolución del modelo. Puedes programar el modelo
para que responda a tus acciones de determinadas maneras y luego
observar cómo evoluciona por sí solo.
MODELO TURING
Un ordenador basado en el modelo de Turing es una máquina teórica capaz de
realizar cualquier tarea computable para la que pueda escribirse un conjunto de
instrucciones, o algoritmo. La máquina de Turing se considera el fundamento
teórico de la informática moderna, y sirve de modelo para el diseño y análisis de
ordenadores y lenguajes de programación reales.
2.4 ¿Por qué es parcial?
Para modelizar el problema que te interesa, sólo considerarás un aspecto
parcial de la realidad. Es habitual en los modelos científicos simplificar
ciertos aspectos que no son relevantes para aislar un problema. Cuando
realizas experimentos científicos, necesitas aislar determinadas variables
y fijar el resto para comprobar las hipótesis.
En tu simulador, no podrás modelar toda la realidad, sólo una parte
relevante de ella. No necesitas modelar todo el objeto de observación (es
decir, el mundo real), sólo el comportamiento interesante. Muchas de las
recetas de este libro abordan el problema de sobrediseñar modelos
incluyendo detalles innecesarios.
2.5 ¿Por qué es explicativo?
El modelo debe ser lo suficientemente declarativo como para observar su
evolución y ayudarte a razonar y predecir el comportamiento de la
realidad que estás modelando. Debe ser capaz de explicar lo que hace y
cómo se comporta. Muchos algoritmos modernos de aprendizaje
automático no proporcionan información sobre cómo llegan a sus valores
de salida (a veces incluso tienen alucinaciones), pero los modelos deben
ser capaces de explicar lo que han hecho, aunque no revelen los pasos
concretos dados para conseguirlo.
EXPLICAR
Aristóteles dijo que "explicar es encontrar las causas". Según él, todo fenómeno o
acontecimiento tiene una causa o serie de causas que lo producen o determinan.
El objetivo de la ciencia es identificar y comprender las causas de los fenómenos
naturales y, a partir de ahí, predecir cómo se comportarán en el futuro.
Para Aristóteles, "explicar" consistía en identificar y comprender todas estas
causas y cómo interactúan entre sí para producir un fenómeno concreto.
"Predecir", en cambio, se refiere a la capacidad de utilizar este conocimiento de
las causas para predecir cómo se comportaría un fenómeno en el futuro.
2.6 ¿Por qué se trata de la realidad?
El modelo tiene que reproducir las condiciones que se dan en un entorno
observable. El objetivo final es predecir el mundo real, como cualquier
simulación. En este libro oirás hablar mucho de la realidad, el mundo real
y las entidades del mundo real. El mundo real será tu fuente última de
verdad.
2.7 Inferir las reglas
Ahora que tienes un punto de partida sobre lo que es el software, puedes
empezar a deducir buenas prácticas de modelado y diseño. Los principios
MAPPER aparecerán a lo largo de las recetas del libro.
En los capítulos siguientes, seguirás descubriendo principios, heurísticas,
recetas y reglas para construir excelentes modelos de software a partir del
sencillo axioma que aquí se presenta: Modelo: Abstracto Parcial y
Programable que Explica la Realidad. La definición práctica de software
para este libro es "un simulador que hace honor al acrónimo MAPPER".
AXIOMA
Un axioma es una afirmación o proposición que se da por verdadera sin
necesidad de prueba. Te permite construir un marco lógico para el
razonamiento y la deducción, estableciendo un conjunto de conceptos y
relaciones fundamentales que pueden utilizarse para deducir otras verdades.
2.8 El único principio de diseño de
software
Si construyes todo el paradigma para el diseño de software sobre una
única regla mínima, puedes mantener la sencillez y hacer modelos
excelentes. Ser minimalista y axiomático significa que puedes derivar un
conjunto de reglas a partir de una única definición:
El comportamiento de cada elemento forma parte de la arquitectura en la
medida en que ese comportamiento puede ayudarte a razonar sobre el
sistema. El comportamiento de los elementos encarna cómo interactúan
entre sí y con el entorno. Esto forma parte claramente de nuestra definición
de arquitectura y tendrá un efecto sobre las propiedades que presente el
sistema, como su rendimiento en tiempo de ejecución.
Bass et al., Arquitectura de software en la práctica, 4ª edición
Uno de los atributos de calidad más infravalorados del software es ser
predecible. Los libros suelen enseñarte que el software debe ser rápido,
fiable, robusto, observable, seguro, etc. Ser predecible rara vez es una de
las cinco principales prioridades de diseño. Como experimento mental,
intenta imaginar el diseño de software orientado a objetos siguiendo un
solo principio (como se ilustra en la Figura 2-1): "Cada objeto del dominio
debe estar representado por un único objeto en el modelo computable y
viceversa". Después, intenta derivar todas las reglas de diseño, heurísticas
y recetas de esa única premisa para que tu software sea predecible
siguiendo las recetas de este libro.
Figura 2-1. La relación entre los objetos del modelo y las entidades del mundo real es de 1 a 1
El problema
Llegarás a comprender leyendo en estas recetas de código limpio que la
mayoría de las implementaciones de lenguajes utilizadas en la industria
ignoran la regla del axioma único, lo que causa enormes problemas. La
mayoría de los lenguajes contemporáneos están diseñados para resolver
dificultades de implementación que surgieron en la construcción de
software hace tres o cuatro décadas, cuando los recursos eran escasos y
los cálculos debían ser optimizados por el programador. Ahora, estos
problemas están presentes en muy pocos dominios. Las recetas de este
libro te ayudarán a reconocer, comprender y tratar estos problemas.
Modelos al rescate
Al construir modelos de cualquier tipo, es importante simular las
condiciones que se dan en el mundo real. Puedes seguir cada elemento de
interés en la simulación y estimularla para observar si cambian de la
misma manera que en el mundo real. Los meteorólogos utilizan modelos
matemáticos para predecir y pronosticar el tiempo, y muchas disciplinas
científicas se basan en simulaciones. La física busca modelos unificadores
para comprender y predecir las reglas del mundo real. Con la llegada del
aprendizaje automático, también se construyen modelos opacos para
visualizar el comportamiento en la vida real.
La importancia de la biyección
En matemáticas, una biyección es una función que es uno a uno, lo que
significa que asigna cada elemento del dominio a un elemento único del
rango y cada elemento del rango puede remontarse a un elemento único
del dominio. En otras palabras, una biyección es una función que
establece una correspondencia uno a uno entre los elementos de dos
conjuntos.
Un isomorfismo, en cambio, es un tipo de correspondencia más
fuerte entre dos estructuras matemáticas que preserva la estructura de
los objetos relacionados. En concreto, un isomorfismo es una función
biyectiva que conserva las operaciones de las estructuras. Una biyección
es una correspondencia unívoca entre dos conjuntos.
En el ámbito del software, siempre tendrás uno y sólo un objeto que
represente a una entidad del mundo real. Veamos qué ocurre si no
cumples los principios de la biyección.
Casos comunes que violan la biyección
Aquí tienes cuatro casos comunes que violan el principio de biyección.
Caso 1
Tienes un objeto en tu modelo computable para representar más de una
entidad del mundo real. Por ejemplo, muchos lenguajes de programación
modelan medidas algebraicas utilizando sólo la magnitud escalar. Esto es
lo que ocurre en este caso, como se ilustra en la Figura 2-2.
Puedes representar 10 metros y 10 pulgadas (dos entidades
completamente distintas en el mundo real) mediante un único
objeto(el número 10).
Podrías sumarlos, haciendo que el modelo demuestre que
el número 10 (que representa 10 metros) más el número
10 (que representa 10 pulgadas) es igual al número 20 (que
representa quién sabe qué).
Figura 2-2. El número 10 representa más de una entidad del mundo real
La biyección se rompe y esto genera problemas que no siempre se captan
a tiempo. Al tratarse de un problema semántico, el error suele producirse
mucho tiempo después del fallo, como en el famoso caso del Orbitador
Climático de Marte.
ORBITADOR CLIMÁTICO DE MARTE
El Mars Climate Orbiter fue una sonda espacial robótica lanzada por la NASA en
1998 con el objetivo de estudiar el clima y la atmósfera marcianos. La misión
fracasó debido a un problema con el sistema de guía y navegación de la nave.
Los propulsores de la nave estaban programados para utilizar unidades de
fuerza métricas, mientras que el equipo de control en tierra utilizaba unidades
de fuerza inglesas. Este error hizo que la nave se acercara demasiado a la
superficie del planeta, y se destruyó al entrar en la atmósfera marciana. El
problema del Mars Climate Orbiter fue un fallo en la coordinación y conversión
de las unidades de medida, que provocó un error catastrófico en la trayectoria
de la nave. La sonda explotó al mezclar diferentes unidades de medida. Fue un
gran contratiempo para la NASA y costó a la agencia 125 millones de dólares. El
fracaso también provocó una serie de cambios en la NASA, incluida la creación
de una nueva oficina de seguridad y garantía de la misión (véase la Receta 17.1,
"Hacer explícitas las suposiciones ocultas").
Caso 2
El modelo computable representa la misma entidad del mundo real con
dos objetos. Supongamos que en el mundo real hay una atleta Jane
Doe que compite en una disciplina, pero que también es juez en otra
disciplina atlética. Una sola persona en el mundo real debería ser un solo
objeto en el modelo computable. Necesitas modelar sólo el
comportamiento mínimo para cumplir tu simulación parcial.
Si tienes dos objetos diferentes (un competidor y un juez) que representan
a la desconocida, tarde o temprano tendrás incoherencias si asignas
alguna responsabilidad a uno de los dos y no la ves reflejada en el otro
(como se ilustra en la Figura 2-3).
Figura 2-3. Jane Doe está representada en el modelo por dos entidades diferentes
Caso 3
Un monedero bitcoin puede representarse como un objeto anémico con
algunas propiedades relativas a la dirección, el saldo, etc. (véase la Receta
3.1, "Convertir objetos anémicos en objetos ricos") o como un objeto rico
(con responsabilidades como recibir transacciones, escribir en una
blockchain, responder a el saldo, etc.), ya que están relacionados con el
mismo concepto.
Debes dejar de ver las entidades como estructuras de datos con atributos;
en lugar de eso, piensa en ellas como objetos, y comprende que son el
mismo objeto cumpliendo distintas funciones según el contexto en el que
estén interactuando. El Capítulo 3, "Modelos anémicos", incluye varias
recetas para ayudarte a cosificar tus objetos y convertirlos
en entidades de comportamiento.
REIFICACIÓN DE OBJETOS
La reificación de objetos es un proceso en el que se da forma concreta a un
concepto o idea abstractos representando un concepto o idea específicos, además
de proporcionar comportamiento a objetos anémicos y orientados a datos. Al dar
una forma concreta a conceptos abstractos mediante la creación de objetos,
podrás manipular y trabajar con estos conceptos de forma sistemática y
estructurada.
Caso 4
En la mayoría de los lenguajes de programación de objetos modernos,
una fecha puede construirse creándola a partir de su día, mes y año. Si
introduces la fecha 31 de noviembre de 2023, muchos lenguajes de
programación populares devolverán suavemente un objeto válido
(probablemente el 1 de diciembre de 2023).
Esto se presenta como una ventaja, pero oculta ciertos errores que pueden
producirse durante la carga de datos. El error surgirá al ejecutar un lote
nocturno que procese estas fechas no válidas lejos de la causa raíz,
violando el principio de fallo rápido (ver Capítulo 13, "Fallar rápido").
PRINCIPIO DE FALLAR RÁPIDO
El principio "fail fast " establece que debes interrumpir la ejecución lo antes
posible cuando se produzca un error, en lugar de ignorarlo y fallar como
consecuencia más adelante.
El efecto en el lenguaje cuando construyes
modelos
Este libro trata de mejorar el código sucio, no declarativo, críptico y
optimizado prematuramente. El conocimiento del mundo se define por el
lenguaje que hablas, y necesitas buenas metáforas para tus objetos y
comportamientos, como postula la hipótesis de Sapir-Whorf.
HIPÓTESIS DE SAPIR-WHORF
La hipótesis de Sapir-Whorf, también conocida como teoría de la relatividad
lingüística, sugiere que la estructura y el vocabulario de la lengua de una
persona pueden influir en su percepción del mundo que le rodea y darle forma.
La lengua que hablas no sólo refleja y representa la realidad, sino que también
desempeña un papel en su configuración y construcción. Esto significa que la
forma en que piensas y experimentas el mundo está parcialmente determinada
por el lenguaje que utilizas para describirlo.
Si piensas en los objetos como "soportes de datos", tus modelos perderán
la propiedad biyectiva y romperán el MAPPER. Encontrarás muchas
recetas relacionadas en el Capítulo 3, "Modelos anémicos". Si tratas los
objetos como poseedores de datos, tu modelo computacional (el software
que estás creando) no podrá predecir ni simular con precisión el mundo
real. Tus clientes se darán cuenta de que el software ya no les ayuda a
hacer su trabajo. Ésta es una fuente común de defectos de software
(comúnmente -y mal llamados- bugs).
ERROR
El término fallo es un error común en la industria. En este libro, hablo en su
lugar de defectos. Los bugs originales estaban relacionados con insectos externos
que entraban en circuitos calientes y alteraban la salida del software. Esto ya no
es así. Se recomienda emplear el término defecto porque se refiere a algo
introducido y no a un invasor externo.
Capítulo 3. Modelos anémicos
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y
comentarios: translation-feedback@[Link]
La corrección es claramente la cualidad principal. Si un sistema no hace lo
que se supone que debe hacer, todo lo demás importa poco.
Bertrand Meyer, Construcción de software orientado a objetos
3.0 Introducción
Los modelos de dominio anémicos, o simplemente objetos anémicos, son
objetos formados por un montón de atributos sin un comportamiento
real. Un objeto anémico suele denominarse "objeto de datos" porque sirve
principalmente para almacenar datos, pero carece de métodos u
operaciones significativos que puedan realizarse con esos datos.
Exponer datos al mundo exterior mediante getters y setters puede violar
el principio de encapsulación, ya que permite que fuentes externas
accedan a los datos de un objeto y los modifiquen potencialmente, en
lugar de mantenerlos contenidos dentro del propio objeto. Esto puede
hacer que el objeto sea más susceptible a la corrupción o a cambios no
intencionados.
La forma anémica de diseñar también puede conducir a un estilo de
programación más procedimental, en el que lo principal es manipular
datos en lugar de encapsularlos en objetos que tengan un
comportamiento significativo. Este libro te anima a crear objetos ricos.
Los objetos ricos tienen un conjunto más robusto de comportamientos y
métodos que les permiten realizar operaciones significativas y
proporcionan un único punto de acceso, evitando la lógica repetida.
ENCAPSULACIÓN
La encapsulación consiste en proteger las responsabilidades de un
objeto. Normalmente puedes conseguirlo abstrayendo la implementación real.
También proporciona una forma de controlar el acceso a los métodos de un
objeto. En muchos lenguajes de programación, es posible especificar la
visibilidad de las propiedades y métodos de un objeto, lo que determina si otras
partes del programa pueden acceder a ellos o modificarlos. Esto permite a los
desarrolladores ocultar los detalles de la implementación interna de un objeto y
exponer sólo el comportamiento necesario para que lo utilicen otras partes del
programa.
3.1 Convertir objetos anémicos en
objetos ricos
Problema
Quieres proteger tus objetos de manipulaciones externas y exponer
comportamientos en lugar de datos y estructuras para restringir la
propagación de cambios.
Solución
Convierte todos tus atributos en privados.
Debate
Imagina que tu dominio ha evolucionado y necesitas mantenerte al día
con las reglas de negocio de tus clientes en el mundo de la música. Ahora
las canciones necesitan tener un género asociado y tienes que añadirlo.
También quieres proteger sus atributos esenciales. En primer lugar, aquí
tienes una definición de clase que representa los metadatos de Canción en
el mundo real:
public class Song {
String name;
String authorName;
String albumName;
}
En este ejemplo, tratas el Song como una tupla (una colección fija de
elementos). El objetivo aquí es manipular sus atributos en varios lugares
repetidos a lo largo del código para que puedas editar el artista o el
álbum. Como la manipulación de la canción se repite, tendrás múltiples
impactos y lugares de cambio.
Cambia la visibilidad de tus atributos de public a private:
public class Song {
private String name;
private Artist author; // Will reference rich objects
private Album album; // instead of primitive data types
public String albumName() {
return [Link]() ;
}
}
A partir de ahora, tendrás que confiar en el comportamiento público
de Song, que favorece la encapsulación.
Si cambias la visibilidad a private, no podrás acceder a los atributos
hasta que añadas nuevos métodos para manipularlos. La manipulación
externa suele duplicarse en todo el sistema (véase la Receta 10.1,
"Eliminar el código repetido"). Si tus objetos tienen atributos public,
mutarán y cambiarán de forma inesperada.
Según tu MAPPER definido en el Capítulo 2, debes diseñar objetos
basándote en su comportamiento, en lugar de crear objetos anémicos que
sólo modelen datos. Ten en cuenta que algunos lenguajes como Java, C++,
C# o Ruby tienen visibilidad de atributos, mientras que otros como
JavaScript no la tienen en absoluto. Algunos, como Python o Smalltalk,
siguen convenciones culturales en las que la visibilidad se documenta
pero no se impone. No se trata de una refactorización segura , tal como se
define en el Capítulo 1. Cambiar la privacidad de un atributo puede
romper las dependencias existentes.
ADVERTENCIA
Aplicar ésta y muchas otras recetas requiere un conjunto de pruebas exhaustivo
que actúe como red de seguridad frente a posibles defectos mientras se modifica
el comportamiento del código. Michael Feathers aborda cómo crear y mantener
esta red de seguridad en su libro Working Effectively with Legacy Code.
Recetas relacionadas
Receta 3.5, "Eliminar propiedades automáticas"
Receta 10.1, "Eliminar código repetido"
3.2 Identificar la esencia de tus objetos
Problema
Quieres crear invariantes (ver Receta 13.2, "Hacer cumplir las condiciones
previas") de tus objetos y mantenerlas válidas todo el tiempo.
Solución
No permitas cambios en los atributos esenciales ni en el
comportamiento. Establece los atributos esenciales durante la creación
del objeto y protégelos para que no cambien una vez creado el objeto.
Debate
Toda entidad del mundo real tiene un comportamiento esencial. El
comportamiento determina que sea un objeto determinado y no otro. Este
comportamiento está presente en la biyección mapeada al mundo real. La
esencia es el ADN de un objeto. No puede cambiar una vez que nace; no se
puede manipular. Los objetos pueden mutar de forma accidental, no
esencial.
ESENCIA Y ACCIDENTE
En su libro The Mythical Man-Month, el informático Fred Brooks utiliza los
términos "accidental" y "esencial" para referirse a dos tipos diferentes de
complejidad en ingeniería de software, utilizando la definición de Aristóteles.
La complejidad "esencial" es inherente al problema que se resuelve y no puede
evitarse, ya que es la complejidad necesaria para que el sistema funcione según
lo previsto y presente en el mundo real. Por ejemplo, la complejidad de un
sistema de aterrizaje espacial es esencial porque es necesaria para aterrizar con
seguridad un vehículo explorador.
La complejidad "accidental" surge de la forma en que se diseña e implementa el
sistema, más que de la naturaleza del problema que se resuelve. Puede reducirse
creando buenos diseños. La complejidad accidental innecesaria es uno de los
mayores problemas del software y en este libro encontrarás muchas soluciones.
Veamos qué ocurre cuando cambias el mes de un Date:
const date = new Date();
[Link](4);
Esto no es válido en el mundo real, ya que el mes es esencial para
cualquier fecha, pero es válido en muchos lenguajes de programación.
Cambiar el mes lo convierte en otra fecha en el mundo real. Llamar
a setMonth() con un único argumento sólo fijará el valor del mes,
dejando los valores del día y del año sin cambiar.
Cualquier otro objeto que dependa de la fecha cambia ahora como
consecuencia del efecto dominó. Si tienes un pago que vence en la fecha
que creaste anteriormente y luego cambias la esencia de la fecha,
entonces tu pago se verá afectado silenciosamente. Una solución mejor
sería cambiar la referencia payment a una nueva fecha invocando un
protocolo real como defer(). Este cambio sólo afectará al
objeto payment y causará un efecto dominó restringido.
EFECTO DOMINÓ
El efecto dominó se refiere a la forma en que un cambio o modificación en una
parte de un sistema puede tener consecuencias imprevistas en otras partes del
sistema. Si realizas un cambio en un objeto concreto, podría afectar
potencialmente a otras partes del sistema que dependan de él. Esto podría
provocar errores o comportamientos inesperados en esas otras partes del
sistema.
Aquí tienes una opción mejor:
const date = new ImmutableDate("2022-03-25");
// The date essence is identified and you will never change it from
now on
Una vez creada la fecha, es inmutable. Puedes confiar en que siempre se
asignará a la misma entidad del mundo real. Es importante modelar qué
atributos/comportamientos son esenciales y cuáles son accidentales: lo
que es esencial en el mundo real debe serlo en tu modelo y viceversa.
ADVERTENCIA
Muchos lenguajes modernos no identifican ni protegen la esencia de los objetos.
La clase Date es un ejemplo común. Sin embargo, existen paquetes alternativos
robustos para la manipulación de calendarios en la mayoría de ellos.
Recetas relacionadas
Receta 5.3, "Prohibir los cambios en la Esencia"
Receta 17.11, "Evitar el efecto dominó"
3.3 Eliminar los definidores de los
objetos
Problema
Quieres proteger tus objetos de manipulaciones externas mediante setters
y favorecer la inmutabilidad.
Solución
Después de hacer privados tus atributos (consulta la Receta 3.1, "Convertir
objetos anémicos en objetos ricos"), elimina todos los definidores.
Debate
Este es un ejemplo clásico de una clase Point con setters:
public class Point {
protected int x;
protected int y;
public Point() { }
public void setX(int x) {
this.x = x;
}
public void setY(int y) {
this.y = y;
}
}
Point location = new Point();
// At this moment, it is not clear which points are represented
// It is coupled to the constructor decision
// Might be null or some other convention
[Link](1);
// Now you have point(1,0)
[Link](2);
// Now you have point(1,2)
// If you are setting essential properties move
// them to the constructor and remove the setter method
Aquí tienes una versión más corta después de aplicar la receta y eliminar
los fijadores:
public class Point {
public Point(int x, int y) {
this.x = x;
this.y = y;
// You remove the setters
}
Point location = new Point(1, 2);
Los fijadores favorecen la mutabilidad (véase el Capítulo 5,
"Mutabilidad") y los modelos anémicos. Sólo debes cambiar tus objetos
invocando un método cuyo efecto secundario sea el cambio. Esto también
sigue el principio "Dilo, no lo preguntes".
"PRINCIPIO "DILO, NO PREGUNTES
El principio "Dilo, no preguntes" define una forma de interactuar con los objetos
invocando sus métodos en lugar de pedir sus datos.
Añadir setters a un objeto lo hace mutable de formas inesperadas, y
necesitas añadir controles de integridad invariante en múltiples
lugares. Esto da lugar a código repetido (consulta la Receta 10.1, "Eliminar
el código repetido"). Los métodos con el
nombre setXXX() violan la nomenclatura MAPPER, ya que rara vez
existen en el mundo real, y nunca debes mutar objetos en su esencia,
como verás en el Capítulo 4. Muchos lenguajes permiten mutar fechas
(utilizando, por ejemplo, [Link](5)), pero estas operaciones
crean un efecto dominó en todos los objetos que dependen de los
mutados.
Recetas relacionadas
Receta 3.5, "Eliminar propiedades automáticas"
Receta 3.7, "Completar constructores vacíos"
Receta 3.8, "Eliminar Getters"
3.4 Eliminar los generadores de código
anémicos
Problema
Estás utilizando código anémico generadores pero quieres tener más
control sobre tus atributos para convertirlos en objetos ricos, evitar la
duplicación de código y centrarte en el comportamiento en lugar de en los
datos.
Solución
Elimina los asistentes y generadores de código. Si quieres evitar el trabajo
repetitivo, tienes que aplicar la Receta 10.1, "Eliminar el código
repetido" y crear un objeto intermedio con el comportamiento repetido.
Debate
Los magos del código estaban de moda durante los años 90. Muchos
proyectos se medían utilizando líneas de código, y tener grandes bases de
código era una virtud. Se utilizaban generadores de código automatizados
en los que ponías una plantilla de clase con atributos y generaban el
código automáticamente. Esto llevaba a la duplicación de código y a
sistemas difíciles de mantener.
Hoy en día, los asistentes de código como Codex, Code
Whisperer, ChatGPT o GitHub Copilot crean código de forma similar
cuando se les pide. Como hemos comentado en recetas anteriores, debes
evitar el código anémico, y esto es lo que produce el estado actual de los
asistentes de código con inteligencia artificial.
He aquí un ejemplo de clase anémica generada con metaprogramación
(véase el capítulo 23, "Metaprogramación"):
AnemicClassCreator::create(
'Employee',
[
new AutoGeneratedField(
'id', '$validators->getIntegerValidator()'),
new AutoGeneratedField(
'name', '$validators->getStringValidator()'),
new AutoGeneratedField(
'currentlyWorking', '$validators-
>getBooleanValidator()')
]);
La metaprogramación crea setters y getters mágicos bajo el capó:
getId(), setId(), getName(), ….
// validation is not explicit
Carga la clase con un cargador automático de clases:
$john = new Employee;
$john->setId(1);
$john->setName('John');
$john->setCurrentlyWorking(true);
$john->getName();
// returns 'John'
Para aplicar esta receta, necesitas que el código sea explícito, legible y
depurable:
final class Employee {
private $name;
private $workingStatus;
public function __construct(string $name, WorkingStatus
$workingStatus) {
// Constructor and initialization code goes here
}
public function name(): string {
return $this->name;
// This is not a getter.
// It is Employee's responsibility to tell her/his name
// Accidentally, you have implemented an attribute with the
same name
}
}
// You have no magic setters or getters
// All methods are real and can be debugged
// Validations are implicit
// since the WorkingStatus object is valid by construction
$john = new Employee('John', new HiredWorkingStatus());
$john->name(); // returns 'John'
Aunque esto parezca tedioso, encontrar las abstracciones que faltan para
tratar el código explícito es mucho mejor que generar código anémico.
Recetas relacionadas
Receta 10.1, "Eliminar código repetido"
Receta 23.1, "Eliminar el uso de la metaprogramación"
3.5 Eliminar propiedades automáticas
Problema
Tienes código que utiliza propiedades automáticas favoreciendo la
creación rápida y descontrolada de objetos anémicos antes de pensar en
el comportamiento.
Solución
Elimina las propiedades automáticas. Crea a mano la única que necesites
e implementa un comportamiento claro en el MAPPER.
Debate
Los objetos y fijadores anémicos violan los controles de integridad y el
principio de fallo rápido. Hay un capítulo entero sobre este tema(Capítulo
13, "Fail Fast"). Se trata de un objeto Person con una propiedad de
nombre automático que crea accesores
anémicos getName() y setName():
class Person
{
public string name
{ get; set; }
}
La herramienta de propiedades automatizadas favorece a los objetos
anémicos. La solución es hacerlo explícito:
class Person
{
private string name;
public Person(string personName)
{
name = personName;
// immutable
// no getters, no setters
}
// ... more protocol, probably accessing private variable name
}
ADVERTENCIA
Los setters y getters son malas prácticas del sector. Es una característica del
lenguaje y muchos IDE también favorecen esta práctica. Debes pensártelo bien
antes de exponer tus propiedades por conveniencia accidental.
Algunos lenguajes proporcionan soporte explícito para construir modelos
anémicos y DTOs (véase la Receta 3.6, "Eliminar los DTOs"), y necesitas
comprender las consecuencias de utilizar este tipo de características; el
primer paso es dejar de pensar en las propiedades y centrarse
únicamente en el comportamiento.
Recetas relacionadas
Receta 3.1, "Convertir objetos anémicos en objetos ricos"
Receta 3.3, "Eliminar definidores de objetos"
Receta 3.4, "Eliminar generadores de código anémicos"
Receta 3.6, "Eliminar DTOs"
Receta 3.8, "Eliminar Getters"
Receta 4.8, "Eliminar propiedades innecesarias"
3.6 Eliminar los DTOs
Problema
Quieres tener objetos completos y transferirlos entre capas.
Solución
Utiliza objetos reales y evita los objetos de datos. En su lugar, puedes
transferir datos anémicos con matrices o diccionarios, y si necesitas
transferir objetos parciales, puedes utilizar proxies u objetos
nulos (véase la Receta 15.1, "Crear objetos nulos") para romper el grafo de
referencia.
Debate
DTOS
Un DTO (Objeto de Transferencia de Datos) se utiliza para transferir datos
entre las distintas capas de una aplicación. Es un objeto simple, serializable e
inmutable que transporta datos entre el cliente y el servidor de la aplicación. El
único propósito de un DTO es proporcionar una forma estándar de intercambiar
datos entre las distintas partes de la aplicación.
Los DTO o clases de datos son herramientas de objetos anémicos muy
utilizadas. No aplican reglas de negocio, y la lógica para tratar con ellas
suele estar duplicada. Algunos estilos arquitectónicos favorecen la
creación de muchos DTO para reflejar objetos reales. Esta solución
contamina el espacio de nombres con objetos anémicos y dificulta el
mantenimiento de los sistemas. Cuando necesitas cambiar un objeto,
también debes actualizar el DTO, por lo que se duplica mucho el esfuerzo.
Los DTO son anémicos, pueden transportar datos incoherentes, forzar la
duplicación de código, contaminar los espacios de nombres (ver Receta
18.4, "Eliminar clases globales") con clases inútiles y crear efectos dominó.
Además, la integridad de los datos es más difícil de aplicar, ya que puede
duplicarse en todo el sistema.
Aquí tienes una clase de dominio SocialNetworkProfile con su DTO
asociado SocialNetworkProfileDTO para llevar información de un
lugar a otro:
final class SocialNetworkProfile {
private $userName;
private $friends; // friends is a reference to a large collection
private $feed; // feed references the whole user feed
public function __construct($userName, $friends, UserFeed $feed)
{
$this->assertUsernameIsValid($userName);
$this->assertNoFriendDuplicates($friends);
$this->userName = $userName;
$this->friends = $friends;
$this->feed = $feed;
$this->assertNoFriendofMylsef($friends);
}
}
// If you need to transfer to an external system you need
// to duplicate (and maintain) the structure
final class SocialNetworkProfileDTO {
private $userName; // duplicated to be synchronized
private $friends; // duplicated to be synchronized
private $feed; // duplicated to be synchronized
public function __construct() {
// Empty constructor without validations
}
// No protocol, just serializers
}
// If you need to transfer to an external system you create an anemic
DTO
$janesProfileToTransfer = new SocialNetworkProfileDTO();
Aquí tienes una versión explícita sin DTOs anémicos:
final class SocialNetworkProfile {
private $userName;
private $friends;
private $feed;
public function __construct(
$userName,
FriendsCollection $friends,
UserFeedBehavior $feed)
{
$this->assertUsernameIsValid($userName);
$this->assertNoFriendDuplicates($friends);
$this->userName = $userName;
$this->friends = $friends;
$this->feed = $feed;
$this->assertNoFriendOfMyself($friends);
}
// lots of protocol associated with the profile
// No serialization protocol
// No behavior or attribute duplication
}
interface FriendsCollectionProtocol { }
final class FriendsCollection implements FriendsCollectionProtocol {
}
final class FriendsCollectionProxy implements
FriendsCollectionProtocol {
// proxy protocol
// travels as a lightweight object and can get contents when
requested
}
abstract class UserFeedBehavior { }
final class UserFeed extends UserFeedBehavior { }
final class NullFeed extends UserFeedBehavior {
// throws an error when requested for behavior
}
// If you need to transfer to an external system you create a valid
object
$janesProfileToTransfer = new SocialNetworkProfile(
'Jane',
new FriendCollectionProxy(),
new NullFeed()
);
Puedes comprobar si hay clases anémicas sin comportamiento de objeto
de negocio (eliminando serializadores, constructores, mutadores, etc.).
ADVERTENCIA
Los DTO son una herramienta y una práctica establecida en algunos
lenguajes. Debes utilizarlos con cuidado y responsabilidad, y si necesitas
desmontar tus objetos para enviarlos fuera de tus dominios, debes ser
extremadamente cauteloso, ya que los objetos desmembrados no tienen
consideraciones de integridad.
Recetas relacionadas
Receta 3.1, "Convertir objetos anémicos en objetos ricos"
Receta 3.7, "Completar constructores vacíos"
Receta 16.1, "Evitar los identificadores en los objetos"
Receta 17.13, "Eliminar el código de negocio de la interfaz de usuario"
Ver también
Entrada del blog de Martin Fowler sobre los abusos de los DTO
"Clase de datos" en Refactoring Guru
3.7 Completar constructores vacíos
Problema
Tienes constructores vacíos -donde estos objetos vacíos no existen en el
mundo real- sin alguna inicialización esencial, y quieres tener objetos
completos y válidos todo el tiempo.
Solución
Pasa todos los argumentos esenciales al crear objetos y utiliza un
constructor único y completo.
Debate
Los objetos creados sin argumentos suelen ser mutables, impredecibles e
incoherentes. Los constructores sin parámetros son un olor a código de un
objeto no válido que mutará peligrosamente. Los objetos incompletos
causan muchos problemas y no debes cambiar el comportamiento
esencial de un objeto después de crearlo. Si puedes mutarlos, todas tus
referencias serán poco fiables a partir de ese momento. En el Capítulo 5,
"Mutabilidad", trataremos este problema en detalle. Muchos lenguajes te
permiten crear objetos no válidos (por ejemplo, un Date vacío).
He aquí un ejemplo de persona anémica, mutable e incoherente:
public Person();
// Anemic and mutable
// Does not have the essence to be a valid person
Un modelo Person mejor con los atributos esenciales sería:
public Person(String name, int age) {
[Link] = name;
[Link] = age;
}
// You 'pass' the essence to the object
// So it does not mutate
Los objetos sin estado son un contraejemplo válido. No debes aplicarles
esta receta.
ADVERTENCIA
Algunos marcos de persistencia en lenguajes tipados estáticamente exigen un
constructor vacío. Es una mala decisión. Puedes vivir con ello, pero es una
puerta trasera para los malos modelos, y siempre debes crear objetos completos
y hacer que su esencia sea inmutable para que perduren en el tiempo.
Todo objeto necesita que su esencia sea válida desde el principio. Esto está
relacionado con las ideas de Platón sobre la inmutabilidad esencial. Los
aspectos accidentales, como la edad y el lugar físico, pueden cambiar. Los
objetos inmutables favorecen la biyección y sobreviven al paso del
tiempo.
Recetas relacionadas
Receta 3.1, "Convertir objetos anémicos en objetos ricos"
Receta 3.3, "Eliminar definidores de objetos"
Receta 3.8, "Eliminar Getters"
Receta 11.2, "Reducir el exceso de argumentos"
3.8 Eliminar Getters
Problema
Quieres tener control sobre el acceso a tus objetos
y ocultar representación accidental tanto como sea posible para tener la
libertad de cambiar sin romper el software.
Solución
Elimina los getters de tu objeto. Invoca algún método explícito basado en
el comportamiento y no en los datos, y protege tus decisiones de
implementación. Utiliza en su lugar nombres de dominio.
Debate
Si evitas el prefijo getXXX favorecerás la ocultación de información y los
principios de encapsulación, y tu diseño requerirá cambios rápidamente.
Los modelos con getters suelen estar estrechamente acoplados y menos
encapsulados.
OCULTAR INFORMACIÓN
Laocultación de información pretende reducir la complejidad de un sistema de
software separando su funcionamiento interno de su interfaz externa. Esto
permite que la implementación interna de un sistema cambie sin afectar a la
forma en que lo utilizan otros sistemas o usuarios.
Una forma de conseguir ocultar información es mediante el uso de abstracciones
en el MAPPER, que proporcionan una visión simplificada de la funcionalidad de
un sistema y ocultan los detalles subyacentes.
Se trata de una aplicación clásica de la ventana:
final class Window {
public $width;
public $height;
public $children;
public function getWidth() {
return $this->width;
}
public function getArea() {
return $this->width * $this->height;
}
public function getChildren() {
return $this->children;
}
}
No se debe acceder a las propiedades de una ventana mediante este tipo
de getters. Esta es una versión incompatible, pero mejor, que se centra en
el comportamiento real de la ventana:
final class Window {
private $width;
private $height;
private $children;
public function width() {
return $this->width;
}
public function area() {
return $this->height * $this->width;
}
public function addChildren($aChild) {
// Do not expose internal attributes
return $this->children[] = $aChild;
}
Los getters coinciden en ciertos escenarios con la verdadera
responsabilidad. Será razonable que una ventana devuelva su color y que
accidentalmente lo almacene como color. Por tanto, un
método color() que devuelva el atributo color podría ser una buena
solución. getColor() rompe la regla de bijección, ya que es
implementacional y no tiene una contrapartida real en el MAPPER.
Algunos lenguajes devuelven getters con referencias a objetos privados.
Esto rompe la encapsulación. Algunos devuelven colecciones internas (en
lugar de copias), por lo que un cliente puede modificar, añadir o eliminar
elementos sin invocar protocolos más seguros y protectores. Esto también
rompe la ley de Deméter.
LEY DE DEMÉTER
La ley de Deméter es un principio que establece que un objeto sólo debe
comunicarse con sus vecinos inmediatos, y no debe conocer el funcionamiento
interno de otros objetos. Para favorecer la ley de Demeter, debes crear objetos
que estén débilmente acoplados, es decir, que no dependan mucho unos de
otros. Esto hace que el sistema sea más flexible y fácil de mantener, ya que es
menos probable que los cambios en un objeto tengan consecuencias no deseadas
en otros objetos.
Un objeto sólo debe acceder a los métodos de sus vecinos inmediatos, en lugar de
llegar a otros objetos para acceder a sus partes internas. Esto ayuda a reducir el
nivel de acoplamiento entre objetos y hace que el sistema sea más modular y
flexible.
public class MyClass {
private ArrayList<Integer> data;
public MyClass() {
data = new ArrayList<Integer>();
}
public void addData(int value) {
[Link](value);
}
public ArrayList<Integer> getData() {
return data; // breaking encapsulation
}
}
En este ejemplo en Java, el método getData() devuelve una referencia a
la colección de datos interna, en lugar de crear una copia de ella. Esto
significa que cualquier cambio realizado en la colección fuera de la clase
se reflejará directamente en los datos internos, y puede provocar un
comportamiento inesperado o defectos en el código.
ADVERTENCIA
Es importante tener en cuenta que esto puede ser un agujero de seguridad: un
cliente puede modificar el estado interno del objeto sin pasar por el
comportamiento previsto, es decir, por los métodos de la clase (consulta el
Capítulo 25, "Seguridad").
Recetas relacionadas
Receta 3.1, "Convertir objetos anémicos en objetos ricos"
Receta 3.3, "Eliminar definidores de objetos"
Receta 3.5, "Eliminar propiedades automáticas"
Receta 8.4, "Eliminar comentarios del Getter"
Receta 17.16, "Romper la intimidad inapropiada"
3.9 Evitar la Orgía de Objetos
Problema
Tienes código que viola las reglas de las propiedades encapsuladas de
otros objetos.
Solución
Protege las propiedades y expone sólo el comportamiento.
Debate
ORGÍA DE OBJETOS
Una orgía de objetos describe una situación en la que los objetos están
insuficientemente encapsulados, permitiendo un acceso sin restricciones a su
interior. Se trata de un antipatrón común en el diseño orientado a objetos y
puede provocar un aumento del mantenimiento y de la complejidad.
Si ves tus objetos como poseedores de datos, violarás su encapsulación,
pero no deberías. Como en la vida real, siempre debes pedir
consentimiento. Acceder a las propiedades de otros objetos rompe el
principio de ocultación de la información y genera un fuerte
acoplamiento. Ten en cuenta que el acoplamiento al comportamiento
esencial, las interfaces y el protocolo es una mejor decisión que el
acoplamiento a los datos y la implementación accidental.
Considera el conocido ejemplo de Point:
final class Point {
public $x;
public $y;
}
final class DistanceCalculator {
function distanceBetween(Point $origin, Point $destination) {
return sqrt((($destination->x - $origin->x) ^ 2) +
(($destination->y - $origin->y) ^ 2));
}
}
A continuación se muestra un Point más abstracto que no depende de
cómo almacenes la información y sigue el principio "Dilo, no lo preguntes"
(véase la Receta 3.3, "Eliminar los definidores de los objetos"). El Point ha
cambiado su representación interna y accidental, almacenando su
posición mediante coordenadas polares:
final class Point {
private $rho;
private $theta;
public function x() {
return $this->rho * cos($this->theta);
}
public function y() {
return $this->rho * sin($this->theta);
}
}
final class DistanceCalculator {
function distanceBetween(Point $origin, Point $destination) {
return sqrt((($destination->x() - $origin->x() ^ 2) +
(($destination->y() - $origin->y()) ^ 2)));
}
}
Si tus clases están contaminadas con setters, getters y métodos públicos,
seguro que tienes formas de acoplarte a su implementación accidental.
Recetas relacionadas
Receta 3.1, "Convertir objetos anémicos en objetos ricos"
Receta 3.3, "Eliminar definidores de objetos"
3.10 Eliminar propiedades dinámicas
Problema
Puedes utilizar propiedades en una clase sin declararlas.
Solución
Sé explícito con tus atributos.
Debate
Las propiedades dinámicas son difíciles de leer, su definición de ámbito
no está clara y pueden ocultar erratas inadvertidas. Deberías favorecer
los lenguajes que prohíben las propiedades dinámicas. Rompen la
seguridad de tipos, ya que es fácil introducir accidentalmente errores
tipográficos o utilizar nombres de propiedades equivocados. Esto puede
provocar errores en tiempo de ejecución difíciles de depurar,
especialmente en bases de código grandes. También ocultan posibles
colisiones de nombres, ya que las propiedades dinámicas pueden tener el
mismo nombre que las propiedades definidas en la clase u objeto, dando
lugar a conflictos o comportamientos inesperados.
Aquí tienes un ejemplo de propiedad indefinida:
class Dream:
pass
nightmare = Dream()
[Link] = "I am the Sandman"
# presentation is not defined
# it is dynamic property
print([Link])
# Output: "I am the Sandman"
Cuando lo definas en la clase:
class Dream:
def __init__(self):
[Link] = ""
nightmare = Dream()
[Link] = "I am the Sandman"
print([Link])
# Output: "I am the Sandman"
Las propiedades dinámicas están soportadas en muchos lenguajes de
programación como PHP, Python, Ruby, JavaScript, C#, Objective-C, Swift,
Kotlin, etc. y muchos de ellos tienen opciones de compilador para
evitarlas. En estos lenguajes, se pueden añadir propiedades dinámicas a
los objetos en tiempo de ejecución, y acceder a ellas utilizando la sintaxis
del accesor de propiedades del objeto.
Recetas relacionadas
Receta 3.1, "Convertir objetos anémicos en objetos ricos"
Receta 3.5, "Eliminar propiedades automáticas"
Capítulo 4. La obsesión primitiva
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y
comentarios: translation-feedback@[Link]
La obsesión por lo primitivo es una obsesión primitiva.
Rich Hickey
4.0 Introducción
Muchos ingenieros de software piensan que el software consiste en
"mover datos"; las escuelas y los libros de texto orientados a objetos se
centran en los datos y los atributos cuando enseñan a modelar el mundo
real. Éste era un sesgo cultural que se enseñaba en las universidades
durante los años 80 y 90. Las tendencias del sector empujaron a los
ingenieros a crear diagramas entidad-relación (ERD) y a razonar sobre los
datos empresariales en lugar de centrarse en el comportamiento.
Los datos son más relevantes que nunca. La ciencia de datos está
creciendo y el mundo gira en torno a los datos. Necesitas crear un
simulador para gestionar y proteger los datos y exponer el
comportamiento, ocultando al mismo tiempo la información y la
representación accidental para evitar el acoplamiento. Las recetas de este
capítulo te ayudarán a identificar los objetos pequeños y a ocultar la
representación accidental. Descubrirás muchos objetos pequeños
cohesionados y los reutilizarás en muchos contextos diferentes.
COHESIÓN
La cohesión es una medida de el grado en que los elementos de una misma clase
o módulo de software trabajan juntos para lograr un propósito único y bien
definido. Se refiere a lo estrechamente relacionados que están los objetos entre sí
y con el objetivo general del módulo. Puedes considerar que una cohesión alta es
una propiedad deseable en el diseño de software, ya que los elementos de un
módulo están estrechamente relacionados y trabajan juntos con eficacia para
lograr un objetivo específico.
4.1 Crear objetos pequeños
Problema
Tienes objetos grandes que sólo contienen tipos primitivos como campos.
Solución
Busca responsabilidades para objetos pequeños en el MAPPER y reifícalos.
Debate
Desde los primeros días de la informática, los ingenieros mapean todo lo
que ven a los familiares tipos de datos primitivos,
como String, Entero y Colección. El mapeo a esos tipos de datos viola a
veces los principios de abstracción y fail fast. El nombre de una Persona
tiene un comportamiento diferente al de una cadena, como puedes ver en
el siguiente ejemplo:
public class Person {
private final String name;
public Person(String name) {
[Link] = name;
}
}
El concepto de nombre está cosificado:
public class Name {
private final String name;
public Name(String name) {
[Link] = name;
// Name has its own creation rules, comparison, etc.
// Might be different than a string
}
}
public class Person {
private final Name name;
public Person(Name name) {
// Name is created as a valid one,
// you don't need to add validations here
[Link] = name;
}
}
Toma como ejemplo la palabra de cinco letras del juego Wordle. Una
palabra Wordle no tiene las mismas responsabilidades que un char(5) y
no se mapea en la bijección. Si quieres crear un juego Wordle, verás una
bijección entre una palabra Wordle distinta de una String o char(5) ,
ya que no tienen las mismas responsabilidades. Por ejemplo, no es
responsabilidad de un Stringaveriguar cuántas coincidencias tiene con
la palabra Wordle secreta. Y no es responsabilidad de una palabra Wordle
concatenar.
WORDLE
Wordle es un popular juego online de adivinar palabras en el que tienes seis
intentos para adivinar una palabra de cinco letras seleccionada por el
juego. Cada vez que adivines una palabra de cinco letras, el juego te indicará qué
letras son correctas y están en la posición correcta (marcadas con un cuadrado
verde) y qué letras son correctas pero están en la posición incorrecta (marcadas
con un cuadrado amarillo).
En un número muy reducido de sistemas de misión crítica, existe un
compromiso entre abstracción y rendimiento. Pero para evitar una
optimización prematura (véase el Capítulo 16, "Optimización prematura"),
debes confiar en los ordenadores modernos y en las optimizaciones de las
máquinas virtuales y, como siempre, debes ceñirte a las pruebas en
escenarios del mundo real. Encontrar objetos pequeños es una tarea muy
difícil, que requiere experiencia para hacer un buen trabajo y evitar el
sobrediseño. No hay una bala de plata a la hora de elegir cómo y cuándo
mapear algo.
NO HAY BALAS DE PLATA
El concepto de "ninguna bala de plata" es una frase acuñada por el informático y
pionero de la ingeniería de software Fred Brooks en su ensayo de 1986 "Ninguna
bala de plata: Esencia y accidentes de la ingeniería de software". Brooks
argumenta que no existe una única solución o enfoque que pueda resolver todos
los problemas o mejorar significativamente la productividad y eficacia del
desarrollo de software.
Recetas relacionadas
Receta 4.2, "Reificar datos primitivos"
Receta 4.9, "Crear intervalos de fechas"
4.2 Reificar los datos primitivos
Problema
Tienes objetos que utilizan demasiados tipos primitivos.
Solución
Utiliza objetos pequeños en lugar de primitivos.
Debate
Supón que estás construyendo un servidor web:
int port = 8080;
InetSocketAddress in = open("[Link]", port);
String uri = urifromPort("[Link]", port);
String address = addressFromPort("[Link]", port);
String path = pathFromPort("[Link]", port);
Este ejemplo ingenuo tiene muchos problemas. Viola el principio "Dilo, no
lo preguntes" (véase la Receta 3.3, "Eliminar los definidores de los
objetos") y el principio de fallo rápido. Además, no sigue la regla de
diseño MAPPER y viola el principio del subconjunto. Hay manipulación de
código duplicado por todas partes que se necesita para utilizar estos
objetos, ya que no separa claramente el "qué" del "cómo".
La industria es muy perezosa cuando se trata de crear objetos pequeños y
también de separar el qué y el cómo, ya que requiere un esfuerzo
adicional descubrir tales abstracciones. Es importante fijarse en el
protocolo y el comportamiento de los componentes pequeños y olvidarse
de intentar comprender las interioridades de cómo funcionan las cosas.
Una solución conforme a la biyección podría ser:
Port server = [Link](this, "[Link]");
// Port is a small object with responsibilities and protocol
Port in = [Link](this); // returns a port, not a number
URI uri = [Link](this); // returns an URI
InetSocketAddress address = [Link]();
// returns an Address
Path path = [Link](this, "/[Link]"); // returns a Path
// all of them are validated small bijection objects with very few and
precise
// responsibilities
Recetas relacionadas
Receta 4.1, "Crear objetos pequeños"
Receta 4.4, "Eliminar los abusos de las cadenas"
Receta 4.7, "Reificar validaciones de cadenas"
Receta 17.15, "Refactorizar cúmulos de datos"
Ver también
"Obsesión primitiva" en Refactoring Guru
4.3 Reificar matrices asociativas
Problema
Tienes anémicas matrices asociativas (clave/valor ) que
representan objetos del mundo real.
Solución
Utiliza matrices para la creación rápida de prototipos y utiliza objetos
para las cosas serias.
Debate
PROTOTIPADO RÁPIDO
El prototipado rápido se utiliza en el desarrollo de productos para crear
rápidamente prototipos que funcionen y validarlos con el usuario final. Esta
técnica permite a diseñadores e ingenieros probar y refinar un diseño antes de
crear un código limpio coherente, robusto y elegante.
Las matrices asociativas son una forma práctica de representar objetos
anémicos. Si los encuentras en el código, esta receta te ayudará a reificar
el concepto y sustituirlos. Tener objetos ricos es beneficioso para limpiar
el código, de modo que puedas fallar rápido, mantener la integridad,
evitar la duplicación de código y ganar cohesión.
Mucha gente sufre de obsesión por lo primitivo y cree que esto es
sobrediseño. Diseñar software consiste en tomar decisiones y comparar
compensaciones. El argumento del rendimiento no es válido hoy en día,
ya que las máquinas virtuales modernas pueden tratar eficazmente
pequeños objetos de corta duración.
He aquí un ejemplo de código de obsesión anémico y primitivo:
$coordinate = array('latitude'=>1000, 'longitude'=>2000);
// They are just arrays. A bunch of raw data
Esto es más exacto según el concepto de biyección:
final class GeographicCoordinate {
function __construct($latitudeInDegrees, $longitudeInDegrees) {
$this->longitude = $longitudeInDegrees;
$this->latitude = $latitudeInDegrees;
}
}
$coordinate = new GeographicCoordinate(1000, 2000);
// Should throw an error since these values don’t exist on Earth
Necesitas tener objetos que sean válidos desde el principio:
final class GeographicCoordinate {
function __construct($latitudeInDegrees, $longitudeInDegrees) {
$this->longitude = $longitudeInDegrees;
$this->latitude = $latitudeInDegrees;
}
}
$coordinate = new GeographicCoordinate(1000, 2000);
// Should throw an error since these values don't exist on Earth
final class GeographicCoordinate {
function __construct($latitudeInDegrees, $longitudeInDegrees) {
if (!$this->isValidLatitude($latitudeInDegrees)) {
throw new InvalidLatitudeException($latitudeInDegrees);
}
$this->longitude = $longitudeInDegrees;
$this->latitude = $latitudeInDegrees;
}
}
}
$coordinate = new GeographicCoordinate(1000, 2000);
// throws an error since these values don't exist on Earth
Hay un objeto pequeño oscuro (ver Receta 4.1, "Crear objetos
pequeños") para modelar la latitud:
final class Latitude {
function __construct($degrees) {
if (!$degrees->between(-90, 90)) {
throw new InvalidLatitudeException($degrees);
}
}
}
final class GeographicCoordinate {
function distanceTo(GeographicCoordinate $coordinate) { }
function pointInPolygon(Polygon $polygon) { }
}
// Now you are in the geometry world (and not in the world of arrays
anymore).
// You can safely do many exciting things.
Al crear objetos, no debes pensar en ellos como datos. Se trata de un error
muy común. Debes mantenerte fiel al concepto de biyección y descubrir
objetos del mundo real.
Recetas relacionadas
Receta 3.1, "Convertir objetos anémicos en objetos ricos"
4.4 Eliminar los abusos de las cadenas
Problema
Tienes también muchas funciones de análisis sintáctico, explosión, regex,
comparación de cadenas, búsqueda de subcadenas y otras funciones de
manipulación de cadenas.
Solución
Utiliza abstracciones reales y objetos reales en lugar de manipulaciones
accidentales de cadenas.
Debate
No abuses de las cuerdas. Favorece los objetos reales. Encuentra
protocolos ausentes para distinguirlos de las cadenas. Este código hace
muchas manipulaciones primitivas de cadenas:
$schoolDescription = 'College of Springfield';
preg_match('/[^ ]*$/', $schoolDescription, $results);
$location = $results[0]; // $location = 'Springfield'.
$school = preg_split('/[\s,]+/', $schoolDescription, 3)[0];
//'College'
Puedes convertir el código en una versión más declarativa:
class School {
private $name;
private $location;
function description() {
return $this->name . ' of ' . $this->location->name;
}
}
Al encontrar objetos presentes en el MAPPER, tu código es más
declarativo, más comprobable y puede evolucionar y cambiar más
rápidamente. También puedes añadir restricciones a las nuevas
abstracciones. Utilizar cadenas para mapear objetos reales es una
obsesión primitiva y un síntoma de optimización prematura(véase el
Capítulo 16, "Optimización prematura"). A veces, la versión de cadenas es
un poco más eficaz. Si tienes que decidir entre aplicar esta receta o hacer
manipulaciones de bajo nivel, crea siempre escenarios de uso reales y
encuentra mejoras concluyentes y significativas.
Recetas relacionadas
Receta 4.2, "Reificar datos primitivos"
Receta 4.7, "Reificar validaciones de cadenas"
4.5 Reificar marcas de tiempo
Problema
Tu código se basa en marcas de tiempo mientras que tú sólo necesitas
secuenciación.
Solución
No utilices marcas de tiempo para secuenciar. Centraliza y bloquea tu
emisor de tiempo.
Debate
Gestionar las marcas de tiempo en distintas zonas horarias y con
escenarios de gran concurrencia es un problema bien conocido. A veces,
puedes confundir el problema de tener elementos secuenciales y
ordenados con la (posible) solución de ponerles marcas de tiempo. Como
siempre, necesitas comprender los problemas esenciales que hay que
resolver antes de adivinar implementaciones accidentales.
Una posible solución es utilizar una autoridad centralizada o algunos
complejos algoritmos de consenso descentralizados. Esta receta cuestiona
la necesidad de los sellos de tiempo cuando sólo necesitas una secuencia
ordenada. Los sellos de tiempo son muy populares en muchos idiomas y
están omnipresentes. Necesitas utilizar timestamps nativos sólo para
modelar timestamps si los encuentras en la bijección.
Aquí tienes algunos problemas con las marcas de tiempo:
import time
# ts1 and ts2 stores the time in seconds
ts1 = [Link]()
ts2 = [Link]() # might be the same!!
Aquí tienes una solución mejor sin marcas de tiempo, ya que sólo
necesitas un comportamiento secuencial:
numbers = range(1, 100000)
# create a sequence of numbers and use them with a hotspot
# or
sequence = nextNumber()
Recetas relacionadas
Receta 17.2, "Sustituir Singletons"
Receta 18.5, "Modificar la creación de la fecha global"
Receta 24.3, "Cambiar números flotantes a decimales"
4.6 Reificar subconjuntos como objetos
Problema
Modelas objetos en un dominio superconjunto y tienen mucha
duplicación de validación.
Soluciones
Crea objetos pequeños y valida un dominio restringido.
Debate
Los subconjuntos son un caso especial de un olor primitivo de obsesión.
Los objetos subconjunto están presentes en la biyección, por lo que debes
crearlos en tu simulador. Además, cuando intentes crear un objeto no
válido, debe romperse inmediatamente, siguiendo el principio de fail fast
(ver Capítulo 13, "Fail Fast"). Algunos ejemplos de violaciones de
subconjuntos son: los correos electrónicos son un subconjunto de las
cadenas, las edades válidas son un subconjunto de los números
reales y los puertos son un subconjunto de los números enteros. Los
objetos invisibles tienen reglas que debes hacer cumplir en un único
punto.
Mira este ejemplo:
validDestination = "destination@[Link]"
invalidDestination = "[Link]"
// No error is thrown
Aquí tienes una restricción de dominio mejor:
public class EmailAddress {
public String emailAddress;
public EmailAddress(String address) {
string expressions = @"^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+
([-.]\w+)*$";
if ( {
throw new Exception('Invalid email address');
}
[Link] = address;
}
}
destination = new EmailAddress("destination@[Link]");
Esta solución no debe confundirse con la versión anémica de Java. Debes
ser fiel a la biyección del mundo real.
Recetas relacionadas
Receta 4.2, "Reificar datos primitivos"
Receta 25.1, "Desinfección de entradas"
4.7 Reificar las validaciones de cadenas
Problema
Estás validando un subconjunto de strings.
Solución
Busca los objetos de dominio que faltan al validar cadenas y reifícalos.
Debate
El software serio tiene muchas validaciones de cadenas. A menudo, no
están en los lugares correctos, lo que conduce a un software frágil y
corrupto. La solución sencilla es construir sólo abstracciones válidas y del
mundo real:
// First Example: Address Validation
class Address {
function __construct(string $emailAddress) {
// String validation on Address class violates
// Single Responsibility Principle
$this->validateEmail($emailAddress);
// ...
}
private function validateEmail(string $emailAddress) {
$regex = "/[a-zA-Z0-9_-.+]+@[a-zA-Z0-9-]+.[a-zA-Z]+/";
// Regex is a sample / It might be wrong
// Emails and Urls should be first class objects
if (!preg_match($regex, $emailAddress))
{
throw new Exception('Invalid email address ' . emailAddress);
}
}
}
// Second Example: Wordle
class Wordle {
function validateWord(string $wordleword) {
// Wordle word should be a real world entity. Not a subset of
Strings
}
}
He aquí una solución mejor:
// First Example: Address Validation
class Address {
function __construct(EmailAddress $emailAddress) {
// Email is always valid / Code is cleaner and not duplicated
// ...
}
}
class EmailAddress {
// You can reuse this object many times avoiding copy-pasting
string $address;
private function __construct(string $emailAddress) {
$regex = "/[a-zA-Z0-9_-.+]+@[a-zA-Z0-9-]+.[a-zA-Z]+/";
// Regex is a sample / It might be wrong
// Emails and Urls are first class objects
if (!preg_match($regex, $emailAddress))
{
throw new Exception('Invalid email address ' . emailAddress);
}
$this->address = $emailAddress;
}
}
// Second Example: Wordle
class Wordle {
function validateWord(WordleWord $wordleword) {
// Wordle word is a real world entity. Not a subset of string
}
}
class WordleWord {
function __construct(string $word) {
// Avoid building invalid Wordle words
// For example length != 5
}
}
PRINCIPIO DE RESPONSABILIDAD ÚNICA
El principio de responsabilidad única establece que cada módulo o clase de un
sistema de software debe tener responsabilidad sobre una única parte de la
funcionalidad proporcionada por el software y esa responsabilidad debe estar
totalmente encapsulada por la clase. En otras palabras, una clase sólo debe tener
una razón para cambiar.
Los objetos pequeños son difíciles de encontrar. Pero siguen el principio
de "falla rápido" cuando intentas crear objetos no válidos. El nuevo objeto
reificado también sigue el principio de responsabilidad única y
el principio de no repetirse. Tener estas abstracciones te obliga a
implementar un comportamiento específico que ya está disponible en los
objetos que encapsula. Por ejemplo, un WordleWord no es un String,
pero puede que necesites algunas funciones.
PRINCIPIO DE NO REPETIRSE
El principio de no repetirse (DRY) establece que los sistemas de software deben
evitar la redundancia y la repetición de código. El objetivo del principio DRY es
mejorar la mantenibilidad, flexibilidad y comprensibilidad del software
reduciendo la cantidad de conocimientos, código e información duplicados.
Un contraargumento sobre la eficiencia evitando estas nuevas
indirecciones es un signo deoptimización prematura, a menos que tengas
pruebas concretas de una penalización sustancial con escenarios de uso
real de tus clientes. Crear estos pequeños conceptos nuevos mantiene el
modelo fiel a la biyección y garantiza que tus modelos estén siempre
sanos.
PRINCIPIOS SÓLIDOS
SOLID es un mnemotécnico que significa cinco principios de programación
orientada a objetos. Fueron definidos por Robert Martin y son directrices y
heurísticos, no reglas rígidas. Se definen en los capítulos relacionados:
Principio de responsabilidad única (ver Receta 4.7, "Reificar las
validaciones de cadenas")
Principio abierto-cerrado (ver Receta 14.3, "Reificar variables
booleanas")
Principio de sustitución de Liskov (ver Receta 19.1, "Romper la
herencia profunda")
Interfaz principio de segregación (ver Receta 11.9, "Romper interfaces
gordas")
Dependencia principio de inversión (ver Receta 12.4, "Eliminar
interfaces de un solo uso")
Recetas relacionadas
Receta 4.4, "Eliminar los abusos de las cadenas"
Receta 6.10, "Documentar expresiones regulares"
4.8 Eliminar propiedades innecesarias
Problema
Tienes objetos creados en función de sus propiedades en lugar de su
comportamiento.
Solución
Elimina las propiedades accidentales. Añade el comportamiento necesario
y luego añade propiedades accidentales para apoyar el comportamiento
definido.
Debate
Muchas escuelas de programación te dicen que identifiques rápidamente
las partes de los objetos y luego construyas funciones a su alrededor. Tales
modelos suelen estar acoplados y son menos mantenibles que los creados
en función del comportamiento deseado. Siguiendo la premisa
de YAGNI(ver Capítulo 12, "YAGNI"), verás que muchas veces no necesitas
estos atributos.
Cada vez que quieren modelar a una persona o a un empleado, los
programadores junior o los estudiantes añaden un atributo id o nombre
sin pensar si realmente los van a necesitar. Tienes que añadir atributos
"bajo demanda" cuando haya suficientes pruebas de comportamiento. Los
objetos no son "soportes de datos".
Este es un ejemplo clásico de enseñanza:
class PersonInQueue
attr_accessor :name, :job
def initialize(name, job)
@name = name
@job = job
end
end
Si empiezas a centrarte en el comportamiento, podrás construir mejores
modelos:
class PersonInQueue
def moveForwardOnePosition
# implement protocol
end
end
Una técnica asombrosa para descubrir comportamientos es el desarrollo
dirigido por pruebas, en el que te ves obligado a empezar a iterar el
comportamiento y el protocolo y a aplazar todo lo que puedas la
implementación accidental.
DESARROLLO BASADO EN PRUEBAS
El desarrollo dirigido por pruebas (TDD) es un proceso de desarrollo de
software que se basa en la repetición de un ciclo de desarrollo muy corto:
primero, el desarrollador escribe un caso de prueba automatizado que falla y
que define una mejora deseada o un nuevo comportamiento, luego produce un
código de producción mínimo para superar esa prueba y, por último, refactoriza
el nuevo código para que cumpla unos estándares aceptables. Uno de los
principales objetivos de TDD es facilitar el mantenimiento del código
asegurándose de que está bien estructurado y sigue unos buenos principios de
diseño. También ayuda a detectar defectos en una fase temprana del proceso de
desarrollo, ya que cada nuevo fragmento de código se prueba en cuanto se
escribe.
Recetas relacionadas
Receta 3.1, "Convertir objetos anémicos en objetos ricos"
Receta 3.5, "Eliminar propiedades automáticas"
Receta 3.6, "Eliminar DTOs"
Receta 17.17, "Convertir objetos fungibles"
4.9 Crear intervalos de fechas
Problema
Tienes que modelar intervalos del mundo real y tienes información como
"desde la fecha" y "hasta la fecha", pero no invariantes como "desde
la fecha debe ser menor que hasta la fecha".
Solución
Reifica este pequeño objeto y respeta la regla MAPPER.
Debate
Esta receta presenta una abstracción muy común que podrías pasar por
alto y tiene los mismos problemas que has visto en las otras recetas de
este capítulo: abstracciones que faltan, código duplicado, invariante no
aplicada (consulta la Receta 13.2, "Aplicar precondiciones"), obsesión
primitiva y violación del principio "fail fast". La restricción "la fecha de
inicio debe ser inferior a la fecha de finalización" significa que la fecha de
inicio de un determinado intervalo debe ser anterior a la fecha de
finalización del mismo intervalo.
La "fechadesde" debe ser una fecha anterior en el tiempo a la
"fechahasta ". Esta restricción existe para garantizar que el intervalo que
se define tiene sentido lógico y que las fechas utilizadas para definirlo
están en el orden correcto. Lo sabes, pero te olvidas de crear el
objeto Interval. ¿Crearías un Date como un par de tres números
enteros? Desde luego que no.
He aquí un ejemplo anémico:
val from = [Link](2018, 12, 9)
val to = [Link](2022, 12, 22)
val elapsed = elapsedDays(from, to)
fun elapsedDays(fromDate: LocalDate, toDate: LocalDate): Long {
return [Link](fromDate, toDate)
}
// You need to apply this short function
// or the inline version many times in your code
// You don't check fromDate to be less than toDate
// You can make accounting numbers with a negative value
Después de reificar el objeto Interval:
data class Interval(val fromDate: LocalDate, val toDate: LocalDate) {
init {
if (fromDate >= toDate) {
throw IllegalArgumentException("From date must be before
to date")
}
// Of course the Interval must be immutable
// By using the keyword 'data'
}
fun elapsedDays(): Long {
return [Link](fromDate, toDate)
}
}
val from = [Link](2018, 12, 9)
val to = [Link](2002, 12, 22)
val interval = Interval(from, to) // Invalid
Este es un olor a obsesión primitiva y está relacionado con la forma en
que modelas las cosas. Si encuentras un software al que le
faltan validaciones sencillas, sin duda necesita alguna reificación.
Recetas relacionadas
Receta 4.1, "Crear objetos pequeños"
Receta 4.2, "Reificar datos primitivos"
Receta 10.1, "Eliminar código repetido"
Capítulo 5. Mutabilidad
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y
comentarios: translation-feedback@[Link]
Ninguna persona pisa dos veces el mismo río. Porque no es el mismo río y
no es la misma persona.
Heráclito
5.0 Introducción
Desde el principio del concepto de programa almacenado, has aprendido
que el software son programas más datos. Está claro que sin datos no hay
software. En la programación orientada a objetos construyes modelos que
evolucionan con el tiempo, emulando los conocimientos que aprendes
observando la realidad que estás representando. Sin embargo, manipulas
y a veces abusas incontroladamente de esos cambios, violando el único
principio de diseño importante al generar representaciones incompletas
(y por tanto inválidas) y propagando el efecto dominó con tus cambios.
En el paradigma funcional, esto se aborda elegantemente prohibiendo las
mutaciones. Puedes ser (un poco) menos drástico. Siendo fiel a la
biyección en tu modelo computable, como se define en el capítulo 2,
deberías ser capaz de distinguir cuándo cambia un objeto en función de
lo accidental y prohibir todos los cambios esenciales (porque violarían el
principio de biyección).
La inmutabilidad es una propiedad estricta en la programación funcional,
y muchos lenguajes orientados a objetos están desarrollando
herramientas para favorecerla. Sin embargo, muchos de ellos tienen
restos en las clases básicas como Date o String. Los objetos deben saber
defenderse de las representaciones no válidas. Son los poderes contra los
mutantes.
Repasemos la clase Date en los lenguajes más utilizados en la industria
actual:
Ve a
Date es una estructura.
Java
Mutable (obsoleto).
PHP
Mutable con abuso de setters.
Python
Mutable (todos los atributos son públicos en Python).
JavaScript
Mutable con abuso de setters.
Swift
Mutable.
La representación del dominio del tiempo es probablemente uno de los
retos más antiguos y mejores conocidos por la humanidad. El hecho de
que estos getters estén siendo desaprobados según la documentación
oficial de algunos de estos lenguajes habla de un mal diseño inicial en la
mayoría de los lenguajes modernos.
Un enfoque diferente de la mutabilidad
Un posible ataque consiste en invertir la carga de la prueba. Los objetos
son completamente inmutables, salvo que se indique lo contrario. Si
evolucionan, deben hacerlo siempre en sus aspectos accidentales. Nunca
en su esencia. Este cambio no debe ir unido a todos los demás objetos que
lo utilicen (véase la Receta 3.2, "Identificar la esencia de tus objetos").
Si un objeto ha estado completo desde su creación, siempre podrá realizar
funciones. Un objeto debe representar correctamente a la entidad desde
su creación. Si trabajas en un entorno concurrente, es esencial que los
objetos sean siempre válidos. Un objeto debe ser inmutable si la entidad
que representa es inmutable, y la mayoría de las entidades del mundo
real lo son. La propiedad de inmutabilidad forma parte de la biyección.
Estas reglas mantienen la coherencia del modelo con su representación.
Como corolario de la demostración, puedes derivar una serie de reglas:
Corolario 1: Los objetos deben estar completos desde su
creación(Receta 5.3, "Prohibición de cambios en la esencia").
Corolario 2: Los definidores no deben existir(Receta 3.3,
"Eliminar los definidores de los objetos").
Corolario 3: Los Getters no deben existir (a menos que existan en
el mundo real y entonces la biyección sea válida). No es
responsabilidad de ninguna entidad real responder a
un getXXX() mensaje, ya que la responsabilidad de get() no
forma parte del comportamiento de ningún objeto(Receta 3.8,
"Eliminar Getters").
5.1 Cambiar var por const
Problema
Tienes constantes variables declaradas con var.
Solución
Elige sabiamente los nombres de tus variables, su ámbito y su
mutabilidad.
Debate
Muchos lenguajes admiten el concepto de variables y constantes. Definir
un ámbito correcto es clave para seguir el principio "fail fast". La mayoría
no necesitan declaraciones de variables y otros lenguajes permiten
declarar la mutabilidad, pero debes ser estricto y explícito con tus
declaraciones y declarar todas las variables const a menos que necesites
cambiarlas.
Aquí puedes ver cómo la reasignación de una variable no se plantea como
un problema donde debería:
var pi = 3.14
var universeAgeInYears = [Link]
pi = 3.1415 // no error
universeAgeInYears = [Link] // no error
Un enfoque correcto sería definirlos como const:
const pi = 3.14 // Value cannot mutate or change
let universeAgeInYears = [Link] // Value can change
pi = 3.1415 // error. cannot define
universeAgeInYears = [Link] // no error
Con la prueba de mutación forzando una declaración const, puedes
comprobar si un valor permanece constante y ser más declarativo
forzándolo explícitamente. Algunos lenguajes tienen convenciones por las
que las constantes se definen en mayúsculas. Deberías seguirlas, ya que la
legibilidad es siempre muy importante y necesitas declarar
explícitamente tus intenciones y usos.
PRUEBAS DE MUTACIÓN
Las pruebas de mutación son una técnica que puedes utilizar para valorar la
calidad de tus pruebas unitarias. Consiste en introducir pequeños cambios
controlados (llamados "mutaciones") en el código que estás probando y
comprobar si tus pruebas unitarias existentes pueden detectar esos cambios.
Puede ayudarte a identificar áreas del código en las que necesitas pruebas
adicionales, y puedes utilizarla como medida de la calidad de tus pruebas
existentes.
Una mutación consiste en cambiar una pequeña parte del código (por ejemplo,
negar un booleano, sustituir una operación aritmética, sustituir un valor por
nulo, etc.) y ver si falla alguna prueba.
Recetas relacionadas
Receta 5.2, "Declarar variables para que sean variables"
Receta 5.4, "Evitar las matrices const mutables"
5.2 Declarar que las variables son
variables
Problema
Puedes asignar un valor a una variable y utilizarlo, pero nunca cambiarlo.
Solución
Utiliza selectores de mutabilidad si tu lenguaje de programación lo
permite.
Debate
Debes respetar la bijección mutabilidad, cambiar la variable por una
constante y tener claro su ámbito. Siempre estás aprendiendo del ámbito,
y a veces puedes adivinar que un valor puede cambiar con el MAPPER,
como se define en el Capítulo 2. Más tarde, te enteras de que no cambiará
y, por tanto, necesitas promoverlo a constante. Esto evita las
constantes mágicas(ver Receta 6.8, "Sustituir los números mágicos por
constantes").
En el siguiente ejemplo de código, puedes ver una contraseña definida
como una variable donde nunca cambiará:
function configureUser() {
$password = '123456';
// Setting a password on a variable is a vulnerability
$user = new User($password);
// Notice variable doesn’t change
}
Puedes aplicar la receta y declarar que el valor es constante:
define("USER_PASSWORD", '123456')
function configureUser() {
$user = new User(USER_PASSWORD);
}
// or
function configureUser() {
$user = new User(userPassword());
}
function userPassword() : string {
return '123456';
}
LINTERS DE SOFTWARE
Un linter de software comprueba automáticamente el código fuente en busca de
problemas previamente definidos en . El objetivo de un linter es ayudarte a
detectar errores en una fase temprana del proceso de desarrollo, antes de que
sean más difíciles y costosos de solucionar. Puedes configurar tu linter para que
compruebe una amplia gama de problemas, como el estilo de codificación, las
convenciones de nomenclatura y las vulnerabilidades de seguridad. Puedes
utilizar la mayoría de los linters como complementos de tu IDE, y también
pueden añadir valor como pasos en el proceso de integración
continua/implantación continua. También puedes conseguir los mismos
resultados con muchas herramientas de aprendizaje automático generativo,
como ChatGPT, Bard y muchas otras.
Muchos linters de código pueden ayudarte a comprobar si la variable sólo
tiene una asignación, y también puedes realizar pruebas de mutación
(véase la Receta 5.1, "Cambiar var a const") e intentar modificar la
variable para ver si se rompen las pruebas automatizadas. Debes
desafiarte a ti mismo y refactorizar cuando el ámbito de la variable esté
claro y aprendas más sobre sus propiedades y mutabilidad.
INTEGRACIÓN CONTINUA E IMPLEMENTACIÓN CONTINUA
El canal CI/CD (integración continua e implementación continua) automatiza el
proceso de desarrollo, prueba e implementación del software. El canal está
diseñado para agilizar el proceso de desarrollo de software, automatizar tareas,
mejorar la calidad del código y agilizar y gestionar la implementación de nuevas
funciones y correcciones en distintos entornos.
Recetas relacionadas
Receta 5.1, "Cambiar var por const"
Receta 5.6, "Congelar constantes mutables"
Receta 6.1, "Limitar las variables reutilizadas"
Receta 6.8, "Sustituir números mágicos por constantes"
5.3 Prohibir cambios en la esencia
Problema
Tus objetos mutan su esencia.
Solución
Prohíbe cambiar los atributos esenciales una vez fijados.
Debate
Como has visto en la Receta 3.2, "Identificar la esencia de tus objetos", no
debes cambiar la esencia de un objeto después de crearlo si esto no es
posible en el mundo real. Favorece los objetos inmutables para evitar el
efecto dominó y favorecer la transparencia referencial (ver Receta 5.7,
"Eliminar los efectos secundarios"). Los objetos sólo deben mutar de
forma accidental, no esencial.
ESENCIA DEL OBJETO
Describir la esencia de un objeto puede ser un reto, ya que requiere un profundo
conocimiento del dominio al que pertenece. Si puedes eliminar un
comportamiento y el objeto sigue realizando lo mismo, entonces el
comportamiento eliminado no es esencial. Como las propiedades están
acopladas al comportamiento, seguirán la misma regla. Un coche puede cambiar
de color y será el mismo coche, pero sería difícil cambiar el modelo, el número
de serie, etc. Pero esto es muy subjetivo, porque el mundo real también lo es, y
así es como funciona la ingeniería. Se trata de un proceso de ingeniería y no
científico.
Si recuerdas la metáfora Date:
const date = new Date();
[Link](4);
La referencia al objeto date es constante y siempre apuntará a la misma
fecha. La referencia no puede cambiar, pero el objeto date sí, utilizando
cualquier método que cambie su estado interno, en este
ejemplo, setMonth(). Tienes que eliminar todos los setters que cambien
atributos esenciales (consulta la Receta 3.3, "Eliminar setters de los
objetos"):
class Date {
// setMonth(month) {
// [Link] = month;
// }
// REMOVED
}
A partir de ahora, la fecha no puede cambiar y todas las referencias a ella
permanecerán vinculadas a la fecha original asignada.
ADVERTENCIA
Eliminar los setters en el código de producción puede crear defectos. Puedes
hacer pequeñas refactorizaciones que ajusten la creación de objetos y ejecutar
tus pruebas automatizadas.
Recetas relacionadas
Receta 17.11, "Evitar el efecto dominó"
5.4 Evitar las matrices const mutables
Problema
Puedes declarar una matriz como const pero puede mutar.
Solución
Ten mucho cuidado con las directivas de mutabilidad del lenguaje y
comprende su alcance.
Debate
Algunos lenguajes declaran que las referencias son constantes, pero esto
no equivale a inmutabilidad. En su lugar, puedes utilizar el operador de
dispersión en JavaScript:
const array = [1, 2];
[Link](3)
// array => [1, 2, 3]
// Wasn't it constant ?
// constant != immutable ?
La variable array está definida como una constante, lo que significa que
su referencia no puede reasignarse a un valor distinto. Sin embargo,
cuando se asigna una matriz o un objeto a una variable constante, aún
puedes modificar las propiedades del objeto o de los elementos de la
matriz, porque la variable constante mantiene en memoria la referencia
al objeto o a la matriz, pero no a la matriz o al objeto en sí. Por tanto,
cuando llamas a [Link](3), estás modificando el array al que hace
referencia la variable constante array, pero no reasignando la propia
variable.
OPERADOR DE LA DISPERSIÓN
El operador de expansión en JavaScript se representa mediante tres puntos
(...). Permite expandir un iterable (como una matriz o una cadena) en lugares
donde se esperan cero o más elementos (o caracteres). Por ejemplo, puedes
utilizarlo para combinar matrices, copiar matrices, insertar elementos en una
matriz o expandir propiedades de un objeto.
Aquí tienes un ejemplo más declarativo:
const array = [1, 2];
const newArray = [...array, 3]
// array => [1, 2] Didn't mutate
// newArray = [1, 2, 3]
El operador de dispersión crea una copia superficial de la matriz original,
de modo que las dos matrices son diferentes e independientes entre sí.
Siempre debes favorecer la inmutabilidad en tus diseños y tener especial
cuidado con los efectos secundarios, ya que se trata de una "característica
del lenguaje".
COPIA SUPERFICIAL
Una copia superficial es una copia de un objeto que crea una nueva referencia a
la misma ubicación de memoria donde está almacenado el objeto original. Tanto
el objeto original como su copia superficial comparten los mismos valores. Los
cambios que hagas en los valores de uno se reflejarán en el otro. En cambio, una
copia profunda crea una copia completamente independiente del objeto original,
con sus propias propiedades y valores. Los cambios que realices en las
propiedades o valores del objeto original no afectarán a la copia profunda, y
viceversa.
Recetas relacionadas
Receta 5.1, "Cambiar var por const"
Receta 5.6, "Congelar constantes mutables"
Ver también
"Sintaxis de propagación (...)" en [Link]
5.5 Eliminar la inicialización perezosa
Problema
Utiliza la inicialización perezosa para recuperar objetos caros cuando los
necesites y no antes.
Solución
No utilices la inicialización perezosa. Utiliza en su lugar un proveedor de
objetos.
Debate
INICIALIZACIÓN PEREZOSA
Utilizando la inicialización perezosa retrasas la creación de un objeto o el cálculo
de un valor hasta que sea realmente necesario, en lugar de hacerlo
inmediatamente. Esto se suele utilizar para optimizar el uso de recursos y
mejorar el rendimiento, aplazando el proceso de inicialización hasta el último
momento posible.
La inicialización perezosa tiene varios problemas como problemas de
concurrencia y condiciones de carrera si varios subprocesos intentan
acceder e inicializar el objeto al mismo tiempo. También hace que el
código sea más complejo, ya que es un ejemplo clásico de optimización
prematura(Ver Capítulo 16, "Optimización prematura"). En algunas
ocasiones, esto puede dar lugar a situaciones en las que un subproceso
esté esperando a que otro subproceso inicialice un objeto, lo que puede
provocar un bloqueo si este último subproceso también está esperando a
que el primero inicialice un objeto diferente.
He aquí un ejemplo muy sencillo de que utiliza la inicialización perezosa
incorporada en Python:
class Employee
def emails
@emails ||= []
end
def voice_mails
@voice_mails ||= []
end
end
En Python, puedes utilizar una propiedad o un descriptor para retrasar la
creación del recurso hasta que accedas a él por primera vez. El
método emails utiliza el operador ||=, que es una abreviatura de "o igual".
Asigna un valor a una variable sólo si es nulo o falso. En este caso, asigna
una matriz vacía [] a la variable de instancia @emails si es nula.
Aquí tienes el mismo ejemplo con un lenguaje que no admita
explícitamente la inicialización perezosa:
class Employee {
constructor() {
[Link] = null;
[Link] = null;
}
getEmails() {
if (![Link]) {
[Link] = [];
}
return [Link];
}
getVoiceMails() {
if (![Link]) {
[Link] = [];
}
return [Link];
}
}
Deberías eliminar por completo el mecanismo de inicialización perezosa e
inicializar los atributos esenciales al construir, como hemos visto en otras
recetas:
class Employee
attr_reader :emails, :voice_mails
def initialize
@emails = []
@voice_mails = []
end
end
# You can also inject a design pattern to externally deal
# with voice_mails so you can mock it in your tests
La inicialización perezosa es un patrón común que puedes utilizar para
comprobar si hay una variable no inicializada. Pero debes evitar las
optimizaciones prematuras. Si tienes verdaderos problemas de
rendimiento, debes utilizar un patrón proxy, un patrón de fachada o una
solución más independiente en lugar de singletons. Los singletons son
otro antipatrón que suele combinarse con la inicialización perezosa
(véase la Receta 17.2, "Sustitución de los singletons").
ANTIPATRONES
Un antipatrón de software es un patrón de diseño que inicialmente puede
parecer una buena idea, pero que al final tiene consecuencias negativas. En un
principio, muchos expertos los presentaron como buenas soluciones, pero hoy
en día hay pruebas contundentes en contra de su uso.
Recetas relacionadas
Receta 15.1, "Crear objetos nulos"
Receta 17.2, "Sustituir Singletons"
5.6 Congelar constantes mutables
Problema
En declaras algo como una constante utilizando la palabra clave const.
Pero puedes mutar partes de ella.
Solución
Utiliza constantes inmutables.
Debate
Probablemente aprendiste a declarar constantes en tu primer curso de
programación informática. Como siempre, no es importante que algo sea
constante. Lo importante es que no mute. Esta receta varía en los distintos
lenguajes de programación. JavaScript es famoso por no seguir el
principio de la menor sorpresa. Por lo tanto, el siguiente comportamiento
no es sorprendente en absoluto:
const DISCOUNT_PLATINUM = 0.1;
const DISCOUNT_GOLD = 0.05;
const DISCOUNT_SILVER = 0.02;
// Since variables are constants you cannot reassign them
const DISCOUNT_PLATINUM = 0.05; // Error
// You can group them
const ALL_CONSTANTS = {
DISCOUNT: {
PLATINUM = 0.1;
GOLD = 0.05;
SILVER = 0.02;
},
};
const ALL_CONSTANTS = 3.14; // Error
ALL_CONSTANTS.[Link] = 0.08; // NOT AN ERROR. OOPS!
const ALL_CONSTANTS = [Link]({
DISCOUNT:
PLATINUM = 0.1;
GOLD = 0.05;
SILVER = 0.02;
});
const ALL_CONSTANTS = 3.14; // Error
ALL_CONSTANTS.[Link] = 0.12; // NOT AN ERROR. OOPS!
Debes tener cuidado con las constantes dentro de las constantes:
export const ALL_CONSTANTS = [Link]({
DISCOUNT: [Link]({
PLATINUM = 0.1;
GOLD = 0.05;
SILVER = 0.02;
}),
});
const ALL_CONSTANTS = 3.14; // Error
ALL_CONSTANTS.[Link] = 0.12; // ERROR
// Code works, but it is coupled and you cannot test it
Class TaxesProvider {
applyPlatinum(product);
}
// Now you can couple to an interface (the protocol of taxes provider)
// Since class has no setters it is constant and immutable
// And you can replace it on tests
Este complicado comportamiento sólo se da en algunos lenguajes como
JavaScript. Puedes realizar pruebas de mutación (véase la Receta 5.1,
"Cambiar var a const") para encontrar valores cambiados como en la
receta anterior, y necesitas reforzar la mutabilidad con las herramientas
adecuadas.
EL PRINCIPIO DE LA MENOR SORPRESA
El principio de la menor sorpresa o principio del menor asombro dice que un
sistema debe comportarse de la forma menos sorprendente para sus usuarios y
coherente con las expectativas de éstos. Si sigues este principio, el usuario puede
predecir fácilmente lo que ocurrirá cuando interactúe con el sistema. Como
desarrollador, debes crear software más intuitivo y fácil de usar, lo que
aumentará la satisfacción del usuario y su productividad.
Recetas relacionadas
Receta 5.4, "Evitar las matrices const mutables"
Receta 6.1, "Limitar las variables reutilizadas"
Receta 6.8, "Sustituir números mágicos por constantes"
5.7 Eliminar los efectos secundarios
Problema
En tienes funciones que ejecutan efectos secundarios.
Solución
Evita los efectos secundarios.
Debate
Los efectos secundarios conllevan acoplamiento, resultados inesperados y
violan el principio de la menor sorpresa (ver Receta 5.6, "Congelar
constantes mutables"). También pueden generar conflictos en un entorno
multiproceso. Puedes favorecer la transparencia referencial
interactuando cuidadosamente sólo contigo mismo y con tus argumentos.
TRANSPARENCIA REFERENCIAL
Las funcionesde transparencia referencial siempre producen la misma salida
para una entrada dada y no tienen efectos secundarios, como modificar
variables globales o realizar operaciones de E/S. En otras palabras, una función o
expresión es referencialmente transparente si puede sustituirse por su resultado
evaluado sin cambiar el comportamiento del programa. Éste es un concepto
fundamental en los paradigmas de programación funcional, en los que las
funciones se tratan como expresiones matemáticas que asignan entradas a
salidas.
Aquí puedes ver una función que afecta tanto a una variable global como
a un recurso externo:
let counter = 0;
function incrementCounter(value: number): void {
// Two side effects
counter += value; // it modifies the global variable counter
[Link](`Counter is now ${counter}`); // it logs a message to
the console
}
Al evitar todos los efectos secundarios, tu función es reentrante y
predecible:
let counter = 0;
function incrementCounter(counter: number, value: number): number {
return counter + value; // Not too efficient
}
La mayoría de los linters pueden avisarte cuando algo accede al estado
global o a funciones y crea efectos secundarios. La programación
funcional es increíble y puede enseñar mucho sobre cómo escribir código
limpio.
Recetas relacionadas
Receta 18.1, "Reificar funciones globales"
5.8 Evitar la elevación
Problema
Utilizas variables antes de su declaración.
Solución
Declara tus variables y vigila el ámbito.
Debate
La sobrecarga perjudica la legibilidad y viola el principio de menor
sorpresa. Siempre debes ser explícito en tus declaraciones de variables,
utilizar declaraciones const cuando sea posible (véase la Receta 5.1,
"Cambiar var por const"), y declarar las variables al principio del
ámbito. La elevación permite mover las declaraciones de variables a la
parte superior del ámbito que las contiene durante la fase de
compilación. En varios lenguajes, las variables declaradas con var y las
declaraciones de funciones se "elevan" automáticamente a la parte
superior de sus respectivos ámbitos.
En este ejemplo utilizas una variable antes de definirla:
[Link](willBeDefinedLater);
// Output: undefined (but no error)
var willBeDefinedLater = "Beatriz";
[Link](willBeDefinedLater);
// Output: "Beatriz"
Utilizando la declaración explícita const:
const dante = "abandon hope all ye who enter here";
// Declaring a constant 'dante'
// with value "abandon hope all ye who enter here"
[Link](dante);
// Output: "abandon hope all ye who enter here"
dante = "Divine Comedy"; // Error: Assignment to constant variable
Puedes realizar pruebas de mutación para comprobar si al cambiar el
ámbito de las variables se obtienen resultados inesperados. La elevación
es otra herramienta mágica que algunos compiladores proporcionan para
favorecer a los programadores perezosos. Pero se defiende a la hora de
depurar.
Recetas relacionadas
Receta 5.1, "Cambiar var por const"
Receta 21.3, "Eliminar la advertencia/desactivación estricta"
Capítulo 6. Código declarativo
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y
comentarios: translation-feedback@[Link]
El comportamiento es lo más importante del software. Es de lo que
dependen los usuarios. A los usuarios les gusta que añadamos
comportamiento (siempre que sea lo que realmente querían), pero si
cambiamos o eliminamos el comportamiento del que dependen
(introducimos errores), dejan de confiar en nosotros.
Michael Feathers, Trabajar eficazmente con código heredado
6.0 Introducción
El código declarativo es un tipo de código de programación que
describe lo que debe hacer un programa , en lugar de especificar los
pasos que debe seguir el programa para realizar una tarea. El código se
centra en el resultado deseado (qué), más que en el proceso para lograr
ese resultado (cómo). El código declarativo es más fácil de leer y
comprender que el imperativo, que especifica los pasos que debe seguir
un programa para realizar una tarea. El código también es más conciso y
se centra en el resultado final, más que en los detalles específicos de cómo
se consigue ese resultado.
El código declarativo se utiliza a menudo en lenguajes de programación
que admiten la programación funcional, que es un paradigma de
programación que hace hincapié en el uso de funciones para describir el
cálculo de un programa. Ejemplos de lenguajes de programación
declarativos son SQL, que se utiliza para gestionar bases de datos,
y HTML, que se utiliza para estructurar y formatear documentos para la
web.
El desarrollo de software arrastra una inercia de tiempos en los que
necesitabas escribir software en lenguajes de bajo nivel debido a
restricciones de tiempo y espacio. Esto ya no es así, pues los modernos
compiladores y máquinas virtuales son más inteligentes que nunca y te
dejan a ti la importante tarea de escribir código de alto nivel, declarativo
y limpio.
6.1 Reducir las variables reutilizadas
Problema
Estás reutilizando la misma variable en ámbitos diferentes.
Solución
No debes leer y escribir la misma variable con fines distintos, ya que
debes intentar definir el alcance mínimo (tiempo de vida) de todas las
variables locales.
Debate
Reutilizar variables hace que los ámbitos y límites sean más difíciles de
seguir y también impide que las herramientas de refactorización
extraigan bloques de código independientes. Cuando programas un script
es habitual que reutilices variables. Tras algunas operaciones de cortar y
pegar, es posible que obtengas bloques contiguos. La raíz del problema es
copiar código. En su lugar, debes aplicar la Receta 10.1, "Eliminar código
repetido". Como regla general, debes reducir el ámbito todo lo posible, ya
que un ámbito ampliado lleva a confusión y dificulta la depuración.
En este ejemplo de código, puedes ver que se reutiliza la variable total:
// print line total
double total = [Link]() * [Link]();
[Link]("Line total: " + total);
// print amount total
total = [Link]() - [Link]();
[Link]( "Amount due: " + total );
// 'total' variable is reused
Debes reducir el ámbito de la variable y dividirla en dos bloques
diferentes. Puedes conseguirlo si los extraes utilizando la Receta 10.7,
"Extraer un método a un objeto":
function printLineTotal() {
double lineTotal = [Link]() * [Link]();
[Link]("Line total: " + lineTotal);
}
function printAmountTotal() {
double amountTotal = [Link]() - [Link]();
[Link]("Amount due: " + amountTotal);
}
Como norma general, debes evitar reutilizar los nombres de las variables.
Utiliza nombres más locales, específicos y que revelen tu intención.
Recetas relacionadas
Receta 10.1, "Eliminar código repetido"
Receta 10.7, "Extraer un método a un objeto"
Receta 11.1, "Romper métodos demasiado largos"
Receta 11.3, "Reducir el exceso de variables"
INTENCIÓN-REVELACIÓN
El códigoque revela intenciones comunica claramente su propósito o intención
a otros desarrolladores que puedan leer o trabajar con tu código en el futuro. El
objetivo de revelar la intención del código es hacerlo más conductual,
declarativo, legible, comprensible y fácil de mantener.
6.2 Eliminar líneas vacías
Problema
Tienes grandes trozos de código separados por líneas vacías.
Solución
Utiliza la Receta 10.7, "Extraer un método a un objeto", para romper los
bloques de comportamiento identificados por las líneas en blanco.
Debate
Las funciones más cortas favorecen la legibilidad, incrementan la
reutilización de y siguen el principio KISS. Aquí tienes un ejemplo en el
que puedes agrupar trozos contiguos de código separados por líneas en
blanco:
function translateFile() {
$this->buildFilename();
$this->readFile();
$this->assertFileContentsOk(); // A lot more lines
// Empty space to pause definition
$this->translateHyperlinks();
$this->translateMetadata();
$this->translatePlainText();
// Yet another empty space
$this->generateStats();
$this->saveFileContents(); // A lot more lines
}
Puedes cambiarlo utilizando la Receta 10.7, "Extraer un método a un
objeto", para obtener una versión más corta, agrupando los trozos:
function translateFile() {
$this->readFileToMemory();
$this->translateContents();
$this->generateStatsAndSaveFileContents();
}
Si utilizas un linter, puedes configurarlo para que te avise cuando utilices
líneas en blanco y también cuando los métodos sean demasiado
largos. Las líneas en blanco son inofensivas, pero presentan una
oportunidad para romper el código en pequeños pasos. Si rompes tu
código con comentarios en lugar de (o además de) líneas en blanco, esto es
un olor a código que pide una refactorización (véase la Receta 8.6,
"Eliminar comentarios dentro de los métodos").
EL PRINCIPIO KISS
El principio KISS es la abreviatura de "Keep It Simple, Stupid" (Mantenlo Simple,
Estúpido). Aconseja que los sistemas de funcionan mejor cuando se mantienen
sencillos en lugar de complicarlos. Los sistemas más sencillos son más fáciles de
entender, utilizar y mantener que los complejos, y por tanto es menos probable
que fallen o produzcan resultados inesperados.
Recetas relacionadas
Receta 8.6, "Eliminar comentarios dentro de métodos"
Receta 10.7, "Extraer un método a un objeto"
Receta 11.1, "Romper métodos demasiado largos"
Ver también
Puedes encontrar esta receta explicada en detalle en el libro Clean
Code de Robert Martin.
6.3 Eliminar métodos versionados
Problema
Tienes métodos nombrados con marcas de tiempo de
versión como sort, sortOld, sort20210117, sortFirstVersion, workingSort,
etc.
Solución
Elimina el versionado del nombre y utiliza en su lugar el software de
control de versiones .
Debate
Las funciones versionadas perjudican la legibilidad y la
mantenibilidad. Debes mantener una sola versión de trabajo de tu
artefacto (clase, método, atributo) y dejar el control del tiempo a tu
sistema de control de versiones. Si tienes código que utiliza métodos
versionados como éste
findMatch()
findMatch_new()
findMatch_newer()
findMatch_newest()
findMatch_version2()
findMatch_old()
findMatch_working()
findMatch_for_real()
findMatch_20200229()
findMatch_thisoneisnewer()
findMatch_themostnewestone()
findMatch_thisisit()
findMatch_thisisit_for_real()
Debes sustituir todas las apariciones por la más sencilla:
findMatch()
Como muchos otros patrones, puedes crear una política interna y
comunicarla claramente; también puedes añadir reglas automáticas para
encontrar métodos versionados con patrones. La gestión del tiempo y de
la evolución del código siempre está presente en el desarrollo de software.
Por suerte, hoy en día dispones de herramientas maduras para abordar
este problema.
SISTEMA DE CONTROL DE FUENTES DE SOFTWARE
Un sistema de control de código fuente de software es una herramienta que
permite a los desarrolladores hacer un seguimiento de los cambios realizados en
el código fuente de un proyecto de software. Puedes trabajar con muchos otros
desarrolladores al mismo tiempo en el mismo código base, lo que favorece la
colaboración, la reversión de cambios y la gestión de distintas versiones del
código. En la actualidad, Git es el sistema más utilizado.
Recetas relacionadas
Receta 8.5, "Convertir comentarios en nombres de funciones"
6.4 Eliminar los dobles negativos
Problema
Tienes un método con el nombre de una condición negativa y necesitas
asegurarte de que no se produce.
Solución
Nombra siempre tus variables, métodos y clases con nombres positivos.
Debate
Esta receta es para facilitar la lectura; tu cerebro puede equivocarse al
leer condiciones negativas. He aquí un ejemplo de doble negativo:
if (![Link]())
Cuando lo conviertes en positivo:
if ([Link]())
Puedes decirle a tu linter que compruebe las expresiones
regulares como !not o !isNot como advertencia. Debes confiar en la
cobertura de las pruebas y crear renombramientos seguros y
otras refactorizaciones.
Recetas relacionadas
Receta 10.4, "Eliminar la astucia del código"
Receta 14.3, "Reificar variables booleanas"
Receta 14.11, "Evitar que se devuelvan valores booleanos en
las comprobaciones de condiciones"
Receta 24.2, "Tratar con valores de verdad"
Ver también
"Eliminar el doble negativo" en [Link]
6.5 Cambiar las responsabilidades
equivocadas
Problema
Tienes métodos en los objetos equivocados.
Solución
Crea o sobrecarga los objetos adecuados para encontrar el lugar correcto
utilizando el MAPPER.
Debate
Encontrar objetos responsables es una tarea difícil. Debes responder a la
pregunta "¿De quién es la responsabilidad...?". Si hablas con alguien ajeno
al mundo del software, podrá darte una pista sobre dónde debes colocar
cada responsabilidad. Los ingenieros de software, por el contrario,
tienden a colocar las conductas en lugares extraños... ¡como ayudantes!
He aquí algunos ejemplos para la responsabilidad add:
Number>>#add: a to: b
^ a + b
// This is natural in many programming languages, but unnatural in
real life
Aquí tienes un enfoque diferente:
Number>>#add: adder
^ self + adder
// This won't compile in some programming languages
// because they usually forbid changing some behavior to base classes
// But it is the right place for the 'add' responsibility
Hay algunos lenguajes en los que puedes añadir la responsabilidad sobre
tipos primitivos y, si colocas las responsabilidades en el objeto adecuado,
seguramente las encontrarás en el mismo lugar. Aquí tienes otro ejemplo
en el que se define la constante PI:
class GraphicEditor {
constructor() {
[Link] = 3.14;
// You shouldn't define the constant here
}
pi() {
return [Link];
// Not this object's responsibility
}
drawCircle(radius) {
[Link]("Drawing a circle with radius ${radius} " +
"and circumference " + (2 * [Link]()) * radius");
}
}
Cuando trasladas la responsabilidad de a un objeto RealConstants evitas
código repetido:
class GraphicEditor {
drawCircle(radius) {
[Link]("Drawing a circle with radius " + radius +
" and circumference " + (2 * [Link]() * radius));
}
}
// PI's definition is RealConstants (or Number or similar)
responsibility
class RealConstants {
pi() {
return 3.14;
}
}
Recetas relacionadas
Receta 7.2, "Renombrar y romper ayudantes y utilidades"
Receta 17.8, "Evitar la envidia de funciones"
6.6 Sustitución de iteraciones explícitas
Problema
Puede que aprendieras a utilizar bucles cuando te iniciabas en el
código. Pero los enumeradores e iteradores son la siguiente generación y
necesitas un mayor nivel de abstracción.
Solución
No utilices índices al iterar. Prefiere colecciones de nivel superior.
Debate
Los índices suelen romper la encapsulación y son menos declarativos. Si
tu lenguaje lo admite, debes favorecer foreach() o iteradores de alto
orden, y puedes utilizar yield(), cachés, proxies, lazy loading y mucho
más cuando ocultas los detalles de tu implementación.
Aquí tienes un ejemplo con el índice i haciendo una iteración estructural:
for (let i = 0; i < [Link]; i++) {
[Link](colors[i]);
}
Lo siguiente es más declarativo y de alto nivel:
[Link]((color) => {
[Link](color);
});
// You use closures and arrow functions
Hay algunas excepciones. Si el dominio del problema necesita que los
elementos se biyecten (como se define en el Capítulo 2) a números
naturales como los índices, la primera solución es adecuada. Recuerda
encontrar siempre análogos del mundo real. A muchos desarrolladores no
les suena este tipo de olor porque piensan que se trata de una sutileza,
pero construir código limpio consiste en estas pocas cosas declarativas
que pueden marcar la diferencia.
Recetas relacionadas
Receta 7.1, "Expandir abreviaturas"
6.7 Documentar las decisiones de
diseño
Problema
Tienes que tomar decisiones no triviales sobre el código y
necesitas documentar las razones.
Solución
Utiliza nombres declarativos y que revelen la intención.
Debate
Debes ser declarativo en tus decisiones de diseño o implementación, por
ejemplo extrayendo la decisión y dándole un nombre claro que revele tu
intención. No debes utilizar comentarios de código, ya que los
comentarios son "código muerto" y pueden quedar obsoletos fácilmente, y
no compilan en absoluto. Simplemente sé explícito sobre la decisión o
convierte el comentario en un método. A veces puedes encontrar reglas
arbitrarias que no son tan fácilmente comprobables. Por ejemplo, si no
puedes escribir una prueba que falle, necesitas una función con un
nombre excelente y declarativo en lugar de un comentario para advertir
de futuros cambios.
He aquí un ejemplo de decisión de diseño no explícita:
// You need to run this process with more memory
set_memory("512k");
run_process();
Lo que sigue es explícito y claro, y da pistas sobre las razones del aumento
de memoria:
increase_memory_to_avoid_false_positives();
run_process();
El código es prosa. Y las decisiones de diseño deben ser narrativas.
Recetas relacionadas
Receta 8.5, "Convertir comentarios en nombres de funciones"
Receta 8.6, "Eliminar comentarios dentro de métodos"
6.8 Sustituir números mágicos por
constantes
Problema
Tienes un método que hace cálculos con muchos números sin describir su
semántica.
Solución
Evita los números mágicos sin explicación. No conoces su origen y debes
tener mucho miedo de cambiarlos y romper el código.
Debate
Los números mágicos son una fuente de acoplamiento. Son difíciles de
probar y de leer. Deberías renombrar cada constante con un nombre
semántico (significativo y que revele la intención) y sustituirlas por
parámetros, para que puedas burlarte de ellas (véase la Receta 20.4,
"Sustituir burlas por objetos reales") desde el exterior. Una definición de
constante suele ser un objeto distinto del usuario de la constante y, por
suerte, muchos linters pueden detectar literales numéricos en atributos y
métodos.
He aquí una constante bien conocida:
function energy($mass) {
return $mass * (299792 ** 2);
}
Si reescribes el ejemplo puedes obtener:
function energy($mass) {
return $mass * (LIGHT_SPEED_KILOMETERS_OVER_SECONDS ** 2);
}
Recetas relacionadas
Receta 5.2, "Declarar variables para que sean variables"
Receta 5.6, "Congelar constantes mutables"
Receta 10.4, "Eliminar la astucia del código"
Receta 11.4, "Eliminar paréntesis excesivos"
Receta 17.1, "Hacer explícitos los supuestos ocultos"
Receta 17.3, "Romper objetos de Dios"
6.9 Separar "Qué" y "Cómo"
Problema
Tienes código mirando los engranajes internos del reloj en vez de mirar
las agujas.
Solución
No te líes con los detalles de implementación. Sé declarativo, no
imperativo.
Debate
Elegir los nombres es importante para evitar el acoplamiento
accidental. Separar las preocupaciones puede ser una tarea difícil en la
industria del software, pero el software funcional tiene la capacidad de
resistir el paso del tiempo. Por el contrario, el software de
implementación conlleva acoplamiento y es más difícil de cambiar.
A veces, la gente documenta los cambios mediante comentarios, pero ésta
no es una buena solución, ya que los comentarios rara vez se mantienen
(véase la Receta 8.5, "Convertir comentarios en nombres de funciones"). Si
favoreces el diseño para el cambio y las intenciones, tu código sobrevivirá
más tiempo y funcionará mejor.
En este ejemplo de código, la acción de mover se acopla a la tarea
pendiente en stepWork:
class Workflow {
moveToNextTransition() {
// You couple the business rule with the accidental
implementation
if ([Link]()) {
throw new Error('Preconditions are not met yet..');
} else {
[Link]();
}
}
}
Aquí tienes una solución mejor utilizando esta receta:
class Workflow {
moveToNextTransition() {
if ([Link]()) {
[Link]();
} else {
throw new Error('Preconditions are not met yet..');
}
}
canMoveOn() {
// You hide accidental implementation 'the how'
// under the 'what'
return ![Link]();
}
}
Tienes que elegir buenos nombres y añadir capas de indirección cuando
sea necesario para evitar la optimización prematura(véase el Capítulo 16,
"Optimización prematura"). El argumento de que estás malgastando
recursos computacionales y necesitas conocer las percepciones no es
importante. Además, cualquier máquina virtual moderna puede
almacenar en caché o inline estas llamadas de invocación adicionales.
Recetas relacionadas
Receta 8.5, "Convertir comentarios en nombres de funciones"
Receta 19.6, "Renombrar clases aisladas"
6.10 Documentar expresiones regulares
Problema
Tienes expresiones regulares mágicas que son difíciles de entender .
Solución
Divide tus complejas expresiones regulares en ejemplos más breves y
declarativos.
Debate
Las expresiones regulares perjudican la legibilidad e impiden el
mantenimiento y la comprobabilidad; sólo debes utilizarlas para validar
cadenas. Si necesitas manipular objetos, no los conviertas en cadenas:
crea objetos pequeños utilizando la Receta 4.1, "Crear objetos pequeños".
Aquí tienes un ejemplo de expresión regular no declarativa:
val regex = Regex("^\\+(?:[0-9][- -]?){6,14}[0-9a-zA-Z]$")
Esto es más declarativo y más fácil de entender y depurar:
val prefix = "\\+"
val digit = "[0-9]"
val space = "[- -]"
val phoneRegex = Regex("^$prefix(?:$digit$space?){6,14}$digit$")
Las expresiones regulares son una herramienta válida. No hay muchas
formas automatizadas de comprobar posibles abusadores. Una lista de
permitidos puede ser de ayuda. También son una gran herramienta para
la validación de cadenas. Debes utilizarlas de forma declarativa y sólo
para cadenas. Los buenos nombres son muy importantes para entender el
significado de los patrones. Si necesitas manipular objetos o jerarquías,
debes hacerlo con objetos, a menos que tengas una prueba concluyente
de una mejora impresionante del rendimiento.
Recetas relacionadas
Receta 4.7, "Reificar validaciones de cadenas"
Receta 10.4, "Eliminar la astucia del código"
Receta 16.2, "Eliminar la optimización prematura"
Receta 25.4, "Sustituir expresiones regulares malignas"
6.11 Reescribir las condiciones de Yoda
Problema
Estás comprobando los valores esperados en la parte izquierda de tu
expresión.
Solución
Escribe tus condiciones con el valor de la variable a la izquierda y el valor
a comprobar a la derecha.
Debate
La mayoría de los programadores escriben primero la variable o
condición y después el valor de la prueba. De hecho, éste es el orden
correcto para las aserciones. En algunos lenguajes, esta preferencia de
estilo se utiliza para evitar la asignación accidental en lugar de la
comparación de igualdad, que puede provocar un error lógico en el
código.
He aquí un ejemplo de una condición Yoda:
if (42 == answerToLifeMeaning) {
// prevents the accidental assignation typo
// since '42 = answerToLifeMeaning' is invalid
// but 'answerToLifeMeaning = 42' is valid
}
Este es el aspecto que debería tener después de reescribirlo:
if (answerToLifeMeaning == 42) {
// might be mistaken with answerToLifeMeaning = 42
}
Comprueba siempre los valores constantes en el lado izquierdo de la
comparación.
Recetas relacionadas
Receta 7.15, "Cambiar el nombre de los argumentos según su función"
6.12 Eliminar métodos comediantes
Problema
Tienes código o ejemplos que pueden ofender a la gente.
Solución
No seas informal ni ofensivo. Sé amable con tu código y con tus lectores.
Debate
Tienes que escribir código de forma profesional utilizando nombres
significativos. Tu profesión tiene un lado creativo. A veces puedes
aburrirte e intentar ser gracioso, perjudicando la legibilidad de tu código
y tu reputación. Aquí tienes algo de código no profesional:
function erradicateAndMurderAllCustomers();
// unprofessional and offensive
Una versión más profesional del método:
function deleteAllCustomers();
// more declarative and professional
Puedes tener una lista de palabras prohibidas y soeces y comprobarlas
automáticamente o en las revisiones del código. Las convenciones de
nomenclatura deben ser genéricas y no deben incluir jerga cultural.
Debes escribir el código de producción de forma que garantices que los
futuros desarrolladores de software (incluso tu yo futuro) lo entenderán
fácilmente.
Recetas relacionadas
Receta 7.7, "Renombrar nombres abstractos"
6.13 Evitar el infierno de las
devoluciones de llamada
Problema
Tienes código asíncrono que utiliza retrollamadas excesivamente
anidadas y difíciles de leer y mantener.
Solución
No proceses las llamadas utilizando llamadas de retorno. Escribe una
secuencia.
Debate
Tienes un infierno de retrollamadas cuando tu código tiene anidadas
múltiples retrollamadas, dando lugar a una estructura de código compleja
y difícil de leer. Esto se ve a menudo en JavaScript cuando se utiliza
programación asíncrona, donde una función de devolución de llamada se
pasa como argumento a otra función. El anidamiento profundo genera un
código también llamado Pirámide de la Perdición.
Cuando llamas a la función interna, ésta puede devolver una función que
toma una llamada de retorno, lo que da lugar a una cadena de llamadas
de retorno anidadas que rápidamente puede resultar difícil de seguir y
razonar.
Este es un breve ejemplo del infierno de las devoluciones de llamada:
asyncFunc1(function (error, result1) {
if (error) {
[Link](error);
} else {
asyncFunc2(function (error, result2) {
if (error) {
[Link](error);
} else {
asyncFunc3(function (error, result3) {
if (error) {
[Link](error);
} else {
// Nested callback continues...
}
});
}
});
}
});
Puedes reescribirlo así:
function asyncFunc1() {
return new Promise((resolve, reject) => {
// Async operation
// ...
// If successful
resolve(result1);
// If error
reject(error);
});
}
function asyncFunc2() {
return new Promise((resolve, reject) => {
// Async operation
// ...
// If successful
resolve(result2);
// If error
reject(error);
});
}
async function performAsyncOperations() {
try {
const result1 = await asyncFunc1();
const result2 = await asyncFunc2();
const result3 = await asyncFunc3();
// Continue with further operations
} catch (error) {
[Link](error);
}
}
performAsyncOperations();
Puedes utilizar promises y async/await para resolver este problema,
haciendo el código más legible y fácil de depurar.
Recetas relacionadas
Receta 10.4, "Eliminar la astucia del código"
Receta 14.10, "Reescribir código de flechas anidado"
6.14 Generar buenos mensajes de error
Problema
Necesitas crear buenas descripciones de errores, tanto para los
desarrolladores que utilizan tu código (y para ti mismo) como para los
usuarios finales.
Solución
Utiliza descripciones significativas y sugiere acciones correctivas. Mostrar
esta amabilidad a tus usuarios te llevará muy lejos.
Debate
Los programadores rara vez son expertos en UX. No obstante, debes
utilizar mensajes de error declarativos pensando en los usuarios finales y
mostrar esos mensajes con acciones de salida claras. Debes seguir el
principio de la menor sorpresa (ver Receta 5.6, "Congelar constantes
mutables"), aplicado a tus usuarios.
Aquí tienes una mala descripción del error:
alert("Cancel the appointment?", "Yes", "No");
// No consequences and actions
// The options are not clear
Puedes cambiar esto por un error más declarativo:
alert("Cancel the appointment? \n" +
"You will lose all the history",
"Cancel Appointment",
"Keep Editing");
// The consequences are clear
// The choice options have context
No enmascares situaciones de error utilizando valores de dominio
válidos, y distingue claramente entre un cero y un error. Revisa el
siguiente código que oculta un error de red y muestra incorrectamente un
saldo de 0, produciendo pánico en el usuario final:
def get_balance(address):
url = "[Link] + address
response = [Link](url)
if response.status_code == 200:
return [Link]
else:
return 0
Esta versión es más clara y explícita:
def get_balance(address):
url = "[Link] + address
response = [Link](url)
if response.status_code == 200:
return [Link]
else:
raise BlockchainNotReachableError("Error reaching
blockchain")
DESCRIPCIONES DE EXCEPCIÓN DECLARATIVAS
Las descripciones de las excepciones deben mencionar la regla de negocio y no
el error. Una buena descripción: "El número debe estar entre 1 y 100". Mala
descripción: "Número fuera de los límites". ¿Cuáles son los límites?
Tienes que leer todos los mensajes de excepción en las revisiones de
código y pensar en los usuarios finales al plantear excepciones o mostrar
mensajes.
Recetas relacionadas
Receta 15.1, "Crear objetos nulos"
Receta 17.13, "Eliminar el código de negocio de la interfaz de usuario"
Receta 22.3, "Reescribir excepciones para casos esperados"
Receta 22.5, "Sustituir códigos de retorno por excepciones"
6.15 Evitar las correcciones mágicas
Problema
Tienes algunas frases que son válidas y mágicas en algunos lenguajes,
pero que deben ser más explícitas y respetar el principio "fail fast".
Solución
Elimina las correcciones mágicas de tu código.
Debate
Algunos lenguajes esconden los problemas bajo la alfombra y hacen
correcciones mágicas y fundidos oscuros, violando el principio de fallo
rápido. Debes ser explícito y eliminar toda ambigüedad. Cambia este tipo
de sentencias mágicas:
new Date(31, 02, 2020);
1 + 'Hello';
!3;
// This is valid in most languages
Aquí tienes una solución explícita:
new Date(31, 02, 2020);
// Throw an exception
1 + 'Hello';
// Type Mismatch
!3;
// Negating is a boolean operation
En la Figura 6-1 puedes ver el resultado inesperado de sumar un número
a una cadena, que no es válido en el mundo real y debería lanzar una
excepción.
Figura 6-1. Ejecutar el método "+" da resultados diferentes en el modelo y en el mundo real
Muchos de estos problemas son fomentados por los propios lenguajes.
Debes ser muy declarativo y explícito; no abuses de las soluciones
accidentales de los lenguajes, sobre todo si parecen mágicas (en lugar de
racionales). Muchos programadores pretenden ser inteligentes
explotando las características del lenguaje; esto es código
innecesariamente complejo que intenta ser inteligente, contrario al código
limpio.
Recetas relacionadas
Receta 10.4, "Eliminar la astucia del código"
Receta 24.2, "Tratar con valores de verdad"
Capítulo 7. Cómo nombrar
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y
comentarios: translation-feedback@[Link]
Sólo hay dos cosas difíciles en Informática: invalidar la caché y nombrar las
cosas.
Phil Karlton
7.0 Introducción
La nomenclatura es un aspecto crítico del desarrollo de software. Incide
directamente en la legibilidad, comprensibilidad y mantenibilidad del
código. Debes elegir buenos nombres para los objetos, clases, variables,
funciones, etc. Los buenos nombres ayudan a reducir la confusión y los
errores y facilitan a otros desarrolladores el uso, la modificación y la
depuración del código. Los nombres son irrelevantes para los
compiladores e intérpretes. Pero escribir código es una actividad centrada
en el ser humano. Una mala elección de nombres puede provocar
confusión, malentendidos y defectos. Si un nombre es demasiado genérico
o poco claro, puede no reflejar con exactitud la finalidad o el
comportamiento del elemento de código que representa y dificultar que
otros desarrolladores comprendan cómo utilizarlo o modificarlo, lo que
provoca errores y pérdida de tiempo.
7.1 Ampliación de abreviaturas
Problema
Tienes nombres abreviados ambiguos.
Solución
Utiliza nombres fuertes, suficientemente largos, inequívocos y
descriptivos.
Debate
Nombrar mal es un problema al que se enfrentan los programadores en la
mayoría de los proyectos de software, y las abreviaturas son contextuales
y ambiguas. Antes, los programadores utilizaban nombres cortos porque
la memoria era escasa, pero ahora rara vez te enfrentas a este problema.
Debes evitar las abreviaturas en todos los contextos: variables, funciones,
módulos, paquetes, espacios de nombres, clases, etc.
Revisa el siguiente ejemplo con las convenciones de nomenclatura
estándar de Golang:
package main
import "fmt"
type YVC struct {
id int
}
func main() {
[Link]("Hello, World")
}
Este tipo de optimización prematura (véase el Capítulo 16, "Optimización
prematura") del texto perjudica la legibilidad y la mantenibilidad. En
algunos lenguajes, esta mala práctica está arraigada y no puedes
cambiarla. Si tienes libertad para hacerlo, deberías cambiar el código
anterior por este ejemplo mejorado:
package main
import "formatter"
type YoutTubeVideoContent struct {
imdbMovieIdentifier int
}
function main() {
[Link]("Hello, World")
}
En la Figura 7-1 puedes ver que fmt corresponde a varios conceptos
diferentes del mundo real, como muchas abreviaturas.
Figura 7-1. Los nombres están abreviados y son ambiguos en el modelo, ya que corresponden a
más de un concepto posible
La informática nació de las matemáticas. En matemáticas, la asignación
de variables de una sola letra(i, j, x, y) es una buena práctica. El concepto
de referencia surgió de la variable y mucha gente se ha preguntado por
qué los matemáticos pueden trabajar con variables tan cortas, mientras
que los informáticos no. Para los matemáticos, una vez introducidas en
una fórmula, las variables pierden toda semántica y se vuelven
indistinguibles. Tienen muchas convenciones para nombrarlas y piensan
muy cuidadosamente en sus nombres. La diferencia es que los
matemáticos siempre tienen un contexto local plena y formalmente
definido, en el que basta una letra para distinguir las variables.
No es el caso de la programación, donde tu cerebro puede gastar mucha
energía en averiguar el significado de una abreviatura e incluso entonces
a veces puedes equivocarte. Como recordatorio, escribe software para
humanos, no para compiladores. Un nombre ambiguo puede tener más de
un significado. Por ejemplo, /usr significa recursos universales del
sistema, no usuario, y /dev significa dispositivo, no desarrollo.
Recetas relacionadas
Receta 7.6, "Renombrar nombres largos"
Receta 10.4, "Eliminar la astucia del código"
7.2 Renombrar y romper Ayudantes y
Utilidades
Problema
Has una clase con el nombre Helper y tiene un comportamiento no
cohesivo y poco claro.
Solución
Cambia el nombre de la clase por uno más preciso y desglosa las
responsabilidades.
Debate
Puedes encontrar ayudantes en muchos frameworks y ejemplos de
código. Se trata de otro nombre ambiguo y vacío que suele violar el
principio de la menor sorpresa (ver Receta 5.6, "Congelar constantes
mutables") y la biyección (como se define en el Capítulo 2) con el mundo
real. Para solucionar este problema, tienes que encontrar un nombre
adecuado. Si el ayudante es una biblioteca, divide todos los servicios en
métodos diferentes.
Aquí tienes un ejemplo de clase ayudante:
export default class UserHelpers {
static getFullName(user) {
return `${[Link]} ${[Link]}`;
}
static getCategory(userPoints) {
return userPoints > 70 ? 'A' : 'B';
}
}
// Notice static methods
import UserHelpers from './UserHelpers';
const alice = {
firstName: 'Alice',
lastName: 'Gray',
points: 78,
};
const fullName = [Link](alice);
const category = [Link](alice);
Puedes utilizar mejores nombres y dividir las responsabilidades:
class UserScore {
// This is an anemic class and should have a better protocol
constructor(name, lastname, points) {
this._name = name;
this._lastname = lastname;
this._points = points;
}
name() {
return this._name;
}
lastname() {
return this._lastname;
}
points() {
return this._points;
}
}
class FullNameFormatter {
constructor(userscore) {
this._userscore = userscore;
}
fullname() {
return `${this._userscore.name()} ${this._userscore.lastname()}`;
}
}
class CategoryCalculator{
constructor(userscore1) {
this._userscore = userscore1;
}
display() {
return this._userscore.points() > 70 ? 'A' : 'B';
}
}
let alice = new UserScore('Alice', 'Gray', 78);
const fullName = new FullNameFormatter(alice).fullname();
const category = new CategoryCalculator(alice).display();
Otra opción es hacer que el ayudante anterior sea sin estado para
reutilizarlo, como en este ejemplo:
class UserScore {
// This is an anemic class and should have a better protocol
constructor(name, lastname, points) {
this._name = name;
this._lastname = lastname;
this._points = points;
}
name() {
return this._name;
}
lastname() {
return this._lastname;
}
points() {
return this._points;
}
}
class FullNameFormatter {
fullname(userscore) {
return `${[Link]()} ${[Link]()}`;
}
}
class CategoryCalculator {
display(userscore) {
return [Link]() > 70 ? 'A' : 'B';
}
}
let alice = new UserScore('Alice', 'Gray', 78);
const fullName = new FullNameFormatter().fullname(alice);
const category = new CategoryCalculator().display(alice);
Puedes ver en la Figura 7-2 un NumberHelper que tiene más
responsabilidades y comportamientos que su homólogo del mundo real.
Figura 7-2. No puedes asignar el NumberHelper a una única entidad del mundo real
Recetas relacionadas
Receta 6.5, "Cambiar las responsabilidades equivocadas"
Receta 7.7, "Renombrar nombres abstractos"
Receta 11.5, "Eliminar el exceso de métodos"
Receta 18.2, "Reificar funciones estáticas"
Receta 23.2, "Reificar funciones anónimas"
7.3 Cambiar el nombre de MisObjetos
Problema
Tienes variables con el prefijo "mi".
Solución
Renombra las variables con "mi" como prefijo.
Debate
Los objetos prefijados con "mi" carecen de contexto y violan la
biyección. Deberías cambiar el nombre por otro que sugiera una
función. Varios tutoriales antiguos utilizan la palabra "mi" como nombre
vago. Esto es vago y lleva a errores de contexto como este ejemplo:
MainWindow myWindow = [Link] as MainWindow;
Aquí tienes una versión mejorada con el papel de un escaparate:
MainWindow salesWindow = [Link] as
MainWindow;
/*
Since the window is instantiated, you are currently working
with a specialized window playing a special role
*/
7.4 Cambiar el nombre de las variables
de resultado
Problema
Nombras el resultado de una función, llamada a un método, o cálculo con
el ambiguo nombre de "resultado".
Solución
Utiliza siempre buenos nombres que sugieran roles. "Resultado" es
siempre una muy mala elección.
Debate
Encuentra la semántica del resultado. Si no sabes cómo nombrarlo,
simplemente nombra la variable con el mismo nombre que la última
llamada a la función. Aquí puedes ver una variable resultado asignada
tras una llamada a un método:
var result;
result = lastBlockchainBlock();
// Many function calls
addBlockAfter(result);
Aquí tienes un enfoque mejor con un nombre de rol:
var lastBlockchainBlock;
lastBlockchainBlock = findLastBlockchainBlock();
// Many function calls
// you should refactor them to minimize space
// between variable definition and usage
addBlockAfter(lastBlockchainBlock);
"Resultado" es un ejemplo de nombre genérico y sin sentido, y una
refactorización de renombrado es barata y segura. Si te encuentras con
este código sigue la regla del Boy Scout:
REGLA DE LOS BOY SCOUTS
La regla del Boy Scout del tío Bob sugiere dejar el código de mejor de lo que lo
encontraste, igual que dejas el camping más limpio de lo que lo encontraste
como Boy Scout. La regla anima a los desarrolladores a realizar pequeñas
mejoras incrementales en el código base cada vez que lo tocan, en lugar de crear
un lío de deuda técnica (véase el Capítulo 21, "Deuda técnica") que será difícil de
limpiar más adelante; también favorece cambiar las cosas que no están del todo
bien. Esto contradice el principio "Si no está roto, no lo arregles".
theResult es un problema similar, como puedes ver en otro ejemplo:
var result;
result = getSomeResult();
var theResult;
theResult = getSomeResult();
Aplicando la misma receta obtienes
var averageSalary;
averageSalary = calculateAverageSalary();
var averageSalaryWithRaises;
averageSalaryWithRaises = calculateAverageSalary();
PRINCIPIO "SI NO ESTÁ ROTO, NO LO ARREGLES".
El principio "Si no está roto, no lo arregles" es una expresión común de en el
desarrollo de software que afirma que si un sistema de software funciona bien,
no hay necesidad de hacerle ningún cambio o mejora. El principio se remonta a
los tiempos en que el software no tenía pruebas automatizadas, por lo que hacer
cualquier cambio probablemente rompería la funcionalidad existente. Los
usuarios del mundo real suelen tolerar los defectos en las nuevas funciones, pero
se enfadan mucho cuando algo que antes funcionaba deja de hacerlo como se
esperaba.
Recetas relacionadas
Receta 7.7, "Renombrar nombres abstractos"
Ver también
Código limpio: A Handbook of Agile Software Craftsmanship por Robert
C. Martin
7.5 Renombrar variables con nombre
de tipo
Problema
Tienes nombres que incluyen tipos, pero los nombres siempre deben
indicar función.
Solución
Elimina el tipo ya que es accidental y no está presente en la bijección.
Debate
Siempre debes diseñar para el cambio, ocultando los detalles de la
implementación accidental. Para ello, cambia el nombre de tus variables
según su función. Mira el siguiente ejemplo, en el que regex es la nueva
instancia que creas:
public bool CheckIfPasswordIsValid(string textToCheck)
{
Regex regex = new Regex(@"[a-z]{2,7}[1-9]{3,4}")
var bool = [Link](textToCheck);
return bool;
}
Este es el aspecto después de dar un nombre significativo a la
variable regex:
public bool CheckIfStringHas3To7LowercaseCharsFollowedBy3or4Numbers(
string password)
{
Regex stringHas3To7LowercaseCharsFollowedBy3or4Numbers =
new Regex(@"[a-z]{2,7}[1-9]{3,4}")
var hasMatch =
[Link](password)
;
return hasMatch;
}
Los nombres deben ser lo suficientemente largos, pero no más (revisa la
Receta 7.6, "Renombrar nombres largos", más adelante). También puedes
garantizar esta regla semántica indicando a tus linters que te adviertan
contra el uso de nombres relacionados con clases, tipos o palabras
reservadas existentes, ya que son demasiado implementativos. El primer
nombre que se te ocurra puede estar relacionado con un punto de vista
accidental. Lleva tiempo construir una teoría sobre los modelos que estás
construyendo utilizando el MAPPER, tal como se define en el Capítulo 2.
Una vez que lo consigas, deberás cambiar el nombre de tus variables.
Recetas relacionadas
Receta 7.6, "Renombrar nombres largos"
Receta 7.7, "Renombrar nombres abstractos"
Receta 7.9, "Eliminar los nombres de clase de los atributos"
7.6 Renombrar nombres largos
Problema
Tienes nombres muy largos y redundantes.
Solución
Los nombres deben ser largos y descriptivos, pero no demasiado. Acorta
tus nombres pero no utilices abreviaturas personalizadas.
Debate
Los nombres largos pueden afectar negativamente a la legibilidad y
aumentar la carga cognitiva. Una regla general es utilizar nombres
relacionados con el MAPPER. Si las abreviaturas se dan en el mundo real
(por ejemplo, URL, HTTL, SSN) están perfectamente bien, ya que no son
ambiguas cuando trabajas en un dominio específico.
Mira el siguiente ejemplo de nombre largo y redundante:
[Link]
Aquí tienes un nombre más corto y conciso:
[Link]
Puedes entrenar a tus linters para que te avisen de los nombres
demasiado largos. Recuerda que no hay reglas rígidas sobre la longitud de
los nombres, sólo heurística, y la heurística es contextual.
CARGA COGNITIVA
La carga cognitiva es la cantidad de esfuerzo mental y recursos necesarios para
procesar la información y completar una tarea. Es la carga que soporta la
memoria de trabajo de una persona cuando intenta procesar, comprender y
recordar información a la vez.
Recetas relacionadas
Receta 7.1, "Expandir abreviaturas"
Ver también
"Lo largo y lo corto de nombrar" por Nutria Ágil
7.7 Renombrar nombres abstractos
Problema
Tus nombres son demasiado abstractos.
Solución
Sustituye los nombres abstractos por otros concretos utilizando
el MAPPER del mundo real.
Debate
Los nombres deben tener significados del mundo real. Al dar nombre a
las entidades, tienes que asignar nombres abstractos a conceptos del
mundo real. Estas generalizaciones aparecen más adelante en el proceso,
después de que hayas modelado muchos conceptos concretos.
Normalmente, estos nombres de dominio existen, pero son más difíciles
de determinar. Por el contrario, utilizar nombres pseudoabstractos
como abstracto, base, genérico, ayudante, etc. es una mala práctica.
He aquí algunos ejemplos abstractos:
final class MeetingsCollection {}
final class AccountsComposite {}
final class NotesArray {}
final class LogCollector {}
abstract class AbstractTransportation {}
Aquí tienes nombres mejores y más concretos para cada uno de ellos, que
corresponden a conceptos del mundo real:
final class Schedule {}
final class Portfolio {}
final class NoteBook {}
final class Journal {}
final class Vehicle {}
Puedes establecer tus propias políticas y reglas advirtiendo sobre ciertas
palabras como base, abstracto, ayudante, gestor, objeto, etc. Encontrar
nombres es lo último que debes hacer en tus diseños. A menos que tengas
un claro entendimiento del negocio, los buenos nombres surgen al final,
después de haber definido el comportamiento y los límites del protocolo.
Recetas relacionadas
Receta 7.2, "Renombrar y romper ayudantes y utilidades"
Receta 7.14, "Eliminar el prefijo/sufijo "Impl de los nombres de las clases"
Receta 12.5, "Eliminar abusos de patrones de diseño"
7.8 Corregir los errores ortográficos
Problema
Tienes erratas y faltas de ortografía en tus nombres.
Solución
Cuida tus nombres. Utiliza correctores ortográficos automáticos.
Debate
La legibilidad siempre es importante y una mala ortografía dificulta la
búsqueda de términos en el código. Ten en cuenta que el polimorfismo
(véase la Receta 14.14, "Convertir funciones no polimórficas en
polimórficas") se basa en métodos con exactamente el mismo nombre. El
siguiente ejemplo incluye una errata:
comboFeededBySupplyer = [Link]();
Esto es lo que parece si lo corriges:
comboFedBySupplier = [Link]();
Presta mucha atención a tus nombres, porque probablemente serás la
persona que lea tu propio código dentro de unos meses.
Recetas relacionadas
Receta 9.1, "Seguir las normas del Código"
7.9 Eliminar los nombres de clase de
los atributos
Problema
Tienes variables que incluyen un nombre de clase.
Solución
No antepongas a tus atributos el nombre de tu clase.
Debate
La redundancia en los nombres huele mal. Se trata de una receta muy
sencilla, ya que no debes leer los atributos de forma aislada y los nombres
son contextuales. En este fragmento de código, la clase Employee tiene
atributos con el prefijo emp:
public class Employee {
String empName = "John";
int empId = 5;
int empAge = 32;
}
Después de eliminarlos, ya no hay redundancia y el código es
más compacto:
public class Employee {
String name;
int id; // Ids are another smell
int age; // Storing the age is another code smell
}
Cuando se incluye el nombre completo en el prefijo, tus linters pueden
avisarte. Como siempre, asegúrate de nombrar según el comportamiento,
no según el tipo o los datos.
Recetas relacionadas
Receta 7.3, "Cambiar el nombre de MisObjetos"
Receta 7.5, "Renombrar variables con nombre de tipo"
Receta 7.10, "Eliminar la primera letra de clases e interfaces"
7.10 Eliminar la primera letra de
clases e interfaces
Problema
Utiliza una letra para anteponer un prefijo a tus clases indicando
abstracto, interfaz, etc.
Solución
No pongas prefijos ni sufijos a tus clases. Utiliza siempre conceptos
completos del mundo real a continuación del MAPPER.
Debate
Esta práctica es muy común en algunos lenguajes, pero perjudica la
legibilidad y crea carga cognitiva cuando intentas mapear el concepto que
ves en el código. También presenta detalles de implementación
accidentales. Como siempre, debes nombrar tus objetos por lo que hacen,
no por lo que son. Algunos lenguajes tienen convenciones culturales
relacionadas con tipos de datos, clases abstractas o interfaces, y estos
nombres cargan tus modelos con traducciones cognitivas difíciles de
seguir, rompiendo el principio KISS (ver Receta 6.2, "Eliminar líneas
vacías"). En C#, es práctica común poner "I" en el nombre de una interfaz
porque, sin ella, no se puede saber si es una interfaz o una clase.
Aquí tienes un ejemplo de la interfaz del motor, el coche abstracto y el
coche implementado:
public interface IEngine
{
void Start();
}
public class ACar {}
Si te ciñes a los nombres de las bijuciones, aparece así:
public interface Engine
{
void Start();
}
public class Vehicle {}
public class Car {}
Con un tesauro, puedes hacer referencia a términos poco convencionales
que no existen en la realidad.
Recetas relacionadas
Receta 7.9, "Eliminar los nombres de clase de los atributos"
Receta 7.14, "Eliminar el prefijo/sufijo "Impl de los nombres de las clases"
7.11 Renombrar Funciones Básicas /
Hacer
Problema
Tienes muchas variantes de las mismas acciones
como sort, doSort, basicSort, doBasicSort, primitiveSort, superB
asicPrimitiveSort.
Solución
Elimina los envoltorios de funciones porque causan confusión y parecen
hacks.
Debate
Utilizar envoltorios perjudica la legibilidad y crea acoplamiento entre
métodos. Puede hacer que el verdadero punto de entrada sea difícil de
encontrar. ¿A qué método llamarías? Si necesitas envolver un
comportamiento, puedes utilizar buenas envolturas de objetos, como los
decoradores dinámicos.
Aquí tienes una clase Calculator con muchos puntos de entrada:
final class Calculator {
private $cachedResults;
function computeSomething() {
if (isSet($this->cachedResults)) {
return $this->cachedResults;
}
$this->cachedResults = $this->logAndComputeSomething();
}
private function logAndComputeSomething() {
$this->logProcessStart();
$result = $this->basicComputeSomething();
$this->logProcessEnd();
return $result;
}
private function basicComputeSomething() {
// Do real work here
}
}
Este fragmento utiliza objetos en lugar de métodos:
final class Calculator {
function computeSomething() {
// Do real work here since I am the compute method
}
}
// Clean and cohesive class, single responsibility
final class CalculatorDecoratorCache {
private $cachedResults;
private $decorated;
function computeSomething() {
if (isset($this->cachedResults)) {
return $this->cachedResults;
}
$this->cachedResults = $this->decorated->computeSomething();
}
}
final class CalculatorDecoratorLogger {
private $decorated;
function computeSomething() {
$this->logProcessStart();
$result = $this->decorated->computeSomething();
$this->logProcessEnd();
return $result;
}
}
Puedes indicar a tus linters estáticos que busquen métodos envolventes si
siguen convenciones como doXXX(), basicXXX()etc.
PATRÓN DECORADOR
El patrón de diseño del decorador te permite añadir
dinámicamente comportamiento a un objeto individual sin afectar al
comportamiento de otros objetos de la misma clase.
7.12 Convertir clases plurales en
singulares
Problema
Tienes nombres de clase que utilizan términos plurales.
Solución
Renombra tus clases con términos singulares. Las clases representan
conceptos, y los conceptos son singulares.
Debate
Poner nombres a las cosas requiere un esfuerzo adicional, y tienes que
acordar ciertas reglas en todo tu sistema. He aquí un pequeño ejemplo de
nombre de clase en plural:
class Users
Simplemente debes cambiarle el nombre:
class User
Ahora puedes crear un usuario, luego otro, y otro más, y ponerlos todos
juntos en una colección que contenga usuarios.
7.13 Eliminar "Colección" de los
nombres
Problema
Tienes la palabra "colección" en tu nombre.
Solución
No utilices "colección" en tu nombre. Es demasiado abstracto para
conceptos concretos.
Debate
Nombrar es muy importante y tienes que tratar mucho con colecciones.
Las colecciones son increíbles porque no necesitan nulos para modelar la
ausencia. Una colección vacía es polimórfica con una colección completa y
evitarás los nulos y los if. Utilizar "colección" como parte de tu nombre
perjudica la legibilidad y es un abuso de la abstracción. Busca buenos
nombres en el MAPPER.
He aquí un breve ejemplo de una variable
llamada customerCollection:
for (var customer in customerCollection) {
// iterate with current customer
}
for (var currentCustomer in customerCollection) {
// iterate with current customer
}
Y aquí tienes un pequeño cambio de nombre:
for (var customer in customers) {
// iterate with current customer
}
Todos los linters pueden detectar una mala nomenclatura como ésta, pero
también pueden dar lugar a falsos positivos, por lo que debes ser
precavido. Debes cuidar todo tu código limpio, variables, clases y
funciones, ya que los nombres precisos son esenciales para comprender
tu código.
Recetas relacionadas
Receta 12.6, "Sustituir colecciones de empresas"
7.14 Eliminar "Impl de los nombres de
las clases
Problema
Tienes clases con el prefijo/sufijo "Impl".
Solución
Pon a tus clases nombres de conceptos del mundo real.
Debate
Algunos lenguajes incluyen modismos y usos comunes que son contrarios
a la buena nomenclatura de los modelos, pero debes elegir tus nombres
con cuidado. Es agradable ver que una clase implementa interfaces. Es
más agradable entender lo que hace. Aquí tienes una interfaz y una clase
que realiza esa interfaz:
public interface Address extends ChangeAware, Serializable {
String getStreet();
}
// Wrong Name - There is no concept 'AddressImpl' in the real world
public class AddressImpl implements Address {
private String street;
private String houseNumber;
private City city;
// ..
}
En esta solución más sencilla sólo tienes el Address:
// Simple
public class Address {
private String street;
private String houseNumber;
private City city;
// ..
}
// OR
// Both are real-world names
public class Address implements ContactLocation {
private String street;
private String houseNumber;
private City city;
// ..
}
Recuerda elegir los nombres de las clases según la bijección esencial; no
sigas la implementación accidental, y no añadas "I" a tus interfaces ni
"Impl" a tus implementaciones.
Recetas relacionadas
Receta 7.5, "Renombrar variables con nombre de tipo"
Receta 7.7, "Renombrar nombres abstractos"
7.15 Cambiar el nombre de los
argumentos según su función
Problema
Tienes argumentos de método que carecen de nombre declarativo.
Solución
Nombra tus argumentos según el papel y no según la posición accidental.
Debate
Al escribir métodos, a veces no te paras a buscar nombres decentes y rara
vez refactorizas para adoptar nombres que revelen tu intención. Mira los
nombres de los argumentos del ejemplo:
class Calculator:
def subtract(self, first, second):
return first - second
Aplicando la regla de sugerencia de función tienes nombres contextuales
claros:
class Calculator:
def subtract(self, minuend, subtrahend):
return minuend - subtrahend
Aquí tienes un ejemplo utilizando el marco de pruebas unitarias:
$this->assertEquals(one, another);
En su lugar, cambia el nombre de los argumentos siguiendo la receta:
$this->assertEquals(expectedValue, actualValue);
Esto es más importante cuando la definición del argumento está lejos de
su uso. Un nombre de función es más contextual y útil.
Recetas relacionadas
Receta 7.5, "Renombrar variables con nombre de tipo"
7.16 Eliminar nombres de parámetros
redundantes
Problema
Has repetido los nombres de los argumentos de tu método.
Solución
No repitas los nombres de tus parámetros. Los nombres deben ser
contextuales y locales.
Debate
Se trata de un problema de duplicación de código. Tus argumentos deben
tener un nombre contextual y no deben incluir la clase que estás creando.
Esto puede parecer una receta pequeña e inocente, pero es importante, ya
que este tipo de constructores presentan propiedades anémicas. Si
incluyes el nombre de la clase, podrías estar creando los objetos
basándote en propiedades en lugar de en su comportamiento. Al utilizar
nombres, puedes pasar por alto que las palabras son contextuales y deben
leerse como una frase completa. Los parámetros deben ser cortos y tener
nombres contextuales.
Echa un vistazo a este ejemplo redundante:
class Employee
def initialize(
@employee_first_name : String,
@employee_last_name : String,
@employee_birthdate : Time)
end
end
Cuando eliminas la parte repetida del nombre obtienes nombres más
sintéticos:
class Employee
def initialize(
@first_name : String,
@last_name : String,
@birthdate : Time)
end
end
Recetas relacionadas
Receta 7.9, "Eliminar los nombres de clase de los atributos"
Receta 9.5, "Unificar el orden de los parámetros"
7.17 Eliminar el contexto gratuito de
los nombres
Problema
Antepones o sufijas a tus clases un identificador global.
Solución
No pongas prefijos ni sufijos con información irrelevante. Elimina esta
información para honrar al MAPPER y facilitar la búsqueda de códigos.
Debate
El prefijo de clase era una práctica muy extendida que se utilizaba hace
décadas para reclamar la propiedad. Ahora sabes que los nombres
limpios son más importantes. El contexto gratuito se refiere a la inclusión
innecesaria de información o datos adicionales en el código o las
interfaces de usuario que no contribuyen a la funcionalidad o usabilidad
del software.
Aquí tienes un ejemplo con el prefijo gratuito WEBB:
struct WEBBExoplanet {
name: String,
mass: f64,
radius: f64,
distance: f64,
orbital_period: f64,
}
struct WEBBGalaxy {
name: String,
classification: String,
distance: f64,
age: f64,
}
Esto es lo que parece después de eliminar el prefijo gratuito:
struct Exoplanet {
name: String,
mass: f64,
radius: f64,
distance: f64,
orbital_period: f64,
}
struct Galaxy {
name: String,
classification: String,
distance: f64,
age: f64,
}
Aplicar esta receta es fácil si tienes herramientas de renombrado en tu
IDE. Recuerda que los nombres deben ser siempre contextuales.
Recetas relacionadas
Receta 7.9, "Eliminar los nombres de clase de los atributos"
Receta 7.10, "Eliminar la primera letra de clases e interfaces"
Receta 7.14, "Eliminar el prefijo/sufijo "Impl de los nombres de las clases"
7.18 Evitar la denominación de datos
Problema
Utiliza nombres de dominio de entidad para modelar objetos de dominio
de entidad.
Solución
No nombres tus variables "datos".
Debate
Una mala nomenclatura perjudica la legibilidad. Siempre debes utilizar
nombres que sugieran funciones y encontrar esos nombres en la
bijección. Utilizar nombres con "datos" favorece el tratamiento anémico
de los objetos, y debes pensar en nombres específicos del dominio y que
sugieran funciones. Aquí compruebas si existen datos:
if (!dataExists()) {
return '<div>Loading Data...</div>';
}
Y aquí compruebas si has encontrado personas:
if (!peopleFound()) {
return '<div>Loading People...</div>';
}
Puedes comprobar esta subcadena en tu código y avisar a tus
desarrolladores. Los datos están en todas partes si ves el mundo sólo
como datos. Nunca puedes ver los datos que manipulas. Sólo puedes
inferirlos a través del comportamiento. No conoces la temperatura actual.
Observas que el termómetro señala 35 grados. Tus variables deben
reflejar el dominio y la función que cumplen.
Recetas relacionadas
Receta 3.1, "Convertir objetos anémicos en objetos ricos"
Receta 7.5, "Renombrar variables con nombre de tipo"
Capítulo 8. Comentarios
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y
comentarios: translation-feedback@[Link]
El uso adecuado de los comentarios es compensar nuestra incapacidad
para expresarnos en código.
Robert C. Martin, Código limpio: Un manual de artesanía ágil del
software
8.0 Introducción
En los tiempos de la programación en lenguaje ensamblador, la
distancia entre lo que pretendías decir como programador y cómo
funcionaba el ordenador era enorme. Cada pocas líneas (a veces en cada
línea), necesitabas una pequeña historia que te ayudara a entender qué
significaban las siguientes instrucciones. Hoy en día, los comentarios
suelen ser un fallo a la hora de elegir buenos nombres. Sólo los necesitas
para describir decisiones de diseño muy importantes. Son código muerto,
ya que no compilan ni se ejecutan. Los comentarios tienden a desviarse
del código que antes describían. Se convierten en islas flotantes de
irrelevancia y extravío en el código. El código limpio casi no necesita
comentarios. Puedes encontrar algunos criterios sobre cómo utilizarlos en
las siguientes recetas.
LENGUAJE ENSAMBLADOR
El lenguaje ensamblador es un lenguaje de programación de bajo nivel para
escribir programas de software para arquitecturas informáticas específicas. Es
un código imperativo de lenguaje legible por humanos que está diseñado para
traducirse fácilmente a lenguaje máquina, que es el lenguaje que pueden
entender los ordenadores.
8.1 Eliminar código comentado
Problema
Has comentado código.
Solución
No dejes código comentado. Utiliza cualquier sistema de control de
versiones de código fuente y luego elimina de forma segura el código
comentado.
Debate
Antes de la década de 2000, los sistemas de control de versiones eran poco
comunes y las pruebas automatizadas no eran una práctica establecida.
Para depurar y probar pequeños cambios, los programadores solían
comentar algunos trozos de código, pero hoy en día esto es un signo de
dejadez. En su lugar, puedes utilizar varias herramientas para encontrar
versiones anteriores y cambios como git bisect.
Con la Receta 8.5, "Convertir comentarios en nombres de función", puedes
extraer el código y comentar sólo la llamada a la función. Cuando estés
seguro de que no la necesitas, puedes eliminar la función comentada y,
potencialmente, refactorizar el código eliminando los métodos no
utilizados. Como paso final, una vez superadas todas las pruebas, elimina
los comentarios siguiendo las prácticas de código limpio.
En el siguiente ejemplo, hay líneas comentadas:
function arabicToRoman(num) {
var decimal = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4,
1];
var roman = ['M', 'CM', 'D', 'CD', 'C', 'XC',
'L', 'XL', 'X', 'IX', 'V', 'IV', 'I'];
var result = '';
for(var i = 0; i < [Link]; i++) {
// print(i)
while(num >= decimal[i]) {
result += roman[i];
num -= decimal[i];
}
}
// if (result > 0 return ' ' += result)
return result;
}
Cuando pases las pruebas, puedes eliminarlos con seguridad:
function arabicToRoman(arabicNumber) {
var decimal = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4,
1];
var roman = ['M', 'CM', 'D', 'CD', 'C', 'XC',
'L', 'XL', 'X', 'IX', 'V', 'IV', 'I'];
var romanString = '';
for(var i = 0; i < [Link]; i++) {
while(arabicNumber >= decimal[i]) {
romanString += roman[i];
num -= decimal[i];
}
}
return romanString;
}
Detectar cuándo utilizar esta receta es difícil. Algunos linters comerciales
y analizadores de aprendizaje automático pueden detectar o analizar los
comentarios y guiarte para eliminarlos.
GIT BISECTAR
Git es un sistema de control de versiones para el desarrollo de software. Te
ayuda a seguir los cambios en el código, colaborar con otros y volver a versiones
anteriores si es necesario. Git almacena todo el historial de versiones de cada
archivo. También puede gestionar múltiples desarrolladores trabajando en el
mismo código base.
git bisect es un comando que te ayuda a localizar la confirmación que
introdujo un cambio concreto en el código. El proceso comienza especificando
una confirmación "buena" que se sabe que no contiene el defecto y una
confirmación "mala" que se sabe que contiene el cambio. Al iterar puedes
encontrar el commit culpable y localizar rápidamente la causa raíz.
Recetas relacionadas
Receta 8.2, "Eliminar comentarios obsoletos"
Receta 8.3, "Eliminar comentarios lógicos"
Receta 8.5, "Convertir comentarios en nombres de funciones"
Receta 8.6, "Eliminar comentarios dentro de métodos"
8.2 Eliminar comentarios obsoletos
Problema
Tienes comentarios que ya no son precisos.
Solución
Elimina el comentario obsoleto.
Debate
Los comentarios no añaden casi ningún valor a tu código y debes
restringirlos sólo a decisiones de diseño muy importantes. Como la
mayoría de la gente cambia la lógica del código y se olvida de actualizar
los comentarios, es probable que se queden obsoletos. Piénsatelo bien
antes de añadir un comentario. Una vez que está en la base de código,
escapa a tu control y puede empezar a engañarte en cualquier momento.
Como dice Ron Jeffries: "El código nunca miente; los comentarios a veces
sí". Puedes eliminar el comentario o sustituirlo por pruebas(consulta la
Receta 8.7, "Sustituir comentarios por pruebas").
En este ejemplo de código, el autor dejó un comentario mostrando algún
cambio necesario:
void Widget::displayPlugin(Unit* unit)
{
// TODO the Plugin will be modified soon, so I don’t implement this
right now
if (!isVisible) {
// hide all widgets
return;
}
}
Deberías eliminar los comentarios y no dejar
ningún ToDos o FixMes(ver Receta 21.4, "Prevenir y eliminar ToDos y
FixMes"):
void Widget::displayPlugin(Unit* unit)
{
if (!isVisible) {
return;
}
}
ADVERTENCIA
Como excepción a esta receta, no debes eliminar los comentarios relacionados
con decisiones críticas de diseño que no sea posible explicar utilizando las
recetas de este capítulo, por ejemplo, una decisión importante sobre
rendimiento, seguridad, etc., no relacionada con lo que hace realmente tu
código.
Recetas relacionadas
Receta 8.1, "Eliminar código comentado"
Receta 8.3, "Eliminar comentarios lógicos"
Receta 8.5, "Convertir comentarios en nombres de funciones"
Receta 8.7, "Sustituir comentarios por pruebas"
8.3 Eliminar comentarios lógicos
Problema
Tu código tiene comentarios lógicos como un true o false en una
condición if.
Solución
No cambies la semántica del código para omitir código. Elimina las
condiciones muertas.
Debate
Los comentarios lógicos perjudican la legibilidad, no revelan la intención
y demuestran dejadez. Debes confiar en tu sistema de control de código
fuente para hacer cambios temporales. Cambiar el código con un hack
temporal es una pésima práctica de desarrollador, porque
podrías olvidar algunos y dejarlos para siempre.
El siguiente ejemplo añade una condición false para evitar la
función doStuff() para una depuración más rápida:
if ([Link]() > 11 && [Link]()) {
doStuff();
}
doMore();
// Production code
Este es el aspecto después de añadir un false:
// the false acts to temporary skip the if condition
if (false && [Link]() > 11 && [Link]()) {
doStuff();
}
doMore();
if (true || ([Link]() > 11 && [Link]())) {
// Same hack to force the condition
// code after the true is never evaluated
En lugar de esto, deberías cubrir los dos casos con pruebas unitarias
diferentes para una depuración adecuada:
if ([Link]() > 11 && [Link]()) {
doStuff();
}
doMore();
// Production code
// If you need to force or skip the condition
// you can do it with a covering test forcing a
// real-world scenario and not the code
testLargeCartItems()
testUserIsRetail()
La separación de intereses es extremadamente importante, y la lógica
empresarial y los hacks deben tratarse siempre por separado.
SEPARACIÓN DE PREOCUPACIONES
El concepto de separación de intereses pretende dividir un sistema de software
en partes distintas y autónomas, cada una de las cuales aborda un aspecto o
interés específico del sistema global. El objetivo es crear un diseño modular y
fácil de mantener que fomente la reutilización del código, la escalabilidad y la
facilidad de comprensión, dividiéndolo en partes más pequeñas y manejables, lo
que permite a los desarrolladores centrarse en una preocupación cada vez.
Recetas relacionadas
Receta 8.1, "Eliminar código comentado"
8.4 Eliminar comentarios del Getter
Problema
Tienes captadores con comentarios triviales.
Solución
No utilices getters. No comentes los getters u otras funciones triviales.
Debate
Esta receta trata el doble problema de tener getters y comentarios
triviales. Hace unas décadas, la gente solía comentar todos los métodos.
Incluso los triviales. Aquí tienes un ejemplo de comentario getter sobre la
función getPrice():
contract Property {
int private price;
function getPrice() public view returns(int) {
/* returns the Price */
return price;
}
}
Este es el aspecto después de eliminar los comentarios getter:
contract Property {
int private _price;
function price() public view returns(int) {
return _price;
}
}
Como excepción, si tu función necesita algún comentario y
accidentalmente es un getter, puedes añadir un comentario no trivial
(preferiblemente si está relacionado con una decisión de diseño).
Recetas relacionadas
Receta 3.1, "Convertir objetos anémicos en objetos ricos"
Receta 3.8, "Eliminar Getters"
Receta 8.5, "Convertir comentarios en nombres de funciones"
8.5 Convertir comentarios en nombres
de funciones
Problema
Tienes código con muchos comentarios. Los comentarios van unidos a la
implementación y apenas se mantienen.
Solución
Extrae la lógica a una función con un nombre derivado del comentario.
Debate
Si tienes comentarios que describen lo que debe hacer una función, la
mejor solución es codificar ese comentario en el nombre de la función.
Haz que revele la intención. Revisa la siguiente función con un mal
nombre y un buen comentario descriptivo:
final class ChatBotConnectionHelper {
// ChatBotConnectionHelper is used
// to create connection strings to Bot Platform
// Use this class with getString() function
// to get connection string to platform
function getString() {
//Get connection string from Chatbot
}
}
Después de aplicar la receta tendrás nombres de clases y funciones
reveladores de intenciones que no necesitan comentarios:
final class ChatBotConnectionSequenceGenerator {
function connectionSequence() {
}
}
Como regla general, puedes medir con tus linters para detectar
comentarios y comprobar la proporción de comentarios/líneas de código
con respecto a un umbral predefinido (idealmente cercano a 1).
Recetas relacionadas
Receta 8.6, "Eliminar comentarios dentro de métodos"
8.6 Eliminar comentarios dentro de los
métodos
Problema
Tienes comentarios dentro de un método.
Solución
No añadas comentarios dentro de tus métodos. Extráelos y deja los
comentarios declarativos sólo para decisiones de diseño muy complejas.
Debate
Los comentarios dentro de los métodos dividen las acciones grandes en
otras más pequeñas. Debes aplicar la Receta 10.7, "Extraer un método a un
objeto", y renombrar los métodos extraídos con la descripción del
comentario. Aquí tienes un método largo separado por comentarios:
function recoverFromGrief() {
// Denial stage
absorbTheBadNews();
setNumbAsProtectiveState();
startToRiseEmotions();
feelSorrow();
// Anger stage
maskRealEffects();
directAngerToOtherPeople();
blameOthers();
getIrrational();
// Bargaining stage
feelVulnerable();
regret();
askWhyToMyself();
dreamOfAlternativeWhatIfScenarios();
postponeSadness();
// Depression stage
stayQuiet();
getOverwhelmed();
beConfused();
// Acceptance stage
acceptWhatHappened();
lookToTheFuture();
reconstructAndWalktrough();
}
Después de romperlo, es más legible:
function recoverFromGrief() {
denialStage();
angerStage();
bargainingStage();
depressionStage();
acceptanceStage();
}
function denialStage() {
absorbTheBadNews();
setNumbAsProtectiveState();
startToRiseEmotions();
feelSorrow();
}
function angerStage() {
maskRealEffects();
directAngerToOtherPeople();
blameOthers();
getIrrational();
}
function bargainingStage() {
feelVulnerable();
regret();
askWhyToMyself();
dreamOfAlternativeWhatIfScenarios();
postponeSadness();
}
function depressionStage() {
stayQuiet();
getOverwhelmed();
beConfused();
}
function acceptanceStage() {
acceptWhatHappened();
lookToTheFuture();
reconstructAndWalktrough();
}
Los comentarios sirven para documentar opciones de diseño no evidentes
y no debes colocarlos dentro del cuerpo de una función.
Recetas relacionadas
Receta 6.2, "Eliminar líneas vacías"
Receta 6.7, "Documentar las decisiones de diseño"
Receta 8.5, "Convertir comentarios en nombres de funciones"
Receta 10.7, "Extraer un método a un objeto"
Receta 11.1, "Romper métodos demasiado largos"
8.7 Sustituir comentarios por pruebas
Problema
Tienes comentarios que describen lo que hace una función (y quizá cómo
lo consigue) y, en lugar de descripciones estáticas y obsoletas, quieres
tener una documentación dinámica y bien mantenida.
Solución
Coge tu comentario, compáctalo y dale un nombre a tus funciones.
Pruébalo y elimina los comentarios.
Debate
Los comentarios rara vez se mantienen y son más difíciles de leer que las
pruebas. A veces incluso contienen información irrelevante sobre la
implementación. Puedes tomar el comentario del método que explica lo
que hace la función, renombrar el método con la descripción del
comentario (el qué), crear pruebas para verificar los comentarios y omitir
los detalles de implementación irrelevantes.
He aquí un ejemplo trivial de una función que describe lo que hace y
cómo lo consigue:
def multiply(a, b):
# This function multiplies two numbers and returns the result
# If one of the numbers is zero, the result will be zero
# If the numbers are both positive, the result will be positive
# If the numbers are both negative, the result will be positive
# The multiplication is done by invoking a primitive
return a * b
# This code has a comment that explains what the function does.
# Instead of relying on this comment to understand the behavior of the
code,
# you can write some unit tests that verify the behavior of the
function.
Después de eliminar el comentario y crear los casos de prueba:
def multiply(first_multiplier, second_multiplier):
return first_multiplier * second_multiplier
class TestMultiply([Link]):
def test_multiply_both_possitive_outcome_is_possitive(self):
result = multiply(2, 3)
[Link](result, 6)
def test_multiply_both_negative_outcome_is_positive(self):
result = multiply(-2, -4)
[Link](result, 8)
def test_multiply_first_is_zero_outcome_is_zero(self):
result = multiply(0, -4)
[Link](result, 0)
def test_multiply_second_is_zero_outcome_is_zero(self):
result = multiply(3, 0)
[Link](result, 0)
def test_multiply_both_are_zero_outcome_is_zero(self):
result = multiply(0, 0)
[Link](result, 0)
# You define a test function called test_multiply,
# which calls the multiply function with different arguments
# and verifies that the result is correct using the assertEqual
method.
# 1. Take the comment of the method explaining what the function does.
# 2. Rename the method with the comment description (the what).
# 3. Create tests to verify the comments.
# 4. Omit irrelevant implementation details
Puedes reescribir el comentario y compactarlo, pero no siempre de forma
algorítmica. No es una refactorización segura, pero aumenta la
cobertura. Como excepción, no puedes probar métodos privados
(consulta la Receta 20.1, "Probar métodos privados"). En el improbable
caso de que necesites sustituir un comentario sobre un método privado,
debes probarlo indirectamente o extraerlo a otro objeto mediante la
Receta 10.7, "Extraer un método a un objeto". Como siempre, puedes dejar
comentarios que reflejen decisiones de diseño importantes.
Recetas relacionadas
Receta 8.2, "Eliminar comentarios obsoletos"
Receta 8.4, "Eliminar comentarios del Getter"
Receta 10.7, "Extraer un método a un objeto"
Receta 20.1, "Probar métodos privados"
Capítulo 9. Normas
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y
comentarios: translation-feedback@[Link]
Lo bueno de las normas es que tienes muchas para elegir. Además, si no te
gusta ninguna, puedes esperar al modelo del año siguiente.
Andrew S. Tanenbaum, Redes de ordenadores, cuarta edición
9.0 Introducción
En las grandes organizaciones, es importante tener convenciones de
código para garantizar que los distintos equipos y desarrolladores
trabajan con un conjunto común de normas y buenas prácticas. Esto
puede ayudar a garantizar que el código sea coherente y fácil de
entender, lo que puede facilitar el trabajo y el mantenimiento, y también
ayuda a mejorar la calidad general de la base de código. Al aplicar un
conjunto de normas de codificación, las organizaciones pueden
asegurarse de que los desarrolladores siguen las buenas prácticas y
escriben un código que tiene más probabilidades de ser fiable, escalable y
mantenible.
9.1 Seguir las normas del Código
Problema
Trabajas en una gran base de código con muchos otros
desarrolladores. Necesitas leer todo el código con la misma estructura y
convenciones, pero hay muchas normas mezcladas.
Solución
Sigue las mismas normas en toda la organización y hazlas cumplir
(automáticamente si puedes).
Debate
Trabajar en un proyecto en solitario es fácil, a menos que vuelvas a él al
cabo de unos meses. Trabajar con muchos otros desarrolladores requiere
algunos acuerdos. Seguir unos estándares de código comunes favorece la
mantenibilidad y la legibilidad, y ayuda a los revisores de código. La
mayoría de los lenguajes modernos tienen normas de código comunes,
como PSR2 en PHP, y la mayoría de los IDE modernos las aplican
automáticamente.
En este ejemplo, puedes ver normas de código mixtas:
public class MY_Account {
// This class name has a different case and underscores
private Statement privStatement;
// Attributes have visibility prefixes
private Amount currentbalance = amountOf(0);
public SetAccount(Statement statement) {
[Link] = statement;
}
// Setters and getters are not normalized
public GiveAccount(Statement statement)
{ [Link] = statement; }
// Indentation is not uniform
// Curly brace opens after function definition
public void deposit(Amount value, Date date) {
recordTransaction(
value, date
);
// Some variables are named after type and not role
// Parentheses are inconsistent
}
public void extraction(Amount value, Date date) {
recordTransaction([Link](), date);
// The opposite of *deposit* should be withdrawal
}
public void voidPrintStatement(PrintStream printer)
{
[Link](printer);
// Name is redundant
}
private void privRecordTransactionAfterEnteredthabalance(
Amount value, Date date) {
Transaction transaction = new Transaction(value, date);
Amount balanceAfterTransaction =
[Link](balance);
balance = balanceAfterTransaction;
[Link](
transaction, balanceAfterTransaction);
// Naming is not uniform
// Wrapped lines are not consistent
}
}
Esto es lo que parece si sigues (cualquier arbitraria) norma de código
común:
public class Account {
private Statement statement;
private Amount balance = amountOf(0);
public Account(Statement statement) {
[Link] = statement;
}
public void deposit(Amount value, Date date) {
recordTransaction(value, date);
}
public void withdrawal(Amount value, Date date) {
recordTransaction([Link](), date);
}
public void printStatement(PrintStream printer) {
[Link](printer);
}
private void recordTransaction(Amount value, Date date) {
Transaction transaction = new Transaction(value, date);
Amount balanceAfterTransaction =
[Link](balance);
balance = balanceAfterTransaction;
[Link](transaction,
balanceAfterTransaction);
}
}
Los linters y los IDE deben comprobar las normas de codificación antes de
que se apruebe una solicitud de fusión, y tú puedes añadir tus propias
normas para las convenciones de nomenclatura relacionadas con
cualquier elemento de software como objetos, clases, interfaces, módulos,
etc. El código limpio bien escrito siempre sigue las normas en cuanto a
convenciones de nomenclatura, formato y estilo del código. Las normas
son útiles porque hacen que las cosas sean claras y deterministas para
quienes lean tu código, incluido tú mismo.
El formateo automático del código por parte de un analizador sintáctico o
un compilador es la forma en que las máquinas proporcionan
información sobre cómo interpretan tus instrucciones, evita desacuerdos
y sigue el principio "fail fast". El formateo automático del código es
obligatorio en las grandes organizaciones para reforzar la propiedad
colectiva.
PROPIEDAD COLECTIVA
La propiedad colectiva establece que todos los miembros de un equipo de
desarrollo tienen la capacidad de hacer cambios en cualquier parte del código
base, independientemente de quién lo escribió originalmente. Su objetivo es
promover un sentido de responsabilidad compartida, haciendo que el código sea
más manejable y más fácil de mejorar.
Recetas relacionadas
Receta 7.8, "Corregir los errores ortográficos"
Receta 10.4, "Eliminar la astucia del código"
9.2 Normalizar las indentaciones
Problema
Tienes el código mezclando tabuladores y espacios para la indentación.
Solución
No mezcles estilos de indentación. Elige uno y cíñete a él.
Debate
Hay varias opiniones sobre qué estilo es mejor, pero puedes tomar tu
propia decisión. Lo más importante aquí es la coherencia del código.
Puedes hacerla cumplir con pruebas estándar de código. Aquí tienes un
ejemplo con estilos mezclados:
function add(x, y) {
// --->..return x + y;
return x + y;
}
function main() {
// --->var x = 5,
// --->....y = 7;
var x = 5,
y = 7;
}
Esto es lo que parece cuando lo normalizas:
function add(x, y) {
// --->return x + y;
return x + y;
}
Algunos lenguajes como Python consideran la indentación como parte de
la sintaxis. En estos lenguajes, la indentación no es accidental, ya que
cambia la semántica del código. Algunos IDE convierten automáticamente
una convención en la otra.
Recetas relacionadas
Receta 9.1, "Seguir las normas del código"
9.3 Unificar las convenciones de los
casos
Problema
Tienes una base de código mantenida por un montón de personas
diferentes de todo el mundo que utilizan diferentes convenciones de
mayúsculas y minúsculas.
Solución
No mezcles diferentes convenciones de mayúsculas y minúsculas; elige
una de ellas y hazla cumplir.
Debate
Cuando distintas personas crean software juntas, pueden tener
diferencias personales o culturales, ya que algunas prefieren camel case,
otras snake_case, MACRO_CASE y muchas otras. El código debe ser
sencillo y legible. También existen convenciones lingüísticas estándar
para las mayúsculas y minúsculas, como camelCase para Java o
snake_case para Python.
Aquí tienes varias convenciones de casos mezcladas en un archivo JSON:
{
"id": 2,
"userId": 666,
"accountNumber": "12345-12345-12345",
"UPDATED_AT": "2022-01-07T02:23:41.305Z",
"created_at": "2019-01-07T02:23:41.305Z",
"deleted at": "2022-01-07T02:23:41.305Z"
}
Esto es lo que parece si eliges sólo uno de ellos:
{
"id": 2,
"userId": 666,
"accountNumber": "12345-12345-12345",
"updatedAt": "2022-01-07T02:23:41.305Z",
"createdAt": "2019-01-07T02:23:41.305Z",
"deletedAt": "2022-01-07T02:23:41.305Z"
// This doesn't mean THIS standard is the right one
}
Puedes informar a tus linters sobre las amplias normas de nomenclatura
de tu empresa y hacerlas cumplir. Siempre que llegue gente nueva a la
organización, una prueba automatizada debe pedir educadamente que se
cambie el código. Como excepciones válidas, siempre que necesites
interactuar con código fuera de tu alcance, debes utilizar las normas del
cliente, no las tuyas.
Recetas relacionadas
Receta 9.1, "Seguir las normas del código"
9.4 Escribir código en inglés
Problema
Tienes que codificar utilizando tu idioma local (no el inglés) porque los
nombres de las empresas son más difíciles de traducir.
Solución
Cíñete al inglés. Traduce también al inglés los nombres de las empresas.
Debate
Todos los lenguajes de programación están escritos en inglés. Salvo
algunos experimentos fallidos durante los años 90, todos los lenguajes
modernos utilizan el inglés para sus primitivas y sus marcos de trabajo. Si
querías leer o escribir en la Europa medieval, tenías que aprender
latín. Lo mismo ocurre con los lenguajes de programación actuales con el
inglés, y si mezclas nombres en inglés con otros que no lo son, puedes
romper el polimorfismo (véase la Receta 14.14, "Convertir funciones no
polimórficas en polimórficas"), añadir carga cognitiva, cometer errores
sintácticos, romper la biyección (como se define en el Capítulo 2), y mucho
más. Hoy en día, la mayoría de IDEs y linters tienen herramientas de
traducción o tesauros, y puedes buscar traducciones al inglés de palabras
extranjeras.
Este ejemplo mezcla inglés y español:
const elements = new Set();
[Link](1);
[Link](1);
// This is the standard set
// Sets do not store duplicates
echo [Link]() yields 1
// You defined a multiset in Spanish
// because you are extending the domain
var moreElements = new MultiConjunto();
// 'multiconjunto' is the Spanish word for 'multiset'
// 'agregar' is the Spanish word for 'add'
[Link]('hello');
[Link]('hello');
echo [Link]() // yields 2 // Since it is a multiset
// elements and moreElements are NOT polymorphic
// You cannot exchange their accidental implementation
class Person {
constructor() {
[Link] = new Set();
}
visitCity(city) {
[Link](city);
// Breaks if you change the set (expecting 'add()')
// with a MultiConjunto (expecting 'agregar()')
}
}
Esto es lo que parece escrito totalmente en inglés:
const elements = new Set();
[Link](1);
[Link](1);
// This is the standard set
echo [Link]() // yields 1
// You define a multiset in English
var moreElements = new MultiSet();
[Link]('hello');
[Link]('hello');
echo [Link]() // yields 2 // Since it is a multiset
// elements and moreElements are polymorphic
// You can use either one in Person class. Even in runtime
9.5 Unificar el orden de los parámetros
Problema
Tienes incoherencias con los parámetros que utilizas.
Solución
No confundas a tus lectores. Mantén un orden coherente.
Debate
El código se lee como prosa. Tienes que leer todos los métodos en el
mismo orden. También puedes utilizar parámetros con nombre si tu
lenguaje los admite. En el siguiente ejemplo, los dos métodos parecen
similares:
function giveFirstDoseOfVaccine(person, vaccine) { }
function giveSecondDoseOfVaccine(vaccine, person) { }
giveFirstDoseOfVaccine(jane, flu);
giveSecondDoseOfVaccine(jane, flu);
// Unnoticed mistake since you changed the parameters' order
Esto es lo que parece si eres coherente con la ordenación de los
parámetros:
function giveFirstDoseOfVaccine(person, vaccine) { }
function giveSecondDoseOfVaccine(person, vaccine) { }
giveFirstDoseOfVaccine(jane, flu);
giveSecondDoseOfVaccine(jane, flu);
Este es el aspecto si utilizas argumentos con nombre:
function giveFirstDoseOfVaccine(person, vaccine) { }
giveFirstDoseOfVaccine(person=jane, vaccine=flu);
// equivalent to giveFirstDoseOfVaccine( vaccine=flu, person=jane);
giveSecondDoseOfVaccine(person=jane, vaccine=flu);
// equivalent to giveSecondDoseOfVaccine( vaccine=flu, person=jane);
PARÁMETROS CON NOMBRE
Los parámetros con nombre son una característica de muchos lenguajes de
programación que permiten al programador especificar el valor de un
parámetro proporcionando su nombre en lugar de su posición en la lista de
parámetros. También se conocen como argumentos de palabra clave.
Recetas relacionadas
Receta 7.16, "Eliminar nombres de parámetros redundantes"
Receta 11.2, "Reducir el exceso de argumentos"
9.6 Arreglar ventanas rotas
Problema
Estás cambiando alguna parte del código pero encuentras otra cosa fuera
de lugar.
Solución
Sigue la regla del Boy Scout (ver Receta 7.4, "Cambiar el nombre de las
variables de resultado") y deja el código más limpio de lo que lo
encontraste. Si encuentras un desastre, límpialo independientemente de
quién lo haya hecho. Si encuentras un problema, arréglalo.
Debate
Como programador, lees código muchas más veces de las que lo escribes.
Hazte cargo del código que leas que contenga errores y déjalo mejor. Si
encuentras código así mientras haces otros cambios:
int mult(int a,int other)
{ int prod
prod= 0;
for(int i=0;i<other ;i++)
prod+= a ;
return prod;
}
// Formatting, naming, assignment and standards inconsistent
Deberías cambiarlo aplicando muchas recetas:
int multiply(int firstMultiplier, int secondMultiplier) {
int product = 0;
for(int index=0; index<secondMultiplier; index++) {
product += firstMultiplier;
}
return product;
}
// or just multiply them :)
No tengas miedo de hacer cambios, esfuérzate siempre por conseguir una
buena cobertura de pruebas para asegurarte de que no se ve afectada
ninguna funcionalidad empresarial, y recuerda que desarrollar software
es una actividad de equipo, por lo que necesitas encontrar consenso al
hacer este tipo de cambios.
Recetas relacionadas
Receta 9.2, "Normalizar las indentaciones"
Receta 9.3, "Unificar las convenciones de mayúsculas y minúsculas"
Receta 21.4, "Evitar y eliminar ToDos y FixMes"
Capítulo 10. Complejidad
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y
comentarios: translation-feedback@[Link]
La programación orientada a objetos aumenta el valor de estas métricas
gestionando esta complejidad. La herramienta más eficaz para gestionar la
complejidad es la abstracción. Se pueden utilizar muchos tipos de
abstracción, pero la encapsulación es la principal forma de abstracción
mediante la que se gestiona la complejidad en la programación orientada a
objetos.
Rebecca Wirfs-Brock y Brian Wilkinson, "Diseño orientado a objetos: Un
enfoque basado en la responsabilidad"
10.0 Introducción
Según David Farley, si quieres ser un excelente ingeniero de software,
tienes que ser un experto en aprender, y tu único deber es mantener la
complejidad accidental en los niveles más bajos posibles. La complejidad
está presente en todo gran sistema de software y suele ser la principal
fuente de problemas. Una de las principales diferencias entre un joven
desarrollador de software y uno más experimentado es cómo gestionan la
complejidad accidental y la mantienen al mínimo.
10.1 Eliminar código repetido
Problema
Tienes un comportamiento duplicado en tu código. Un comportamiento
duplicado no es lo mismo que un código duplicado, ya que el código no es
texto.
Solución
Tienes que encontrar la abstracción que falta y trasladar allí el
comportamiento repetido.
Debate
El código duplicado perjudica la mantenibilidad y viola el principio de no
repetirse. También aumenta los costes de mantenimiento, y los cambios
pueden llevar mucho tiempo y ser propensos a errores. Si el código tiene
un defecto, puede estar presente en varios lugares. El código duplicado
también impide la reutilización, ya que faltan abstracciones. Debes tener
en cuenta estos inconvenientes antes de utilizar comandos de copiar y
pegar.
Aquí tienes un código duplicado para un sustituto de texto utilizado en
un WordProcessor y un Obfuscator:
class WordProcessor {
function replaceText(string $patternToFind, string
$textToReplace) {
$this->text = '<<<' .
str_replace($patternToFind, $textToReplace, $this-
>text) . '>>>';
}
}
final class Obfuscator {
function obfuscate(string $patternToFind, string $textToReplace)
{
$this->text =
strlower(str_ireplace($patternToFind, $textToReplace,
$this->text));
}
}
Este es el aspecto que tiene cuando encapsulas la lógica del sustituto de
texto en una nueva abstracción:
final class TextReplacer {
function replace(
string $patternToFind,
string $textToReplace,
string $subject,
string $replaceFunctionName,
$postProcessClosure) {
return $postProcessClosure(
$replaceFunctionName($patternToFind, $textToReplace,
$subject));
}
}
// Lots of tests on text replacer so you can gain confidence.
final class WordProcessor {
function replaceText(string $patternToFind, string
$textToReplace) {
$this->text = (new TextReplacer())->replace(
$patternToFind,
$textToReplace,
$this->text,
'str_replace', fn($text) => '<<<' . $text . '>>>');
}
}
final class Obfuscator {
function obfuscate(string $patternToFind, string $textToReplace)
{
$this->text = (new TextReplacer())->replace(
$patternToFind,
$textToReplace,
$this->text,
'str_ireplace', fn($text) => strlower($text));
}
}
Los linters pueden encontrar código repetido, pero no son muy buenos
encontrando patrones similares. Quizá pronto el aprendizaje automático
te ayude a encontrar tales abstracciones automáticamente. Con tus
herramientas de refactorización, tienes que refactorizar ese código,
confiando en tus pruebas como red de seguridad.
PROGRAMACIÓN DE COPIAR Y PEGAR
La programación de copiar y pegar es una técnica en la que copias el código
existente y lo pegas en otro lugar, en lugar de escribir código nuevo. Si utilizas
mucho el copiar y pegar, tu código es menos mantenible.
Recetas relacionadas
Receta 19.3, "Romper la subclasificación para reutilizar el código"
10.2 Eliminar Ajustes/Configs y
conmutadores de funciones
Problema
Tu código se basa en configuraciones globales , ajustes o conmutaciones
de funciones.
Solución
Identifica, rastrea y elimina las funciones y personalizaciones una vez
maduras. Reifica la configuración en objetos pequeños.
Debate
BANDERAS DE CARACTERÍSTICAS
Un indicador de función (también conocido como conmutador o interruptor de
función) te permite activar o desactivar una función o funcionalidad específica
en tiempo de ejecución, sin necesidad de una nueva implementación completa.
Esto te permite lanzar nuevas funciones a un subconjunto de usuarios o
entornos, manteniéndolas ocultas a los demás para realizar pruebas A/B y betas
tempranas o lanzamientos canarios.
Cambiar el comportamiento de un sistema en una placa de control es el
sueño de un cliente. Y la pesadilla de un ingeniero de software. La
configuración conlleva un acoplamiento global, si contaminación, y una
explosión de escenarios de pruebas. Puedes gestionar tu configuración
creando objetos polimórficos e inyectándolos externamente. Debes
configurar tus objetos para que puedan comportarse de distintas
maneras, y debes conseguirlo con objetos de comportamiento explícito.
Un sistema con 300 configuraciones booleanas tiene más combinaciones
de prueba (2300) que el número de átomos del universo (1080).
He aquí un ejemplo en el que una configuración inyectada globalmente
afecta a la forma en que recuperas los objetos:
class VerySpecificAndSmallObjectDealingWithPersistency {
retrieveData() {
if
([Link]().valueAt('RetrievDataDirectly'))
{
// Notice the unnoticed typo in 'RetrievDataDirectly'
[Link]();
}
else {
[Link]();
}
}
}
Puedes utilizar explícitamente el patrón de diseño de estrategia
(ver Receta 14.4, "Sustitución de las sentencias Switch/Case/Elseif") y
probarlo después de romper el acoplamiento global:
class VerySpecificAndSmallObjectDealingWithPersistency {
constructor(retrievalStrategy) {
[Link] = retrievalStrategy;
}
retrieveData() {
[Link]();
}
}
// You get rid of the if condition by using a polymorphic strategy
Se trata de un patrón arquitectónico, por lo que debes controlarlo o
evitarlo directamente con políticas de diseño. Como excepciones notables,
a veces utilizas la alternancia de funciones como mecanismo de
salvaguarda. Esto es aceptable en un sistema heredado, pero estas
conmutaciones deben durar muy poco (unas semanas) en un sistema
CI/CD.
PRUEBAS A/B
Las pruebas A/B comparan dos versiones diferentes de un software lanzado
para determinar cuál es mejor para los usuarios finales.
Recetas relacionadas
Receta 14.16, "Reificar las condiciones comerciales codificadas"
Receta 17.3, "Romper objetos de Dios"
10.3 Cambiar el estado como
propiedades
Problema
Realizas cambios de estado modificando algunos atributos internos de.
Solución
Modela el estado de tu objeto como una inclusión en un conjunto de
forma similar a la metáfora del mundo real en el MAPPER.
Debate
Esta receta es contraintuitiva a menos que pienses en ella bajo el
paraguas de la mutabilidad. Debes modelar los estados como una
inclusión matemática de conjuntos, ya que un estado siempre
es accidental; necesitas extraerlo y alejarlo del objeto. Cada diagrama de
estados del ciclo de vida de tu objeto es una oportunidad para aplicar esta
receta. Los ingenieros de software se enfrentan a un reto muy difícil a la
hora de encontrar buenos modelos que sobrevivan a toda su vida útil, ya
que tales modelos son raros.
Aquí tienes Order con un atributo que modela un posible estado:
public abstract class OrderState {}
public class OrderStatePending extends OrderState {}
// This is a polymorphic hierarchy with different behavior
// An enum is not enough to model state
public class Order {
public Order(LinkedList<int> items) {
LinkedList<int> items = items;
OrderState state = new OrderStatePending();
}
public function changeState(OrderState newState) {
OrderState state = newState;
}
public function confirm() {
[Link](this);
}
}
Éste es el aspecto si eliminas el estado de Order y gestionas la agrupación
de colecciones por estado:
class Order {
public Order(LinkedList<int> items) {
items = items;
}
}
class OrderProcessor {
public static void main(String args[]) {
LinkedList<int> elements = new LinkedList<int>();
[Link](1);
[Link](2);
Order sampleOrder = new Order(elements);
Collection<Order> pendingOrders = new LinkedList<Order>();
Collection<Order> confirmedOrders = new LinkedList<Order>();
[Link](sampleOrder);
[Link](sampleOrder);
[Link](sampleOrder);
}
}
En la Figura 10-1, puedes ver que la Orden 1 pertenece a la colección de
órdenes pendientes, tanto en el mundo real como en el modelo.
Convertirla en un estado rompería la biyección. Las órdenes confirmadas
están vacías en este momento.
Figura 10-1. Los pedidos pertenecen a los mismos conjuntos en el modelo y en el mundo real
Si quieres ser extremista, debes considerar cada definidor como un
cambio de estado potencial. No hay balas de plata (véase la Receta 4.1,
"Crear objetos pequeños") que te impidan sobrediseñar. Por ejemplo,
cambiar el color de un componente visual debería ser un contraejemplo.
Debes ser consciente y muy cauto.
SOBREDISEÑAR
El sobrediseño es la práctica de añadir complejidad accidental innecesaria a
una aplicación de software. Esto puede ocurrir cuando te centras demasiado en
hacer que el software tenga tantas funciones como sea posible, en lugar de
mantenerlo sencillo y centrado en la funcionalidad principal.
Recetas relacionadas
Receta 3.3, "Eliminar definidores de objetos"
Receta 16.2, "Eliminar la optimización prematura"
10.4 Eliminar la astucia del código
Problema
El código te parece difícil de leer y engañoso, lleno de nombres sin
semántica. A veces el código utiliza la complejidad accidental de un
lenguaje.
Solución
Elimina la astucia y los trucos. Sé humilde y no actúes como si fueras
demasiado listo. El código limpio pide legibilidad y sencillez por encima
de los micro hacks.
Debate
La ingeniosidad es lo contrario de la legibilidad y la mantenibilidad. El
código ingenioso lleno de optimizaciones prematuras es difícil de
mantener y suele tener problemas de calidad. He aquí un algoritmo para
obtener los factores primos de un número:
function primeFactors(n){
var f = [], i = 0, d = 2;
for (i = 0; n >= 2; ) {
if(n % d == 0){
f[i++]=(d);
n /= d;
}
else{
d++;
}
}
return f;
}
Deberías cubrirlo con pruebas para asegurarte de que no rompes el
código. Luego haz pequeñas refactorizaciones y renombramientos,
utilizando las recetas de este libro para dejar el código más limpio. Sigue
la regla del Boy Scout: elimina lo ingenioso para que el código sea mejor
que cuando lo encontraste por primera vez:
function primeFactors(numberToFactor) {
var factors = [],
divisor = 2,
remainder = numberToFactor;
while(remainder>=2) {
if(remainder % divisor === 0){
[Link](divisor);
remainder = remainder / divisor;
}
else {
divisor++;
}
}
return factors;
}
Una excepción notable en la que debes ejercitar la inteligencia es el
código optimizado para operaciones de bajo nivel, ya que el rendimiento
es más importante que la legibilidad en tales escenarios. Puedes escribir
el código sin optimizarlo, y luego cubrirlo con pruebas automatizadas
para asegurarte de que funciona como se espera. Cuando tengas
suficiente cobertura de pruebas, puedes mejorarlo, incluso hasta el punto
de sacrificar algo de legibilidad. También es una oportunidad para utilizar
la técnica de desarrollo dirigido por pruebas (ver Receta 4.8, "Eliminar
propiedades innecesarias") en sistemas existentes.
Recetas relacionadas
Receta 6.8, "Sustituir números mágicos por constantes"
Receta 6.15, "Evitar las correcciones mágicas"
Receta 16.2, "Eliminar la optimización prematura"
10.5 Romper promesas múltiples
Problema
Tienes promesas independientes y tienes que esperar a que se completen
todas.
Solución
No te bloquees de forma ordenada. Espera todas las promesas a la vez.
Debate
Mientras estudiabas sistemas operativos, probablemente aprendiste sobre
los semáforos, que son útiles para esperar hasta que se cumplan todas las
condiciones, independientemente de su orden.
SEMÁFOROS
Un semáforo es un objeto de sincronización que ayuda a gestionar el acceso a
recursos compartidos y a coordinar la comunicación entre procesos o hilos
concurrentes.
Aquí tienes un ejemplo con promesas en serie:
async fetchLongTask() { }
async fetchAnotherLongTask() { }
async fetchAll() {
let result1 = await [Link]();
let result2 = await [Link]();
// But they can run in parallel !!
}
Esto es lo que parece cuando los esperas en paralelo:
async fetchLongTask() { }
async fetchAnotherLongTask() { }
async fetchAll() {
let [result1, result2] =
await [Link]([[Link](),
[Link]()]);
// You wait until ALL are done
}
Puedes decirle a tus linters que busquen ciertos patrones relacionados
con la espera de promesas y que trabajen siempre para acercarse lo más
posible a las reglas empresariales del mundo real. Si la regla establece que
debes esperar a todas las operaciones, no debes forzar un orden
concreto.
PROMESAS
Una promesa es un objeto especial que representa la finalización (o el fallo) de
una operación asíncrona y su valor resultante.
10.6 Romper largas cadenas de
colaboraciones
Problema
Tienes largas cadenas de llamadas a métodos.
Solución
Hacer largas cadenas de métodos genera acoplamiento y efectos dominó.
Cualquier cambio a lo largo de la cadena rompe el código. La solución es
enviar mensajes sólo a tus conocidos.
Debate
Si tienes largas cadenas de métodos, el acoplamiento se propaga de la
primera a la última llamada. También rompes la encapsulación y violas la
ley de Demeter (ver Receta 3.8, "Eliminar Getters") y el principio "Dilo, no
lo preguntes" (ver Receta 3.3, "Eliminar Setters de los Objetos"). Para
resolver este problema, puedes crear métodos intermedios y mensajes de
nivel superior.
El siguiente ejemplo pide que se muevan las patas del perro:
class Dog {
constructor(feet) {
[Link] = feet;
}
getFeet() {
return [Link];
}
}
class Foot {
move() { }
}
feet = [new Foot(), new Foot(), new Foot(), new Foot()];
dog = new Dog(feet);
for (var foot of [Link]()) {// incursion = 2
[Link]();
}
// Equivalent to [Link]()[0].move(); [Link]()[1].move() ...
Esto es lo que parece cuando delegas en el perro la responsabilidad de
conseguir su objetivo:
class Dog {
constructor(feet) {
[Link] = feet;
}
walk() {
// This is encapsulated on how the dog walks
for (var foot of [Link]) {
[Link]();
}
}
}
class Foot {
move() { }
}
feet = [new Foot(), new Foot(), new Foot(), new Foot()];
dog = new Dog(feet);
[Link]();
Evita las llamadas sucesivas de mensajes. Intenta ocultar las
colaboraciones intermedias y crea en su lugar nuevos protocolos.
Recetas relacionadas
Receta 17.9, "Eliminar al intermediario"
10.7 Extraer un método a un objeto
Problema
Tienes un método algorítmico largo. Quieres comprenderlo, probarlo y
reutilizar algunas partes.
Solución
Muévelo dentro de un objeto y divídelo en partes más pequeñas.
Debate
Los métodos largos son difíciles de depurar y probar, sobre todo si tienen
visibilidad protegida. Los algoritmos existen en el mundo real y merecen
sus propios objetos. Necesitas crear un objeto que represente una
invocación del método, trasladar el método grande al nuevo objeto y
convertir las variables temporales del método en atributos privados. Por
último, elimina los parámetros de la invocación del método
convirtiéndolos también en atributos privados.
Un objeto-método es adecuado cuando utilizas varios métodos de
extracción, pasando un estado parcial entre ellos como partes de un
algoritmo. Un fuerte indicador de la oportunidad de un objeto-método es
cuando los cálculos no están cohesivamente relacionados con el método
anfitrión. También puedes reificar las funciones anónimas con objetos-
método más atómicos, cohesivos y comprobables.
Imagina un gran método de cálculo de saldo:
class BlockchainAccount {
// ...
public double balance() {
string address;
// Very long untestable method
}
}
Este es su aspecto cuando lo reificas y refactorizas:
class BlockchainAccount {
// ...
public double balance() {
return new BalanceCalculator(this).netValue();
}
}
// 1. Create an object to represent an invocation of the method
// 2. Move the big method to the new object
// 3. Convert the temporary variables of the method into private
attributes
// 4. Break the big method in the new object by using the Extract
Method
// 5. Remove parameters from method invocation
// by also converting them to private attributes
class BalanceCalculator {
private string address;
private BlockchainAccount account;
public BalanceCalculator(BlockchainAccount account) {
[Link] = account;
}
public double netValue() {
[Link]();
//...
this computeTransactions();
}
}
Algunos IDE disponen de herramientas para extraer una función en un
objeto método. Puedes hacer los cambios automáticamente de forma
segura y extraer la lógica en un nuevo componente, probarlo
unitariamente, reutilizarlo, intercambiarlo, etc.
Recetas relacionadas
Receta 11.1, "Romper métodos demasiado largos"
Receta 11.2, "Reducir el exceso de argumentos"
Receta 14.4, "Sustitución de las sentencias Switch/Case/Elseif"
Receta 14.13, "Extraer de ternarios largos"
Receta 20.1, "Probar métodos privados"
Receta 23.2, "Reificar funciones anónimas"
Ver también
Definición original del objeto método en el capítulo 3 de Smalltalk Best
Practice Patterns de Kent Beck
"Objeto Método" en C2 Wiki
10.8 Cuidar los constructores de
matrices
Problema
Utiliza Array creación en JavaScript con new Array().
Solución
Ten mucho cuidado con JavaScript Arrays y evita new Array(), ya que no es
homogéneo ni predecible.
Debate
new Array()viola el principio de menor sorpresa (ver Receta 5.6,
"Congelar constantes mutables") en JavaScript, ya que este lenguaje tiene
muchos trucos de magia. Un lenguaje debería ser intuitivo, homogéneo,
predecible y sencillo, pero JavaScript, Python, PHP y muchos otros no lo
son. Debes utilizar estos lenguajes de la forma más sencilla, clara y
predecible posible.
Aquí tienes un ejemplo contraintuitivo cuando creas una matriz con un
solo argumento (el número 5):
const arrayWithFixedLength = new Array(3);
[Link](arrayWithFixedLength); // [ <3 empty items> ]
[Link](arrayWithFixedLength[0]); // Undefined
[Link](arrayWithFixedLength[1]); // Undefined
[Link](arrayWithFixedLength[2]); // Undefined
[Link](arrayWithFixedLength[3]); // Undefined too
// But should be Index out of range
[Link]([Link]); // 3
Y esto es lo que ocurre cuando lo creas con dos argumentos:
const arrayWithTwoElements = new Array(3, 1);
[Link](arrayWithTwoElements); // [ 3, 1 ]
[Link](arrayWithTwoElements[0]); // 3
[Link](arrayWithTwoElements[1]); // 1
[Link](arrayWithTwoElements[2]); // Undefined
[Link](arrayWithTwoElements[5]); // Undefined (should be out of
range)
[Link]([Link]); // 2
const arrayWithTwoElementsLiteral = [3,1];
[Link](arrayWithTwoElementsLiteral); // [ 3, 1 ]
[Link](arrayWithTwoElementsLiteral[0]); // 3
[Link](arrayWithTwoElementsLiteral[1]); // 1
[Link](arrayWithTwoElementsLiteral[2]); // Undefined
[Link](arrayWithTwoElementsLiteral[5]); // Undefined
[Link]([Link]); // 2
La mejor solución es evitar la creación de new Array() y utilizar el
constructor sintáctico []. Muchos lenguajes "modernos" están llenos de
hacks destinados a facilitar la vida a los programadores, pero en realidad
son una fuente de posibles defectos no descubiertos.
Recetas relacionadas
Receta 10.4, "Eliminar la astucia del código"
Receta 13.3, "Utilizar parámetros más estrictos"
Receta 24.2, "Tratar con valores de verdad"
10.9 Eliminar Objetos Poltergeist
Problema
Tienes un objeto que aparece y desaparece misteriosamente.
Solución
Añade las capas de indirección necesarias, pero nada más.
Debate
Al añadir objetos intermedios, añades complejidad accidental y perjudicas
la legibilidad. Puedes eliminar los objetos intermedios volátiles si no
añaden valor empresarial a tu solución, siguiendo el principio YAGNI
(consulta el Capítulo 12, "YAGNI").
OBJETOS POLTERGEIST
Un poltergeist es un objeto efímero que se utiliza para realizar una
inicialización o para invocar métodos de otra clase más permanente.
Aquí tienes un conductor que creas y descartas para mover un coche:
public class Driver
{
private Car car;
public Driver(Car car)
{
[Link] = car;
}
public void DriveCar()
{
[Link]();
}
}
Car porsche = new Car();
Driver homer = new Driver(porsche);
[Link]();
Puedes quitarlo, como se muestra aquí:
// You don't need the driver
Car porsche = new Car();
[Link]();
No añadas complejidad accidental a la complejidad esencial que ya tienes,
y elimina los objetos intermediarios si no son necesarios.
Recetas relacionadas
Receta 16.6, "Quitar los botes de ancla"
Receta 17.9, "Eliminar al intermediario"
Capítulo 11. Bloaters
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y
comentarios: translation-feedback@[Link]
El objetivo de la ingeniería de software es controlar la complejidad, no
crearla.
Pamela Zave en Perlas de programación, 2ª edición de Jon Bentley
11.0 Introducción
Los bloaters son inevitables cuando tu código crece y colabora mucha
gente. A menudo no causan problemas de rendimiento, pero perjudican la
mantenibilidad y la comprobabilidad, impidiendo que el buen software
evolucione. El código se vuelve innecesariamente grande, complejo y
difícil de mantener, a menudo debido a la inclusión de características
innecesarias, malas elecciones de diseño o excesiva repetición. El código
se hincha en pequeños pasos y luego te das cuenta de que tienes un gran
lío. No escribes métodos largos, pero tal vez añades pequeñas porciones y
un compañero de equipo añade algunas más, y así sucesivamente. Éste es
un tipo de deuda técnica (véase el Capítulo 21, "Deuda técnica") que es
más fácil reducir con herramientas automatizadas de última generación.
11.1 Romper métodos demasiado
largos
Problema
Tienes un método con demasiadas líneas de código.
Solución
Extrae el método largo en trozos más pequeños. Divide los algoritmos
complejos en partes. También puedes realizar pruebas unitarias de estas
partes.
Debate
Los métodos largos tienen baja cohesión y alto acoplamiento. Son difíciles
de depurar y tienen poca reutilización. Puedes utilizar esta receta para
dividir las bibliotecas estructuradas y los ayudantes en comportamientos
más pequeños (consulta la Receta 7.2, "Renombrar y dividir ayudantes y
utilidades"). La cantidad de líneas depende del lenguaje de programación,
pero entre 8 y 10 líneas deberían bastar para la mayoría de ellos.
Aquí tienes un método largo:
function setUpChessBoard() {
$this->placeOnBoard($this->whiteTower);
$this->placeOnBoard($this->whiteKnight);
// A lot of lines
// .....
$this->placeOnBoard($this->blackTower);
}
Puedes dividirlo en partes de la siguiente manera
function setUpChessBoard() {
$this->placeWhitePieces();
$this->placeBlackPieces();
}
Ahora puedes realizar pruebas unitarias de cada uno de ellos; debes tener
cuidado de no acoplar las pruebas a los detalles de
implementación. Todos los linters pueden medir y avisarte cuando los
métodos superan un umbral predefinido.
Recetas relacionadas
Receta 7.2, "Renombrar y romper ayudantes y utilidades"
Receta 8.6, "Eliminar comentarios dentro de métodos"
Receta 10.7, "Extraer un método a un objeto"
Receta 14.10, "Reescribir código de flechas anidado"
Receta 14.13, "Extraer de ternarios largos"
Ver también
"Método largo" en Refactoring Guru
11.2 Reducir el exceso de argumentos
Problema
Tienes un método que necesita demasiados argumentos.
Solución
No pases más de tres argumentos a tu método. Agrupa los argumentos
relacionados en objetos parámetro. Puedes unirlos.
Debate
Los métodos con demasiados argumentos tienen baja mantenibilidad,
baja reutilización y alto acoplamiento. Necesitas agruparlos para
encontrar relaciones de cohesión entre los argumentos o simplemente
crear un pequeño objeto con el contexto de los argumentos. Cuando crees
un contexto de este tipo, impondrás las relaciones entre parámetros en el
momento de la creación, siguiendo el principio "fail fast".
También debes evitar los tipos "básicos", como cadenas, matrices, enteros,
etc., y pensar en objetos pequeños (véase la Receta 4.1, "Crear objetos
pequeños"). Necesitas relacionar argumentos y agruparlos. Privilegia
siempre las correspondencias con el mundo real. Averigua cómo se
agrupan los homólogos de los argumentos en el mundo real en objetos
cohesionados. Si una función tiene demasiados argumentos, puede que
algunos de ellos estén relacionados con la construcción de clases.
Este ejemplo invoca el método print con muchos argumentos:
public class Printer {
void print(
String documentToPrint,
String paperSize,
String orientation,
boolean grayscales,
int pageFrom,
int pageTo,
int copies,
float marginLeft,
float marginRight,
float marginTop,
float marginBottom
) {
}
}
En su lugar, agrupa algunos de los argumentos para evitar la obsesión
primitiva:
final public class PaperSize { }
final public class Document { }
final public class PrintMargins { }
final public class PrintRange { }
final public class ColorConfiguration { }
final public class PrintOrientation { }
// Class definition with methods and properties omitted for simplicity
final public class PrintSetup {
public PrintSetup(
PaperSize papersize,
PrintOrientation orientation,
ColorConfiguration color,
PrintRange range,
int copiesCount,
PrintMargins margins
) {}
}
final public class Printer {
void print(
Document documentToPrint,
PrintSetup setup
) {
}
}
La mayoría de los linters avisan cuando la lista de argumentos es
demasiado grande, así que puedes aplicar esta receta si es necesario.
Recetas relacionadas
Receta 3.7, "Completar constructores vacíos"
Receta 9.5, "Unificar el orden de los parámetros"
Receta 10.7, "Extraer un método a un objeto"
Receta 11.6, "Romper demasiados atributos"
11.3 Reducir el exceso de variables
Problema
Tu código tiene demasiadas variables declaradas y activas.
Solución
Rompe el ámbito y deja que las variables sean lo más locales posible.
Debate
Si reduces el ámbito de las variables, tendrás una mejor legibilidad y
reutilizarás trozos más pequeños del código. También encontrarás
oportunidades para eliminar variables no utilizadas. Tu código puede
ensuciarse al programar y los casos de prueba pueden fallar. Con una
buena cobertura, puedes iterar y reducir ámbitos mientras refactorizas y
reduces métodos utilizando la Receta 10.7, "Extraer un método a un
objeto". Los ámbitos son más evidentes en contextos más pequeños.
El siguiente ejemplo tiene muchas variables activas a la vez:
function retrieveImagesFrom(array $imageUrls) {
foreach ($imageUrls as $index => $imageFilename) {
$imageName = $imageNames[$index];
$fullImageName = $this->directory() . "\\" . $imageFilename;
if (!file_exists($fullImageName)) {
if (str_starts_with($imageFilename,
'[Link] {
$url = $imageFilename;
// This variable duplication is not really necessary
// when you scope variables
$save_to = "\\tmp"."\\".basename($imageFilename);
$ch = curl_init ($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$raw = curl_exec($ch);
curl_close ($ch);
if(file_exists($saveTo)){
unlink($saveTo);
}
$fp = fopen($saveTo,'x');
fwrite($fp, $raw);
fclose($fp);
$sha1 = sha1_file($saveTo);
$found = false;
$files = array_diff(scandir($this->directory()),
array('.', '..'));
foreach ($files as $file){
if ($sha1 == sha1_file($this->directory()."\\".
$file)) {
$images[$imageName]['remote'] = $imageFilename;
$images[$imageName]['local'] = $file;
$imageFilename = $file;
$found = true;
// Iteration keeps going on even after you find
it
}
}
if (!$found){
throw new \Exception('Image not found');
}
// Debugging at this point your context is polluted with
variables
// from previous executions no longer needed
// for example: the curl handler
}
Este es el aspecto después de reducir algunos de los alcances:
function retrieveImagesFrom(string imageUrls) {
foreach ($imageUrls as $index => $imageFilename) {
$imageName = $imageNames[$index];
$fullImageName = $this->directory() . "\\" . $imageFilename;
if (!file_exists($fullImageName)) {
if ($this->isRemoteFileName($imageFilename)) {
$temporaryFilename = $this-
>temporaryLocalPlaceFor($imageFilename);
$this->retrieveFileAndSaveIt($imageFilename,
$temporaryFilename);
$localFileSha1 = sha1_file($temporaryFilename);
list($found, $images, $imageFilename) =
$this->tryToFindFile(
$localFileSha1, $imageFilename, $images,
$imageName);
if (!$found) {
throw new Exception('File not found locally ('.
$imageFilename
+ ') Need to retrieve it and store it');
}
} else {
throw new \Exception('Image does not exist on directory
' .
$fullImageName);
}
}
La mayoría de los linters pueden sugerirte que evites el uso de métodos
largos, y esta advertencia también te sugiere que rompas y amplíes el
alcance de tus variables. Deberías utilizar la Receta 10.7, "Extraer un
método a un objeto", en pequeños pasos de bebé.
PASOS DE BEBÉ
Pasos de bebé se refiere a un enfoque iterativo e incremental en el que realizas
tareas o cambios pequeños y manejables durante el proceso de desarrollo. El
concepto de pasos de bebé está arraigado en la metodología de desarrollo ágil.
Recetas relacionadas
Receta 6.1, "Limitar las variables reutilizadas"
Receta 11.1, "Romper métodos demasiado largos"
Receta 14.2, "Cambiar el nombre de las variables de indicador para
eventos"
11.4 Eliminar paréntesis excesivos
Problema
Has una expresión con demasiados paréntesis.
Solución
Utiliza el menor número posible de paréntesis sin cambiar la semántica
del código.
Debate
Lees el código de izquierda a derecha (al menos en la cultura occidental),
y los paréntesis a menudo rompen este flujo, añadiendo complejidad
cognitiva. Escribes código una vez y lo lees muchas más, por lo que la
legibilidad es lo más importante. He aquí una fórmula con demasiados
paréntesis utilizada para calcular el radio de Schwarzschild, que es una
medida del tamaño de un agujero negro no giratorio:
schwarzschild = ((((2 * GRAVITATION_CONSTANT)) * mass) /
((LIGHT_SPEED ** 2)))
Así es como queda después de eliminar los paréntesis sobrantes:
schwarzschild = (2 * GRAVITATION_CONSTANT * mass) / (LIGHT_SPEED **
2)
Puedes compactarlo más:
schwarzschild = 2 * GRAVITATION_CONSTANT * mass / (LIGHT_SPEED ** 2)
Siguiendo el orden de las operaciones en matemáticas, sabes que la
multiplicación y la división tienen prioridad sobre la suma y la resta, por
lo que la multiplicación de 2, GRAVITATION_CONSTANT, y mass puede
realizarse primero, seguida de la división por (LIGHT_SPEED ** 2),
pero es menos legible que el ejemplo anterior. En algunas fórmulas
complejas, puedes añadir paréntesis adicionales para que el término sea
más legible. Como ocurre con muchas recetas, siempre se trata de un
compromiso.
Recetas relacionadas
Receta 6.8, "Sustituir números mágicos por constantes"
11.5 Eliminar el exceso de métodos
Problema
Tienes demasiados métodos en tu clase.
Solución
Divide la clase en pequeños trozos más cohesionados, y no añadas ningún
protocolo accidental a tus clases.
Debate
Los ingenieros tienden a poner un protocolo en la primera clase que
creen conveniente. Eso no es un problema; sólo tienes que refactorizarlo
después de que tus pruebas cubran la funcionalidad. En este ejemplo, la
clase ayudante tiene muchos métodos incoherentes:
public class MyHelperClass {
public void print() { }
public void format() { }
// ... many methods more
// ... even more methods
public void persist() { }
public void solveFermiParadox() { }
}
Puedes dividirlos en abstracciones relevantes utilizando el MAPPER:
public class Printer {
public void print() { }
}
public class DateToStringFormatter {
public void format() { }
}
public class Database {
public void persist() { }
}
public class RadioTelescope {
public void solveFermiParadox() { }
}
La mayoría de los linters cuentan los métodos y pueden avisarte para que
refactorices las clases. Dividirlas es una buena práctica para favorecer los
objetos pequeños y reutilizables.
Recetas relacionadas
Receta 7.2, "Renombrar y romper ayudantes y utilidades"
Receta 11.6, "Romper demasiados atributos"
Receta 11.7, "Reducir las listas de importación "
Receta 17.4, "Romper el cambio divergente"
Receta 17.15, "Refactorizar cúmulos de datos"
Ver también
"Clase grande" en Refactoring Guru
11.6 Romper demasiados atributos
Problema
Tienes una clase que define objetos con muchos atributos.
Solución
Divide la clase en partes cohesionadas. Encuentra métodos relacionados
con atributos, luego agrupa estos métodos y rompe el objeto relacionado
con esos grupos. Al final, encuentra objetos reales relacionados con estos
nuevos objetos y sustituye las referencias existentes.
Debate
Aquí puedes ver una hoja de cálculo con demasiados atributos:
class ExcelSheet {
String filename;
String fileEncoding;
String documentOwner;
String documentReadPassword;
String documentWritePassword;
DateTime creationTime;
DateTime updateTime;
String revisionVersion;
String revisionOwner;
List previousVersions;
String documentLanguage;
List cells;
List cellNames;
List geometricShapes;
}
Este es el aspecto después de dividirlo en partes:
class ExcelSheet {
FileProperties fileProperties;
SecurityProperties securityProperties;
DocumentDatingProperties datingProperties;
RevisionProperties revisionProperties;
LanguageProperties languageProperties;
DocumentContent content;
}
// Object has less attributes
// They are not only grouped for testability
// New objects are more cohesive, more testable,
// have fewer conflicts, and are more reusable
// FileProperties/SecurityProperties can be reused for other documents
// Rules and preconditions on fileProperties will be moved to this
object
// so ExcelSheet constructor will be cleaner
La mayoría de los linters te avisan cuando declaras demasiados atributos.
Establecer un buen umbral de advertencia debería ser fácil, ya que los
objetos hinchados saben demasiado y son muy difíciles de cambiar debido
a la cohesión. Los desarrolladores cambian mucho estos objetos, por lo
que crean conflictos de fusión y son una fuente habitual de problemas.
Recetas relacionadas
Receta 11.2, "Reducir el exceso de argumentos"
Receta 11.5, "Eliminar el exceso de métodos"
Receta 17.3, "Romper objetos de Dios"
Receta 17.4, "Romper el cambio divergente"
11.7 Reducir las listas de importación
Problema
Tu clase depende de demasiadas otras; estará acoplada y será frágil. Una
larga lista de importaciones es un buen indicador de este problema.
Solución
No importes demasiadas cosas en el mismo archivo; rompe las
dependencias y el acoplamiento.
Debate
Puedes romper la clase y ocultar la implementación accidental
intermedia. Aquí tienes una lista de importaciones muy larga:
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link]
import [Link];
import [Link];
import [Link];
// You rely on too many libraries
public class Demo {
public static void main(String[] args) {
}
}
Aquí tienes una solución simplificada:
import [Link];
import [Link];
// You rely on few libraries
// and you hide their implementation
// Maybe transitive imports are the same
// but you don't break encapsulation
public class Demo {
public static void main(String[] args) {
}
}
Puedes establecer un umbral de alerta en tus linters. También tendrás
que pensar en las dependencias cuando construyas tus soluciones para
minimizar los efectos dominó. La mayoría de los IDE modernos emitirán
advertencias por importaciones no utilizadas.
Recetas relacionadas
Receta 11.5, "Eliminar el exceso de métodos"
Receta 17.4, "Romper el cambio divergente"
Receta 17.14, "Cambiar el acoplamiento a clases"
Receta 25.3, "Eliminar dependencias de paquetes"
11.8 Romper las funciones "Y
Problema
Tienes funciones que realizan más de una tarea.
Solución
A menos que necesites atomicidad, no realices más de una tarea por
función y rompe las funciones compuestas.
Debate
Si encuentras una función con "y" como parte del nombre y no necesitas
atomicidad, debes dividirla porque hacer dos cosas en el mismo ámbito
provoca código acoplado. El código es más difícil de probar y leer. Puedes
extraer y romper el método. Hacer más de una cosa a la vez crea
acoplamiento y viola el principio de responsabilidad única (ver Receta 4.7,
"Reificar las validaciones de cadenas"). También perjudica la
comprobabilidad.
Aquí tienes una función que realiza dos tareas:
def fetch_and_display_personnel():
data = # ...
for person in data:
print(person)
Así es como queda después de romperlo:
def fetch_personnel():
return # ...
def display_personnel(data):
for person in data:
print(person)
He aquí otro ejemplo:
calculatePrimeFactorsRemoveDuplicatesAndPrintThem()
// Three responsibilities
Así es como queda después de dividirlo en tres partes:
calculatePrimeFactors();
removeDuplicates();
printNumbers();
// Three different methods
// You can test them and reuse them
Las funciones con "y" en su nombre son buenas candidatas para
romperse. Sin embargo, debes comprobarlas cuidadosamente, ya que
puede haber falsos positivos. Debes evitar hacer más de lo necesario, y tus
funciones deben ser mínimas y atómicas. Al hacer métodos, es muy
importante utilizar el método del patito de goma para determinar si estás
haciendo las cosas correctamente.
DEPURACIÓN DEL PATITO DE GOMA
La depuración con patito degoma es el concepto de explicar tu código línea a
línea como si estuvieras enseñando a programar a un patito de goma. Al
verbalizar y describir cada paso de tu código, puedes descubrir errores o
incoherencias lógicas que antes habías pasado por alto.
Recetas relacionadas
Receta 11.1, "Romper métodos demasiado largos"
11.9 Romper las interfaces gordas
Problema
Tienes interfaces declarando demasiado protocolo.
Solución
Divide tus interfaces.
Debate
El término "interfaz gorda" hace hincapié en que la interfaz está
sobrecargada de métodos, incluidos aquellos que pueden no ser
necesarios o utilizados por todos los clientes. La interfaz viola el principio
de segregar las interfaces en contratos más pequeños y centrados.
PRINCIPIO DE SEGREGACIÓN DE INTERFACES
El principio de segregación de interfaces establece que no se debe obligar a los
objetos a depender de interfaces que no utilizan. Es mejor tener muchas
interfaces pequeñas y especializadas que una interfaz grande y monolítica.
En el siguiente ejemplo, anulas algunos comportamientos:
interface Animal {
void eat();
void sleep();
void makeSound();
// This protocol should be common to all animals
}
class Dog implements Animal {
public void eat() { }
public void sleep() { }
public void makeSound() { }
}
class Fish implements Animal
public void eat() { }
public void sleep() {
throw new UnsupportedOperationException("I do not sleep");}
public void makeSound() {
throw new UnsupportedOperationException("I cannot make sounds");
}
}
class Bullfrog implements Animal
public void eat() { }
public void sleep() {
throw new UnsupportedOperationException("I do not sleep");
}
public void makeSound() { }
}
Cuando segregas las interfaces en otras más atómicas:
interface Animal {
void move();
void reproduce();
}
// You can even break these two responsibilities
class Dog implements Animal {
public void move() { }
public void reproduce() { }
}
class Fish implements Animal {
public void move() { }
public void reproduce() { }
}
class Bullfrog implements Animal {
public void move() { }
public void reproduce() { }
}
Puedes comprobar el tamaño del comportamiento de la interfaz y valorar
la cohesión de todo el protocolo. Favorecer componentes de código
pequeños y reutilizables promueve la reutilización del código y del
comportamiento.
Recetas relacionadas
Receta 12.4, "Eliminar interfaces de un solo uso"
Receta 17.14, "Cambiar el acoplamiento a clases"
Capítulo 12. YAGNI
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y
comentarios: translation-feedback@[Link]
Einstein argumentó repetidamente que debe haber explicaciones
simplificadas de la naturaleza, porque Dios no es caprichoso ni arbitrario.
Ninguna fe semejante consuela al ingeniero de software.
Fred Brooks, El Mítico Hombre-Mes: Ensayos sobre ingeniería de
software
12.0 Introducción
YAGNI, que significa "You Ain't Gonna Need It" (No lo vas a necesitar),
aconseja a los desarrolladores que sólo implementen características o
funcionalidades que realmente se necesiten en ese momento, en lugar de
añadir características o funcionalidades innecesarias que quizá no se
utilicen en el futuro. La idea detrás de YAGNI es minimizar la complejidad
accidental y mantener la atención en las tareas más importantes.
El principio YAGNI puede verse como un contrapunto a la tendencia
habitual en el desarrollo de software de diseñar soluciones en exceso o
añadir características innecesarias en previsión de necesidades o
requisitos futuros. Esto puede llevar a una complejidad innecesaria,
pérdida de tiempo y esfuerzo, y costes de mantenimiento inflados.
El principio YAGNI anima a los desarrolladores a mantener su atención en
las necesidades inmediatas del proyecto y a añadir únicamente las
características o funcionalidades que sean necesarias para satisfacer esas
necesidades. Esto ayuda a mantener el proyecto simple y centrado y
permite a los desarrolladores ser más ágiles y responder mejor a los
requisitos cambiantes.
12.1 Eliminar el Código Muerto
Problema
Tienes código que ya no se utiliza ni se necesita.
Solución
No guardes el código "por si lo necesito". Elimínalo.
Debate
El código muerto perjudica la mantenibilidad y viola el principio KISS
(véase la Receta 6.2, "Eliminar líneas vacías"), ya que si el código no se
ejecuta, nadie lo mantiene. Mira el siguiente ejemplo con código dorado:
class Robot {
walk(){
//...
}
serialize(){
//...
}
persistOnDatabase(database){
//...
}
}
CHAPADO EN ORO
El chapado en oro se refiere a la práctica de de añadir características o
funcionalidades innecesarias a un producto o proyecto, más allá de los requisitos
o especificaciones mínimos. Esto puede ocurrir por diversas razones, como el
deseo de impresionar al cliente o de hacer que el producto destaque en el
mercado. Sin embargo, el gold plating puede ser perjudicial para el proyecto, ya
que puede provocar sobrecostes y retrasos, y puede no aportar ningún valor real
al usuario final.
Aquí tienes un objeto más sencillo con las responsabilidades adecuadas:
class Robot {
walk(){
// ...
}
}
ADVERTENCIA
Las herramientas de cobertura de pruebas pueden encontrar código muerto (sin
cubrir) si tienes un gran conjunto de pruebas. Pero ten cuidado porque la
cobertura tiene problemas con la metaprogramación (consulta el Capítulo 23,
"Metaprogramación"). Cuando se utiliza, es muy difícil encontrar referencias al
código. Elimina el código muerto para simplificar. Si no estás seguro de tu
código, puedes desactivarlo temporalmente utilizando los conmutadores de
funciones. Eliminar código siempre es más gratificante que añadirlo, y siempre
puedes encontrarlo en el historial de Git (consulta la Receta 8.1, "Eliminar código
comentado").
Recetas relacionadas
Receta 16.6, "Quitar los botes de ancla"
Receta 23.1, "Eliminar el uso de la metaprogramación"
12.2 Utilizar código en lugar de
diagramas
Problema
En utilizas diagramas para documentar cómo debe funcionar el software.
Solución
Utiliza el código y las pruebas como documentación autónoma.
Debate
La mayoría de los diagramas se centran sólo en la estructura (accidental)
y no en el comportamiento (esencial). Sólo debes utilizarlos para
comunicar ideas con otros humanos. Confía en tus pruebas. Están vivas y
bien mantenidas.
La Figura 12-1 presenta un diagrama de muestra del Lenguaje Unificado
de Modelado (UML). Aunque el diagrama es útil, es más importante que
entiendas el código y las pruebas en su lugar, ya que el diagrama puede
quedar obsoleto durante el desarrollo; las pruebas no pueden mentir si
las ejecutas continuamente.
Figura 12-1. Un sencillo diagrama UML que representa una biblioteca
Aquí tienes el código de ejecución simplificado de la parte de modelado de
diagramas del dominio Biblioteca:
final class BookItem {
function numberOfPages() { }
function language(): Language { }
function book(): Book { }
function edition(): BookEdition { }
// Loan and overdues are not book items responsibility
}
final class LoanTracker {
function loan(
BookItem $bookCopy,
LibraryUser $reader,
DatePeriod $loanDates) {
// DatePeriod is better than anemic $fromDate and $toDate
}
}
final class LoanTrackerTests extends TestCase {
// Lots of maintained tests telling you how the system really
works
}
Elimina todas las anotaciones de código y prohíbelas en una política bien
conocida. El diseño de software es un deporte de contacto, y necesitas
crear prototipos y aprender de tus modelos en ejecución. Las hojas y los
JPEG son estáticos y no se ejecutan; viven en un mundo utópico donde
todo funciona sin problemas. Algunos diagramas de arquitectura de alto
nivel son útiles para comprender el cuadro completo y comunicar ciertos
conceptos a los demás.
DIAGRAMAS UML
DiagramasUML (Lenguaje Unificado de Modelado) son representaciones visuales
estándar que describen la estructura y el comportamiento de un sistema o
aplicación de software con un conjunto común de símbolos y notaciones.
Estuvieron de moda en los años 80 y 90 y están estrechamente relacionados con
el modelo de desarrollo en cascada, en el que el diseño se termina antes de
empezar la codificación real, a diferencia de las metodologías ágiles. Muchas
organizaciones siguen utilizando UML hoy en día.
Recetas relacionadas
Receta 3.1, "Convertir objetos anémicos en objetos ricos"
Receta 12.5, "Eliminar abusos de patrones de diseño"
Ver también
"Ingeniería de software asistida por ordenador" en Wikipedia
MODELO EN CASCADA
Según David Farley, el modelo de cascada, tal como se aplica al desarrollo de
software, es un enfoque secuencial y por etapas para organizar el trabajo
dividiéndolo en una serie de fases distintas con traspasos bien definidos entre
cada fase. La idea es que abordes cada fase por turnos, en lugar de iterar. Esta
era la idea dominante hasta que las metodologías ágiles cobraron importancia
en los años 90.
12.3 Refactorizar clases con una
subclase
Problema
Tienes una clase con una sola subclase.
Solución
No generalices demasiado de antemano, ya que se trata de un diseño
especulativo; en su lugar, trabaja con los conocimientos que ya has
adquirido. Elimina la clase abstracta hasta que tengas ejemplos más
concretos.
Debate
En el pasado, los expertos solían decir a los ingenieros que diseñaran para
el cambio. Hoy en día, en cambio, hay que trabajar con pruebas reales.
Cuando encuentres una duplicación, elimínala, pero no antes. He aquí un
ejemplo con diseño especulativo:
class Boss(object):
def __init__(self, name):
[Link] = name
class GoodBoss(Boss):
def __init__(self, name):
super().__init__(name)
# This is actually a poor classification example
# Bosses should be immutable but can change their mood
# with constructive feedback
Este es el aspecto después de compactar la jerarquía:
class Boss(object):
def __init__(self, name):
[Link] = name
# Bosses are concrete and can change mood
La detección es muy fácil para los linters, ya que pueden rastrear este
error en tiempo de compilación. Subclasificar nunca debe ser tu primera
opción, ya que debes esperar a que surjan abstracciones y no crearlas de
forma especulativa. Una solución más elegante sería declarar una interfaz
si tu lenguaje tiene esta capacidad, ya que está menos acoplada. Como
excepciones, algunos frameworks crean una clase abstracta como
marcador de posición para construir modelos concretos sobre ellas.
Recetas relacionadas
Receta 12.4, "Eliminar interfaces de un solo uso"
Receta 19.3, "Romper la subclasificación para reutilizar el código"
Receta 19.6, "Renombrar clases aisladas"
Receta 19.7, "Hacer clases concretas finales"
Receta 19.8, "Definir explícitamente la herencia de clases"
Receta 19.9, "Migrar clases vacías"
12.4 Eliminar interfaces de un solo uso
Problema
Tienes una interfaz con una sola realización.
Solución
No generalices en exceso hasta que tengas más de un ejemplo para
extraer protocolos útiles y cohesionados.
Debate
Planificar las interfaces de antemano y generalizar los protocolos es un
signo de diseño especulativo y de exceso de ingeniería. El siguiente
ejemplo trata de determinar cómo debe comportarse un vehículo:
public interface Vehicle {
public void start();
public void stop();
}
public class Car implements Vehicle {
public void start() {
[Link]("Running...");
}
public void stop() {
[Link]("Stopping...");
}
}
// No more concrete vehicles??
Como no hay pruebas suficientes, quédate con una realización:
public class Car {
public void start() {
[Link]("Running...");
}
public void stop() {
[Link]("Stopping...");
}
}
// Wait until you discover more concrete vehicles
Hay algunas excepciones a esta receta, ya que esta regla se aplica a la
lógica empresarial. Algunos frameworks definen una interfaz como un
protocolo que hay que cumplir. En tus biyecciones, necesitas modelar
protocolos existentes en el mundo real. Las interfaces son el
análogo MAPPER (como se define en el Capítulo 2) de los
protocolos. Además, los protocolos de inversión de dependencia declaran
interfaces que se cumplen con sus realizaciones. Hasta ese momento,
pueden estar vacías. Si tu lenguaje define una interfaz para burlarse de
las pruebas, deberías plantearte utilizar la Receta 20.4, "Sustituir burlas
por objetos reales". Siempre hay que esperar a las abstracciones y no ser
innecesariamente creativo o especulativo.
INVERSIÓN DE LA DEPENDENCIA
La inversión de la dependencia es un principio de diseño que desacopla los
objetos de nivel superior de los de nivel inferior invirtiendo la relación de
dependencia tradicional. En lugar de hacer que los objetos de nivel superior
dependan directamente de los objetos de nivel inferior, el principio sugiere que
ambos dependan de abstracciones o interfaces. Esto permite una mayor
flexibilidad y modularidad en la base de código, ya que los cambios en la
implementación de un módulo de nivel inferior no requieren necesariamente
cambios en el módulo de nivel superior.
Recetas relacionadas
Receta 7.14, "Eliminar el prefijo/sufijo "Impl de los nombres de las clases"
Receta 12.3, "Refactorizar clases con una subclase"
Receta 20.4, "Sustituir Mocks por Objetos Reales"
12.5 Eliminar los abusos de los
patrones de diseño
Problema
Tu código tiene síntomas de sobrediseño y abusa de algunos patrones de
diseño.
Solución
Elimina los patrones de diseño. Utiliza conceptos más sencillos. Utiliza
nombres basados en conceptos de biyección del mundo real (esenciales)
en lugar de nombres de patrones de implementación (accidentales).
Debate
Las clases de la Tabla 12-1 se nombran en función de la implementación:
Mal ejemplo Buen ejemplo
FileTreeComposite FileSystem
DateTimeConverterAdapterSingle DateTimeFormatt
ton er
PermutationSorterStrategy BubbleSort
NetworkPacketObserver NetworkSniffer
AccountsComposite Portfolio
Tabla 12-1. Nombres malos usando patrones
Los cinco nombres de la Tabla 12-1 pertenecen al mundo real; tu modelo
mental los mapeará 1:1 con objetos familiares utilizando la biyección. Lo
difícil de eliminar patrones es cambiar el comportamiento de los objetos
para evitar la complejidad añadida por un patrón de diseño en sí.
Recetas relacionadas
Receta 7.7, "Renombrar nombres abstractos"
Receta 10.4, "Eliminar la astucia del código"
Receta 12.2, "Utilizar código en lugar de diagramas"
Receta 17.2, "Sustituir Singletons"
12.6 Sustitución de la recaudación
empresarial
Problema
Tienes colecciones especializadas sin comportamiento adicional.
Solución
No crees abstracciones innecesarias. Utiliza una clase de la biblioteca
estándar de tu idioma.
Debate
Descubrir abstracciones en el MAPPER es una tarea difícil. Debes eliminar
las abstracciones innecesarias a menos que añadan un nuevo
comportamiento. Esto es un diccionario de palabras:
Namespace Spelling;
final class Dictionary {
private $words;
function __construct(array $words) {
$this->words = $words;
}
function wordCount(): int {
return count($this->words);
}
function includesWord(string $subjectToSearch): bool {
return in_array($subjectToSearch, $this->words);
}
}
// This has protocol similar to an abstract data type dictionary
// And the tests
final class DictionaryTest extends TestCase {
public function test01EmptyDictionaryHasNoWords() {
$dictionary = new Dictionary([]);
$this->assertEquals(0, $dictionary->wordCount());
}
public function test02SingleDictionaryReturns1AsCount() {
$dictionary = new Dictionary(['happy']);
$this->assertEquals(1, $dictionary->wordCount());
}
public function test03DictionaryDoesNotIncludeWord() {
$dictionary = new Dictionary(['happy']);
$this->assertFalse($dictionary->includesWord('sadly'));
}
public function test04DictionaryIncludesWord() {
$dictionary = new Dictionary(['happy']);
$this->assertTrue($dictionary->includesWord('happy'));
}
}
Puedes utilizar una clase estándar para conseguir lo mismo:
Namespace Spelling;
// final class Dictionary is no longer needed
// The tests use a standard class
// In PHP you use associative arrays
// Java and other languages have HashTables, Dictionaries, etc.
use PHPUnit\Framework\TestCase;
final class DictionaryTest extends TestCase {
public function test01EmptyDictionaryHasNoWords() {
$dictionary = [];
$this->assertEquals(0, count($dictionary));
}
public function test02SingleDictionaryReturns1AsCount() {
$dictionary = ['happy'];
$this->assertEquals(1, count($dictionary));
}
public function test03DictionaryDoesNotIncludeWord() {
$dictionary = ['happy'];
$this->assertFalse(in_array('sadly', $dictionary));
}
public function test04DictionaryIncludesWord() {
$dictionary = ['happy'];
$this->assertTrue(in_array('happy', $dictionary));
}
}
Puedes ver esto como una contradicción con el concepto MAPPER, en el
que necesitas crear objetos que existan en el mundo real para
encontrarlos en la biyección. La primera P de MAPPER proviene de
Parcial. No necesitas modelar todas las entidades reales. Sólo las
relevantes.
ADVERTENCIA
Como excepción, a veces necesitas optimizar las colecciones por razones de
rendimiento, sólo si tienes pruebas suficientemente sólidas (consulta el Capítulo
16, "Optimización prematura"). De vez en cuando necesitas limpiar código, y las
colecciones especializadas son un buen punto de partida.
Recetas relacionadas
Receta 13.5, "Evitar modificar colecciones mientras se recorre"
Capítulo 13. Fracasa rápido
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y
comentarios: translation-feedback@[Link]
Hay un arte en saber dónde deben comprobarse las cosas y en asegurarse
de que el programa falle rápidamente si cometes un error. Ese tipo de
elección forma parte del arte de la simplificación.
Ward Cunningham
13.0 Introducción
La capacidad de fallar rápido es fundamental para un código
limpio. Tienes que actuar en cuanto falle una regla de negocio. Cada fallo
silencioso es una oportunidad de mejora perdida. Para depurar con
precisión un problema necesitas encontrar la causa raíz. Y la causa raíz te
dará una pista certera para rastrear y resolver el fallo. Los sistemas
rápidos ante fallos son más robustos que los sistemas débiles, en los que
los fallos se barren bajo la alfombra y el procesamiento sigue adelante,
incluso después de que el fallo afectara al resultado correcto.
13.1 Refactorización Reasignación de
variables
Problema
Reutilizas variables con distintos ámbitos.
Solución
No reutilices nombres de variables. Rompes la legibilidad y las
posibilidades de refactorización y no ganas nada; es una optimización
prematura que no ahorra memoria. Limita al máximo los ámbitos.
Debate
Si reutilizas una variable y amplías su alcance, las herramientas de
refactorización automática pueden fallar y tu máquina virtual puede
perder la oportunidad de hacer optimizaciones. Se recomienda que
definas, utilices y deseches las variables manteniendo su ciclo de vida
corto. En este ejemplo, hay dos compras no relacionadas:
class Item:
def taxesCharged(self):
return 1;
lastPurchase = Item('Soda');
# Do something with the purchase
taxAmount = [Link]();
# Lots of stuff related to the purchase
# You drink the soda
# You cannot extract method from below without passing
# useless lastPurchase as parameter
# a few hours later…
lastPurchase = Item('Whisky'); # You bought another drink
taxAmount += [Link]();
Esto es lo que parece si reduces el alcance:
class Item:
def taxesCharged(self):
return 1;
def buySupper():
supperPurchase = Item('Soda');
# Do something with the purchase
# Lots of stuff related to the purchase
# You drink the soda
return supperPurchase;
def buyDrinks():
# You can extract the method!
# a few hours later..
drinksPurchase = Item('Whisky');
# I bought another drink
return drinksPurchase;
taxAmount = buySupper().taxesCharged() + buyDrinks().taxesCharged();
La reutilización de variables es también denominada como una
sugerencia de "copiar y pegar no contextual".
Recetas relacionadas
Receta 11.1, "Romper métodos demasiado largos"
OPTIMIZACIÓN DE MÁQUINAS VIRTUALES
Hoy en día, la mayoría de los lenguajes de programación modernos se ejecutan
en máquinas virtuales (VM). Abstraen los detalles del hardware y realizan
muchas optimizaciones bajo el capó para que puedas centrarte en hacer que el
código sea legible y evitar la optimización prematura(consulta el Capítulo 16,
"Optimización prematura"). Escribir código inteligente de alto rendimiento casi
nunca es necesario, ya que resuelven muchos problemas de rendimiento. En el
Capítulo 16 descubrirás cómo recopilar pruebas reales para determinar si
necesitas optimizar el código.
13.2 Hacer cumplir las condiciones
previas
Problema
Quieres crear objetos más robustos utilizando precondiciones,
postcondiciones e invariantes empresariales.
Solución
Activa las aserciones tanto en desarrollo como en producción, a menos
que tengas pruebas sólidas de que se trata de un problema de
rendimiento importante.
Debate
La coherencia de los objetos es clave para estar a la altura
del MAPPER (como se define en el Capítulo 2). Debes avisar en cuanto tu
código rompa un contrato de software. Es más fácil depurar un problema
en cuanto se produce. Como siempre, los constructores son una excelente
primera línea de defensa.
DISEÑO POR CONTRATO
Construcción de software orientado aobjetos, de Bertrand Meyer, es una guía
completa para el desarrollo de software utilizando el paradigma orientado a
objetos. Una de las ideas clave del libro es el concepto de "diseño por contrato",
que subraya la importancia de crear contratos claros e inequívocos entre los
módulos de software. Un contrato especifica las responsabilidades y el
comportamiento que garantizan que los módulos funcionen correctamente
juntos y que el software siga siendo fiable y mantenible a lo largo del tiempo.
Cuando se rompe un contrato, se respeta el principio de "fail fast" y los
problemas se detectan inmediatamente.
He aquí un ejemplo conocido de de validación de Date:
class Date:
def __init__(self, day, month, year):
[Link] = day
[Link] = month
[Link] = year
def setMonth(self, month):
[Link] = month
startDate = Date(3, 11, 2020)
# OK
startDate = Date(31, 11, 2020)
# Should fail, but it doesn't
[Link](13)
# Should fail, but it doesn't
Este es el aspecto que tiene cuando pones los controles incluso antes de
crear el Date:
class Date:
def __init__(self, day, month, year):
if month > 12:
raise Exception("Month should not exceed 12")
#
# etc ...
self._day = day
self._month = month
self._year = year
startDate = Date(3, 11, 2020)
# OK
startDate = Date(31, 11, 2020)
# fails
[Link](13)
# fails since invariant makes object immutable
Siempre debes ser explícito sobre la integridad de los objetos y activar las
aserciones de producción, aunque ello conlleve pequeñas penalizaciones
de rendimiento. La corrupción de datos y objetos es más difícil de
encontrar, por lo que fallar rápido es una bendición.
Recetas relacionadas
Receta 3.1, "Convertir objetos anémicos en objetos ricos"
Receta 25.1, "Desinfección de las entradas"
Ver también
Construcción de software orientado a objetos por Bertrand Meyer
PRECONDICIONES, POSTCONDICIONES E INVARIANTES
Una precondición es una condición que debe ser verdadera antes de llamar a
una función o método. Especifica los requisitos que deben satisfacer las entradas
de la función o el método. Una invariante es una condición que debe cumplirse
en todo momento durante la ejecución de un programa, independientemente de
los cambios que puedan producirse. Especifica una propiedad del programa que
no debe cambiar con el tiempo. Por último, una postcondición está ligada al
momento posterior a la llamada al método. Puedes utilizarlas para garantizar la
corrección, detectar defectos u orientar el diseño del programa.
13.3 Utilizar parámetros más estrictos
Problema
Tienes funciones mágicas que pueden recibir un montón de argumentos
diferentes (y no polimórficos).
Solución
Crea un contrato claro. Espera sólo un protocolo.
Debate
La firma de una función es muy importante, y los lenguajes que favorecen
las fundiciones mágicas son falsos amigos. Prometen soluciones fáciles,
pero pronto te encontrarás depurando valores inesperados. En lugar de
crear una función flexible contaminada con condiciones if, deberías
aceptar siempre un tipo de argumento que se adhiera a un único
protocolo.
FUNCIÓN FIRMA
La firma de una función especifica su nombre, los tipos de los parámetros y el
tipo de retorno si el lenguaje es estrictamente tipado. Se utiliza para distinguir
una función de otra y para garantizar que las llamadas a funciones se realizan
correctamente.
En este ejemplo, tú puedes recibir varios argumentos diferentes y no
polimórficos:
function parseArguments($arguments) {
$arguments = $arguments ?: null;
// Always the billion-dollar mistake (null)
if (is_empty($arguments)) {
$this->arguments = http_build_query($_REQUEST);
// Global coupling and side effects
} elseif (is_array($arguments)) {
$this->arguments = http_build_query($arguments);
} elseif (!$arguments) { // null unmasked
$this->arguments = null;
} else {
$this->arguments = (string)$arguments;
}
}
Aquí tienes una solución canónica única:
function parseArguments(array $arguments) {
$this->arguments = http_build_query($arguments);
}
Los moldes mágicos y la flexibilidad tienen un precio. Barren la basura
debajo de la alfombra y violan el principio de fallo rápido.
Recetas relacionadas
Receta 10.4, "Eliminar la astucia del código"
Receta 15.1, "Crear objetos nulos"
Receta 24.2, "Tratar con valores de verdad"
13.4 Eliminar los valores por defecto de
los interruptores
Problema
No lanzas una excepción cuando deberías hacerlo en las sentencias
switch.
Solución
No añadas una cláusula default a tus declaraciones case. Cámbiala por
una excepción para ser explícito y evitar soluciones especulativas.
Debate
"Por defecto" significa "todo lo que aún no sabes". No puedes prever el
futuro, así que lo predeterminado debe ser una excepción para "casos
imprevistos". Cuando utilizas casos, a menudo añades un caso por defecto
para que no falle. Pero fallar siempre es mejor que tomar decisiones sin
pruebas. Puesto que tanto case como switch suelen ser un problema,
puedes evitarlos utilizando la Receta 14.4, "Sustitución de las sentencias
Switch/Case/Elseif". En C y C++, el caso por defecto es opcional. En Java, el
caso por defecto es obligatorio, y el compilador genera un error si se
omite. En C#, el caso por defecto es opcional, pero el compilador genera
una advertencia si se omite.
Aquí tienes una propuesta especulativa case:
switch (value) {
case value1:
// if value1 matches, the following will be executed...
doSomething();
break;
case value2:
// if value2 matches, the following will be executed...
doSomethingElse();
break;
default:
// if the value does not presently match the above values
// or future values
// the following will be executed
doSomethingSpecial();
break;
}
Esto es lo que parece cuando lo sustituyes por una excepción:
switch (value) {
case value1:
// if value1 matches the following will be executed...
doSomething();
break;
case value2:
// if value2 matches the following will be executed...
doSomethingElse();
break;
case value3:
case value4:
// You currently know these options exist
doSomethingSpecial();
break;
default:
// if value does not match the above values you need to make a
decision
throw new Exception('Unexpected case ' + value + ', need to
consider it');
break;
}
Puedes decirle a tus linters que te adviertan sobre el uso por defecto a
menos que haya una excepción, porque escribir código robusto no
significa que tengas que tomar decisiones sin pruebas. Puesto que hay
muchos usos válidos, debes ser consciente de los falsos positivos.
Recetas relacionadas
Receta 14.4, "Sustitución de las sentencias Switch/Case/Elseif"
13.5 Evitar la modificación de
colecciones durante el desplazamiento
Problema
Tú estás modificando una colección mientras la recorres.
Solución
No modifiques las colecciones mientras las recorres, ya que puedes crear
incoherencias en los punteros internos.
Debate
Algunos desarrolladores tienden a sobreoptimizar sus soluciones,
suponiendo que copiar colecciones es una operación costosa. Esto no es
cierto para las colecciones pequeñas y medianas, y los lenguajes de
programación iteran las colecciones de muchas formas distintas (véase el
Capítulo 16, "Optimización prematura"). Modificarlas durante una
iteración no suele ser seguro, y las consecuencias pueden aparecer lejos
del código que las recorre.
He aquí un ejemplo con consecuencias erráticas:
// Here you add elements to the collection...
Collection<Object> people = new ArrayList<>();
for (Object person : people) {
if (condition(person)) {
[Link](person);
}
}
// You iterate AND remove elements, elements,
// risking skipping other candidates for removal
Este es el aspecto que tiene cuando cambias el código a una solución más
segura copiando la colección:
// Here you add elements to the collection...
Collection<Object> people = new ArrayList<>();
List<Object> iterationPeople = [Link](people);
for (Object person : iterationPeople) {
if (condition(person)) {
[Link](person);
}
}
// You iterate a copy and remove it from the original
[Link](currentIndex -> currentIndex == 5);
// Or use language tools (if available)
Esto es algo que los desarrolladores aprenden pronto, pero sigue
ocurriendo mucho en la industria y el software del mundo real.
Recetas relacionadas
Receta 6.6, "Sustituir iteraciones explícitas"
Receta 12.6, "Sustituir colecciones de empresas"
13.6 Redefinición de Hash e Igualdad
Problema
Implementas el hashing en tus objetos pero no redefines la igualdad.
Solución
Si compruebas el hash, también debes comprobar la igualdad por
coherencia.
Debate
El hash garantiza que dos objetos son diferentes cuando tienen un valor
hash distinto, pero no que son iguales si tienen el mismo valor hash; esta
asimetría puede provocar un fallo de biyección. Siempre debes
comprobar el hash (rápido) y luego comprobar la igualdad (más lento).
Aquí tienes un ejemplo de comparación Person en grandes colecciones:
public class Person {
public String name;
@Override
public boolean equals(Person anotherPerson) {
return [Link]([Link]);
}
@Override
public int hashCode() {
return (int)([Link]()*256);
}
}
Al utilizar un hashmap, puedes equivocarte y suponer que el objeto no
está presente en la colección. Esto es lo que ocurre cuando también
redefines el hash:
public class Person {
public String name;
// Having public attributes is another problem
@Override
public boolean equals(Person anotherPerson) {
return [Link]([Link]);
}
@Override
public int hashCode() {
return [Link]();
}
}
Muchos linters tienen reglas para la redefinición de hash e igualdad
mediante análisis estático y árboles de análisis sintáctico. Con las pruebas
de mutación (véase la Receta 5.1, "Cambiar var a const"), puedes sembrar
diferentes objetos con el mismo hash y comprobar tus pruebas. Toda
mejora del rendimiento tiene sus inconvenientes, y esa es la razón por la
que siempre debes ajustar el rendimiento después de que tu código
funcione y tenga pruebas funcionales automatizadas que lo cubran.
Recetas relacionadas
Receta 14.15, "Cambiar la comparación de igualdad"
Receta 16.7, "Extraer cachés de objetos de dominio"
HASHING
El hash es el proceso de asignar datos de tamaño arbitrario a un valor de
tamaño fijo. El resultado de una función hash se denomina valor hash o código
hash. Puedes utilizar los valores hash como una tabla de índices en colecciones
grandes; funcionan como un atajo para encontrar elementos de una forma más
eficaz que iterando los elementos secuencialmente.
13.7 Refactorizar sin cambios
funcionales
Problema
Desarrollas y refactorizas al mismo tiempo.
Solución
No cambies la funcionalidad y refactorices al mismo tiempo.
Debate
Mezclar refactorizaciones y cambios funcionales dificulta las revisiones
del código y crea posibles conflictos de fusión. A veces detectas que la
refactorización es necesaria para continuar con el desarrollo; si es así,
pon tu solución en espera. Trabaja en la refactorización y continúa con tu
solución después.
He aquí una refactorización sencilla y un cambio de funcionalidad al
mismo tiempo:
getFactorial(n) {
return n * getFactorial(n);
}
// Rename and change
factorial(n) {
return n * factorial(n-1);
}
// This is a very small example
// Things go worse when dealing with more code
Dividiendo las modificaciones, puedes aumentar su claridad:
getFactorial(n) {
return n * getFactorial(n);
}
// Change
getFactorial(n) {
return n * getFactorial(n-1);
}
// Run the tests
factorial(n) {
return n * factorial(n-1);
}
// Rename
Puedes utilizar una ficha física como recordatorio. O estás en la fase de
refactorización o en la fase de desarrollo.
Capítulo 14. Si
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y
comentarios: translation-feedback@[Link]
Haz que el cambio sea fácil (advertencia: esto puede ser difícil), luego haz el
cambio fácil.
Kent Beck en Twitter
14.0 Introducción
Utilizar GoTos era una práctica bien establecida hasta que Edsger
Dijkstra escribió su increíble artículo: "La instrucción GoTo se
considera perjudicial". Hoy en día nadie utiliza la instrucción GoTo
(véase la Receta 18.3, "Sustituir GoTo por Código Estructurado") y pocos
lenguajes de programación siguen admitiéndola, porque crea código
espagueti, que es imposible de mantener y propenso a errores. La
programación estructurada resolvió el problema del código espagueti
hace años.
CÓDIGO ESPAGUETI
El código espagueti es un código mal estructurado, difícil de entender y de
mantener. El nombre "espagueti" se utiliza porque el código suele estar
enredado e interconectado de forma que se asemeja a un plato de fideos de
espagueti enredados. Contiene código redundante o duplicado, así como
numerosas sentencias condicionales, saltos y bucles que pueden ser difíciles de
seguir.
La siguiente evolución será eliminar la mayoría de las sentencias if,
porque los if/casos y los conmutadores son GoTos disfrazados de flujo
estructurado. Tanto los GoTos como los if están presentes en los lenguajes
de programación de máquinas de bajo nivel, como el Ensamblador.
La mayoría de las sentencias if están acopladas con decisiones
accidentales. Este acoplamiento genera un efecto dominó y hace que el
código sea más difícil de mantener, ya que los if accidentales se
consideran tan dañinos como los GoTos porque violan el principio
abierto/cerrado (véase la Receta 14.3, "Reificar las variables
booleanas"). Tus diseños serán menos extensibles. Las sentencias If abren
la puerta a problemas aún más graves,
como switches, cases, defaults, return, continue, y breaks. Hacen que tus
algoritmos sean más oscuros y te obligan a construir
soluciones accidentalmente complejas.
PROGRAMACIÓN ESTRUCTURADA
La programación estructurada hace hincapié en el uso de construcciones de
flujo de control, como bucles y funciones, para mejorar la claridad, el
mantenimiento, la legibilidad y la fiabilidad de los programas informáticos.
Descompones un programa en partes más pequeñas y manejables, y luego
organizas esas partes utilizando construcciones de flujo de control estructurado.
14.1 Sustituir los if accidentales por el
polimorfismo
Problema
Tienes ifs accidentales dentro de tu código.
Solución
Sustitúyelos por objetos polimórficos.
Debate
Aquí tienes una declaración if esencial:
class MovieWatcher {
constructor(age) {
[Link] = age;
}
watchXRatedMovie() {
if ([Link] < 18)
throw new Error("You are not allowed to watch this movie");
else
[Link]();
}
watchMovie() {
// ..
}
}
const jane = new MovieWatcher(12);
[Link]();
// Throws exception since Jane is too young to watch the movie
Tienes que decidir si eliminas esta sentencia if o no, y tienes que entender
si representa una regla de negocio(esencial) o un artefacto de
implementación(accidental). Las personas ajenas al mundo del software,
en el mundo real, describen las restricciones de edad en lenguaje natural
utilizando ifs. Por tanto, tienes que respetar la biyección, identificarla
como esencial y NO sustituirla.
Este ejemplo tiene un si accidental:
class Movie {
constructor(rate) {
[Link] = rate;
}
}
class MovieWatcher {
constructor(age) {
[Link] = age;
}
watchMovie(movie) {
if (([Link] < 18) && ([Link] === 'Adults Only'))
throw new Error("You are not allowed to watch this movie");
// if the exception is not raised you can watch the movie
playMovie();
}
}
const jane = new MovieWatcher(12);
const theExorcist = new Movie('Adults Only');
[Link](theExorcist);
// Jane cannot watch the exorcist since she is 12
El if de clasificación de películas no está relacionado con un if del mundo
real, sino con una implementación accidental (y acoplada). El problema es
la decisión de diseño de modelar las calificaciones con cadenas (véase el
Capítulo 3, "Modelos anémicos"). Se trata de una solución clásica ni
abierta a la extensión ni cerrada a la modificación.
El problema se agrava con los nuevos requisitos:
class Movie {
constructor(rate) {
[Link] = rate;
}
}
class MovieWatcher {
constructor(age) {
[Link] = age;
}
watchMovie(movie) {
// !!!!!!!!!!!!!!!!! IFS ARE POLLUTING
HERE !!!!!!!!!!!!!!!!!!!!!!!!!!
if (([Link] < 18) && ([Link] === 'Adults Only'))
throw new Error("You are not allowed to watch this movie");
else if (([Link] < 13) && ([Link] === 'PG 13'))
throw new Error("You are not allowed to watch this movie");
// !!!!!!!!!!!!!!!! IFS ARE POLLUTING
HERE !!!!!!!!!!!!!!!!!!!!!!!!!!!
playMovie();
}
}
const theExorcist = new Movie('Adults Only');
const gremlins = new Movie('PG 13');
const jane = new MovieWatcher(12);
[Link](theExorcist);
// Jane cannot watch the exorcist since she is 12
[Link](gremlins);
// Jane cannot watch Gremlins since she is 12
const joe = new MovieWatcher(16);
[Link](theExorcist);
// Joe cannot watch The Exorcist since he is 16
[Link](gremlins);
// Joe CAN watch Gremlins since he is 16
El código está contaminado con ifs, ya que las nuevas valoraciones
traerán nuevos ifs, y también falta una declaración por defecto. Las
cadenas utilizadas para representar las valoraciones no son objetos de
primera clase. Una errata introducirá errores difíciles de encontrar y te
verás obligado a añadir getters en Movie para tomar decisiones.
Este ejemplo crea una jerarquía polimórfica para cada condición if (si no
existe ya), traslada cada cuerpo if a la abstracción anterior y sustituye las
llamadas if por una única llamada a un método polimórfico:
// 1. Create a polymorphic hierarchy for every if condition
// (if it doesn't already exist)
class MovieRate {
// If language permits this should be declared abstract
}
class PG13MovieRate extends MovieRate {
//2. Move every *if body* to the former abstraction
warnIfNotAllowed(age) {
if (age < 13)
throw new Error("You are not allowed to watch this movie");
}
}
class AdultsOnlyMovieRate extends MovieRate {
//2. Move every *if body* to the former abstraction
warnIfNotAllowed(age) {
if (age < 18)
throw new Error("You are not allowed to watch this movie");
}
}
class Movie {
constructor(rate) {
[Link] = rate;
}
}
class MovieWatcher {
constructor(age) {
[Link] = age;
}
watchMovie(movie) {
// 3. Replace if calls by a polymorphic method call
[Link]([Link]);
// watch movie
}
}
const theExorcist = new Movie(new AdultsOnlyMovieRate());
const gremlins = new Movie(new PG13MovieRate());
const jane = new MovieWatcher(12);
// [Link](theExorcist);
// Jane cannot watch the exorcist since she is 12
// [Link](gremlins);
// Jane cannot watch gremlins since she is 12
const joe = new MovieWatcher(16);
// [Link](theExorcist);
// Joe cannot watch the exorcist since he is 16
[Link](gremlins);
// Joe CAN watch gremlins since he is 16
Esta solución es mejor, puesto que el código ya no está contaminado con
ifs, y bastará con ampliar el modelo cuando obtengas nuevos requisitos y
aprendas del dominio. Si necesitas crear nuevas clasificaciones, lo
abordarás con nuevas instancias polimórficas. Además, no es necesario
un comportamiento por defecto, ya que las excepciones rompen el flujo.
Muchas veces bastará con un objeto nulo (ver Receta 15.1, "Crear objetos
nulos"). Las calificaciones son ahora objetos de primera clase; no tienes la
posibilidad de problemas de erratas del ejemplo anterior. Los if esenciales
siguen ahí (comprobación de una edad) y los accidentales han
desaparecido (límites de puntuación).
Para abordar el problema de la cadena de colaboradores, puedes romper
el código:
[Link]([Link]);
class Movie {
constructor(rate) {
this._rate = rate; // Rate is now private
}
warnIfNotAllowed(age) {
this._rate.warnIfNotAllowed(age);
}
}
class MovieWatcher {
constructor(age) {
[Link] = age;
}
watchMovie(movie) {
[Link]([Link]);
// watch movie
}
}
La calificación es privada, por lo que no rompes la encapsulación y no
necesitas getters. Esto es lo que parece si aplicas la receta a los if
esenciales:
class Age {
}
class AgeLessThan13 extends Age {
assertCanWatchPG13Movie() {
throw new Error("You are not allowed to watch this movie");
}
assertCanWatchAdultMovie() {
throw new Error("You are not allowed to watch this movie");
}
}
class AgeBetween13And18 extends Age {
assertCanWatchPG13Movie() {
// No problem
}
assertCanWatchAdultMovie() {
throw new Error("You are not allowed to watch this movie");
}
}
class MovieRate {
// If language permits this should be declared abstract
// abstract assertCanWatch();
}
class PG13MovieRate extends MovieRate {
// Move every *if body* to the former abstraction
assertCanWatch(age) {
age.assertCanWatchPG13Movie()
}
}
class AdultsOnlyMovieRate extends MovieRate {
// Move every *if body* to the former abstraction
assertCanWatch(age) {
[Link]()
}
}
class Movie {
constructor(rate) {
this._rate = rate; // Rate is now private
}
watchByMe(moviegoer) {
this._rate.assertCanWatch([Link]);
}
}
class MovieWatcher {
constructor(age) {
[Link] = age;
}
watchMovie(movie) {
[Link](this);
}
}
const theExorcist = new Movie(new AdultsOnlyMovieRate());
const gremlins = new Movie(new PG13MovieRate());
const jane = new MovieWatcher(new AgeLessThan13());
// [Link](theExorcist);
// Jane cannot watch the exorcist since she is 12
// [Link](gremlins);
// Jane cannot watch gremlins since she is 12
const joe = new MovieWatcher(new AgeBetween13And18());
// [Link](theExorcist);
// Joe cannot watch the exorcist since he is 16
[Link](gremlins);
// Joe CAN watch gremlins since he is 16
Este código funciona, pero tiene síntomas de sobrediseño, ya que las
clases que representan las edades no están relacionadas con conceptos
reales en tu modelo, violando así el principio de biyección. Además, el
modelo es demasiado complejo porque necesitarás nuevas clases
relacionadas con nuevos grupos de edad y los grupos de edad podrían no
ser disjuntos.
Para evitar el último diseño y establecer un límite claro entre los
"si" esenciales y los accidentales, puedes utilizar la siguiente regla:
CONSEJO
Una buena regla de diseño es crear abstracciones si los elementos pertenecen al
mismo dominio (películas y clasificaciones) y no crearlas si cruzan dominios
(películas y edades).
Esta receta te recomienda que evites la mayoría de las frases "si". Esto
puede resultarte difícil, sobre todo al principio, ya que el uso de
condicionales está muy arraigado en la profesión y puede que te sientas
muy cómodo con esta práctica. Sin embargo, es posible eliminar todos los
"si" accidentales. Esto hará que tus modelos estén menos acoplados y sean
más extensibles. El patrón de objetos nulos es un caso especial de esta
técnica. Podrás eliminar todos los nulos, ya que los if nulos
son siempre accidentales (véase el Capítulo 15, "Nulos").
JERARQUÍA POLIMÓRFICA
En una jerarquía polimórfica, las clases se organizan en una estructura
jerárquica basada en sus relaciones "se comporta-como". Esto permite crear
clases especializadas que heredan comportamientos de clases más generales. En
una jerarquía polimórfica, una clase abstracta base sirve de base y define un
comportamiento común compartido por múltiples subclases concretas. Las
subclases heredan estas características de la superclase y pueden añadir su
propio comportamiento. La subclasificación es una forma de imponer el
polimorfismo (consulta la Receta 14.14, "Convertir funciones no polimórficas en
polimórficas"). Pero es rígida, ya que no puedes cambiar una superclase después
del tiempo de compilación.
14.2 Renombrar Variables de Indicador
para Eventos
Problema
Tienes argumentos de bandera (booleanos) con nombres imprecisos en
tus funciones.
Solución
Cambia el nombre de las variables de bandera para mostrar lo que ha
ocurrido.
Debate
Las banderas indican lo que ha ocurrido. A veces su nombre es demasiado
genérico. Aquí tienes un ejemplo de bandera que indica que ha ocurrido
algo:
function dummy() {
$flag = true;
while ($flag == true) {
$result = checkSomething();
if ($result) {
$flag = false;
}
}
}
Esta solución es más declarativa y reveladora de intenciones:
function dummy()
{
$atLeastOneElementWasFound = false;
while (!$atLeastOneElementWasFound) {
$elementSatisfies = checkSomething();
if ($elementSatisfies) {
$atLeastOneElementWasFound = true;
}
}
}
Debes buscar en todo el código banderas mal nombradas. Los indicadores
están muy extendidos en el código de producción. Debes restringir su uso
e imponer nombres claros y que revelen tu intención.
Recetas relacionadas
Receta 6.4, "Eliminar dobles negativos"
Receta 14.11, "Evitar que se devuelvan valores booleanos en
las comprobaciones de condiciones"
Receta 14.3, "Reificar variables booleanas"
14.3 Reificar variables booleanas
Problema
Tienes código que utiliza variables booleanas como banderas, exponiendo
una implementación accidental y contaminando el código con
condiciones if.
BANDERAS BOOLEANAS
Una bandera booleana es una variable que sólo puede ser verdadera o falsa,
representando los dos estados posibles de una condición binaria. Los
indicadores booleanos se utilizan habitualmente para controlar el flujo de la
lógica mediante sentencias condicionales, bucles y otras estructuras de control.
Solución
No utilices variables booleanas, ya que te obligan a escribir ifs (véase la
Receta 14.1, "Sustituir los ifs accidentales por el polimorfismo"). En su
lugar, crea estados polimórficos.
Debate
Las variables booleanas rompen la extensibilidad y el principio abierto-
cerrado de SOLID (consulta la Receta 19.1, "Romper la herencia
profunda"). Son difíciles de comparar en algunos lenguajes que
convierten todos los valores en verdaderos y falsos (consulta la Receta
24.2, "Tratar con valores verdaderos"). Si el booleano mapea a una
entidad booleana del mundo real, debes crearlo siguiendo
el MAPPER definido en el Capítulo 2. Si no, puedes modelarlo utilizando
elpatrón de diseño de estado para favorecer la extensibilidad.
PRINCIPIO ABIERTO-CERRADO
Elprincipio abierto-cerrado es la "O" de SOLID (véase la Receta 19.1, "Romper la
herencia profunda"). Establece que las clases de software deben estar abiertas a
la extensión, pero cerradas a la modificación. Debes poder ampliar el
comportamiento sin modificar el código. Este principio fomenta el uso de
interfaces abstractas, herencia y polimorfismo para permitir que se añada nueva
funcionalidad sin cambiar el código existente. El principio también promueve la
separación de preocupaciones (véase la Receta 8.3, "Eliminar comentarios
lógicos"), facilitando el desarrollo, las pruebas y la
implementación de componentes de software de forma independiente.
Aquí tienes un ejemplo con tres banderas booleanas:
function processBatch(
bool $useLogin,
bool $deleteEntries,
bool $beforeToday) {
// ...
}
Puedes reificarlo en conceptos del mundo real de la siguiente manera:
function processBatch(
LoginStrategy $login,
DeletionPolicy $deletionPolicy,
Date $cutoffDate) {
// ...
}
La detección automática puede advertirte del uso de booleanos, pero
puede dar falsos positivos, y algunos lenguajes tienen problemas con los
comparadores de booleanos. En lenguajes con valores verdaderos y falsos,
como JavaScript (véase la Receta 24.2, "Tratar con valores verdaderos"),
los booleanos son una fuente común de errores, y debes tener especial
cuidado al declarar algo como booleano. Los indicadores son difíciles de
mantener y ampliar. Aprende más sobre el dominio y utiliza el
polimorfismo en lugar de ifs/interruptor/casos (consulta la Receta 14.1,
"Sustituir los ifs accidentales por el polimorfismo").
PATRÓN DE DISEÑO DE ESTADO
El patrón de diseño de estados permite que un objeto cambie su
comportamiento cuando cambia su estado interno en tiempo de ejecución, sin
cambiar su clase. Define un conjunto de objetos de estado válidos que
encapsulen el comportamiento de cada estado dentro de una clase
independiente. Los objetos de estado exponen una interfaz común que permite
al objeto contexto delegar su comportamiento en el objeto de estado
apropiado. Cuando cambia el estado del objeto de contexto, simplemente cambia
al objeto de estado apropiado. Esto promueve un acoplamiento flexible entre el
objeto de contexto y sus objetos de estado y permite que el objeto de contexto sea
más flexible y extensible, favoreciendo el principio abierto-cerrado.
Recetas relacionadas
Receta 6.4, "Eliminar dobles negativos"
Receta 14.2, "Cambiar el nombre de las variables de indicador para
eventos"
Ver también
"FlagArgument" de Martin Fowler
14.4 Sustitución de las sentencias
Switch/Case/Elseif
Problema
Tienes estructuras de control con interruptores y cajas.
Solución
Sustitúyelos por objetos polimórficos.
Debate
Los conmutadores combinan demasiadas decisiones y rompen el
principio abierto-cerrado (ver Receta 14.3, "Reificar variables booleanas"),
ya que cada nueva condición cambiará el algoritmo principal,
favoreciendo los conflictos de fusión. También provocan código duplicado
y métodos muy grandes. Debes crear jerarquías/componer objetos
siguiendo el principio abierto-cerrado utilizando el patrón de estado para
modelar las transiciones y el patrón de estrategia/objeto método para
elegir las ramas.
El siguiente ejemplo convierte varios formatos de audio diferentes a MP3:
class Mp3Converter {
convertToMp3(source, mimeType) {
if([Link]("audio/mpeg")) {
this.convertMpegToMp3(source)
} else if([Link]("audio/wav")) {
this.convertWavToMp3(source)
} else if([Link]("audio/ogg")) {
this.convertOggToMp3(source)
} else if(...) {
// Lots of new else clauses
}
Esto equivale al mismo código problemático:
class Mp3Converter {
convertToMp3(source, mimeType) {
switch (mimeType) {
case "audio/mpeg":
this.convertMpegToMp3(source);
break;
case "audio/wav":
this.convertWavToMp3(source);
break;
case "audio/ogg":
this.convertOggToMp3(source);
break;
default:
throw new Error("Unsupported MIME type: " + mimeType);
}
}
Tener convertidores especializados no es un problema, ya que puedes
añadir uno nuevo sin cambiar el código:
class Mp3Converter {
convertToMp3(source, mimeType) {
const foundConverter = [Link].
find(converter => [Link](mimeType));
// Do not use metaprogramming to find and iterate converters
// since this is another problem.
if (!foundConverter) {
throw new Error('No converter found for ' + mimeType);
}
foundConverter.convertToMp3(source);
}
}
Puesto que hay casos válidos para el uso de if/else, no deberías tirar de la
manta y prohibir estas instrucciones. En su lugar, puedes poner una
proporción de sentencias if/otras sentencias como advertencia.
Recetas relacionadas
Receta 10.7, "Extraer un método a un objeto"
Receta 13.4, "Eliminar los valores predeterminados de los interruptores"
Receta 14.5, "Sustituir las condiciones if codificadas con colecciones"
Receta 14.10, "Reescribir código de flechas anidado"
Receta 15.1, "Crear objetos nulos"
PATRÓN DE DISEÑO ESTRATÉGICO
El patrón de diseño de estrategias define una familia de algoritmos
intercambiables, encapsula cada uno de ellos y los hace intercambiables en
tiempo de ejecución. El patrón permite que un objeto cliente elija entre una
gama de algoritmos a utilizar, en función del contexto o situación específicos en
tiempo de ejecución. También promueve el acoplamiento flexible entre el objeto
cliente y las estrategias, y te facilita ampliar o modificar el comportamiento del
objeto cliente sin afectar a su implementación.
14.5 Sustituir las condiciones if
codificadas con colecciones
Problema
Has codificado las condiciones "si".
Solución
Crea una colección para mapear tus condiciones.
Debate
Las condiciones if codificadas rompen la comprobabilidad. Puedes
sustituir todos los if por una condición dinámica o un polimorfismo. Aquí
tienes un ejemplo de asignación de dominios de Internet a nombres de
países:
private string FindCountryName (string internetCode)
{
if (internetCode == "de")
return "Germany";
else if(internetCode == "fr")
return "France";
else if(internetCode == "ar")
return "Argentina";
// lots of else clauses
else
return "Suffix not Valid";
}
Puedes crear dos colecciones para mapearlas o una sola:
private string[] country_names = {"Germany", "France", "Argentina"};
// lots more
private string[] Internet_code_suffixes= {"de", "fr", "ar" }; // more
// You also can do inline initialization here
private Dictionary<string, string> Internet_codes =
new Dictionary<string, string>();
// There are more efficient ways for collection iteration
// This pseudocode is only for illustration
int currentIndex = 0;
foreach (var suffix in Internet_code_suffixes) {
Internet_codes.Add(suffix, Internet_codes[currentIndex]);
currentIndex++;
}
private string FindCountryName(string internetCode) {
return Internet_codes[internetCode];
}
En el pasado, el hardcoding no era una opción. Con las metodologías
modernas, aprendes codificando, y luego generalizas y refactorizas tus
soluciones.
Recetas relacionadas
Receta 14.4, "Sustitución de las sentencias Switch/Case/Elseif"
Receta 14.10, "Reescribir código de flechas anidado"
Receta 14.16, "Reificar las condiciones comerciales codificadas"
14.6 Cambiar las condiciones booleanas
a cortocircuito
Problema
Tienes una evaluación booleana completa, pero conoces el resultado antes
de terminarla.
Solución
Sé perezoso al evaluar condiciones booleanas. Utiliza un cortocircuito si
tu lenguaje lo permite.
Debate
Las tablas de verdad booleanas son estupendas para las matemáticas,
pero debes tener cuidado, ya que la evaluación en cortocircuito a veces
tiene efectos secundarios y problemas de rendimiento. La evaluación en
cortocircuito te ayuda a evitar construir evaluaciones completas no
válidas, como la evaluación completa incluso cuando el valor ya está
definido (por ejemplo, utilizando una condición OR en la que la primera
parte es verdadera).
Aquí tienes una evaluación completa no válida de utilizando el AND
lógico (&):
if (isOpen(file) & size(contents(file)) > 0)
// It performs a full evaluation since it is the bitwise AND
// will fail since you cannot retrieve contents
// from a file that is not open
Esta versión utiliza la evaluación por cortocircuito:
if (isOpen(file) && size(contents(file)) > 0)
// Short-circuit evaluation
// If the file is not open it will not try to get the contents
Como excepción, no debes utilizar un cortocircuito como alternativa if. Si
los operandos tienen efectos secundarios -ya que la mayoría de los
lenguajes de programación admiten los cortocircuitos y muchos de ellos
lo tienen como única opción- debes favorecer este tipo de expresión.
Recetas relacionadas
Receta 14.9, "Cómo evitar los cortocircuitos"
Receta 14.12, "Modificar la comparación con booleanos"
Receta 24.2, "Tratar con valores de verdad"
14.7 Añadir un Else implícito
Problema
Tienes una sentencia if sin else.
Solución
Sé explícito, incluso con la guarda else, y ponla junto a tu condición if.
Debate
El código con guardias else explícitas es más legible y tiene una menor
carga cognitiva. También puede ayudarte a detectar condiciones
imprevistas y a favorecer el principio de "fail fast" (ver Capítulo 13, "Fail
Fast"). Si realizas un retorno anticipado en una sentencia if, puedes omitir
la parte else y luego eliminar el if y utilizar el polimorfismo. Es entonces
cuando te pierdes los casos reales.
Aquí tienes una función con un else implícito:
function carBrandImplicit(model) {
if (model === 'A4') {
return 'Audi';
}
return 'Mercedes-Benz';
}
Esto es lo que parece cuando lo haces explícito:
function carBrandExplicit(model) {
if (model === 'A4') {
return 'Audi';
}
if (model === 'AMG') {
return 'Mercedes-Benz';
}
// Fail Fast
throw new Exception('Model not found');
}
También puedes reescribirlos y realizar pruebas de mutación (véase la
Receta 5.1, "Cambiar var por const"). Esta receta suscita mucho debate
público y fuertes opiniones. Escucha todas las opiniones y luego evalúa
todos los pros y los contras.
Recetas relacionadas
Receta 14.4, "Sustitución de las sentencias Switch/Case/Elseif"
Receta 14.10, "Reescribir código de flechas anidado"
14.8 Reescribir código de flecha
condicional
Problema
Tienes condiciones booleanas anidadas parecidas a una escalera o flecha .
Solución
Evita comprobar expresiones booleanas y devolver un booleano explícito.
Sustitúyelo por un valor de fórmula.
Debate
El código de escalera, o código de flecha, es difícil de leer. El ámbito
también es difícil de igualar desde el principio hasta el final. Este tipo de
código también puede clasificarse como código ninja, que es más común
en los lenguajes de bajo nivel. Cuando se trata de fórmulas booleanas, es
más legible mostrar una fórmula booleana comercial que una escalera de
comprobaciones booleanas seguidas de la devolución de un
verdadero/falso explícito.
Aquí tienes un ejemplo de código de flecha:
def is_platypus(self):
if self.is_mammal():
if self.has_fur():
if self.has_beak():
if self.has_tail():
if self.can_swim():
return True
return False
# This is also wrong since it is polluted
# with IFs and not readable by a biologist
def is_platypus(self):
if not self.is_mammal():
return False
if not self.has_fur():
return False
if not self.has_beak():
return False
if not self.has_tail():
return False
if not self.can_swim():
return False
return True
Así es como queda cuando reescribes la condición:
def is_platypus(self):
return self.is_mammal() &&
self.has_fur() &&
self.has_beak() &&
self.has_tail() &&
self.can_swim()
# You can even group conditions according to animal taxonomies
Basándote en los árboles sintácticos, puedes refactorizar con seguridad el
código eliminando el valor booleano explícito. Cuidado con devolver
booleanos. Tras la devolución, necesitarás una sentencia if que podrías
eliminar utilizando la receta correspondiente.
CÓDIGO NINJA
Elcódigo ninja (también conocido como código inteligente o código listo) se
refiere al código que está escrito de forma inteligente, pero que es difícil de
entender o mantener. A menudo lo crean programadores experimentados que
disfrutan utilizando técnicas avanzadas de programación o características
específicas del lenguaje para escribir código más eficiente y optimizado antes de
tiempo. Aunque el código ninja puede ser impresionante y ejecutarse más rápido
que otro código, puede ser difícil de leer y comprender, lo que conlleva
problemas de mantenimiento, escalabilidad y desarrollo futuro. El código ninja
es lo contrario del código limpio.
Recetas relacionadas
Receta 14.2, "Cambiar el nombre de las variables de indicador para
eventos"
Receta 14.10, "Reescribir código de flechas anidado"
Receta 14.11, "Evitar que se devuelvan valores booleanos en
las comprobaciones de condiciones"
Receta 14.12, "Modificar la comparación con booleanos"
Receta 22.4, "Reescribir Try/Catches anidados"
Receta 22.6, "Reescribir código de flecha de excepción"
Receta 24.2, "Tratar con valores de verdad"
14.9 Evitar los Ataques de Cortocircuito
Problema
Utilizas la evaluación booleana como un atajo de legibilidad para una
segunda condición que sólo es válida si se ha superado la primera.
Solución
No utilices la comparación booleana para las funciones de efecto
secundario. Reescríbelo como un if.
Debate
A los programadores astutos les gusta escribir código "hacky" y oscuro,
incluso cuando no hay pruebas sólidas que apoyen esta "mejora". Escribir
condiciones dependientes como booleanas es un signo de optimización
prematura (ver Capítulo 16) y perjudica la legibilidad.
El siguiente ejemplo combina una condición y una consecuencia de la
primera condición:
userIsValid() && logUserIn();
// This expression is short-circuited
// Does not value second statement
// unless the first one is true
functionDefinedOrNot && functionDefinedOrNot();
// In some languages undefined works as a false
// If functionDefinedOrNot is not defined, this does
// not raise an error or run
Puedes cambiar ambos casos utilizando más if declarativos:
if (userIsValid()) {
logUserIn();
}
if(typeof functionDefinedOrNot == 'function') {
functionDefinedOrNot();
}
// Checking for a typeOf is not a good solution
Recetas relacionadas
Receta 10.4, "Eliminar la astucia del código"
Receta 14.6, "Cambiar las condiciones booleanas a cortocircuito"
Receta 15.2, "Eliminar el encadenamiento opcional"
14.10 Reescribir código de flechas
anidadas
Problema
Tienes ifs y elses anidados que son muy difíciles de leer y probar.
Solución
Evita los if anidados e intenta evitar todos los if accidentales.
Debate
En el código procedimental, es muy común ver complejos if anidados. Esta
solución está más relacionada con el scripting que con la programación
orientada a objetos. Aquí tienes un ejemplo de código de flecha o escalera:
if (actualIndex < totalItems)
{
if (product[actualIndex].[Link]("arrow"))
{
do
{
if (product[actualIndex].price == null)
{
// handle no price
}
else
{
if (!(product[actualIndex].priceIsCurrent()))
{
// add price
}
else
{
if (hasDiscount)
{
// handle discount
}
else
{
// etc
}
}
}
actualIndex++;
}
while (actualIndex < totalCount && totalPrice <
[Link]);
}
else
actualIndex++;
}
return actualIndex;
}
Puedes refactorizarlo de la siguiente manera
foreach (products as currentProduct) {
addPriceIfDefined(currentProduct)
}
addPriceIfDefined()
{
// You only add the price if it follows the above rules
}
Como muchos linters pueden analizar árboles sintácticos, puedes
comprobar en tiempo de compilación los niveles de anidamiento.
Recetas relacionadas
Receta 6.13, "Evitar el infierno de las devoluciones de llamada"
Receta 11.1, "Romper métodos demasiado largos"
Receta 14.4, "Sustitución de las sentencias Switch/Case/Elseif"
Receta 14.8, "Reescribir código de flecha condicional"
Receta 14.11, "Evitar que se devuelvan valores booleanos en
las comprobaciones de condiciones"
Receta 14.18, "Reescribir ternarios anidados"
Receta 22.6, "Reescribir código de flecha de excepción"
Ver también
"Aplanar el código de las flechas" en el blog Coding Horror
14.11 Evitar que se devuelvan valores
booleanos en las comprobaciones de
condición
Problema
Tienes código comprobación (y reparto de probabilidades) para una
condición booleana.
Solución
No devuelvas booleanos explícitos. La mayoría de los usos de los
booleanos no se corresponden con los booleanos del mundo real en
el MAPPER. Reescríbelo como una condición comercial.
Debate
La mayoría de los retornos booleanos están acoplados a soluciones
accidentales y no corresponden realmente a un booleano del mundo real.
Necesitas escribir código de alto nivel y soluciones que favorezcan la regla
de bijección y muchos booleanos no lo hacen. También puedes devolver
una proposición booleana en lugar de comprobar una negación y crear
respuestas con una fórmula de lógica empresarial, no con un algoritmo.
Cuando se trata de fórmulas booleanas, es más legible mostrar una
fórmula booleana empresarial que introducir una cláusula if negada. Al
tratar con abstracciones de bajo nivel, es habitual encontrar retornos
booleanos. Cuando creas software complejo y maduro, deberías empezar
a olvidarte de esta obsesión primitiva y preocuparte más por las reglas e
identidades del mundo real.
He aquí un ejemplo conocido y no tan inocente:
function canWeMoveOn() {
if ([Link]())
return false;
else
return true;
}
Puedes vincular tu regla de negocio directamente para mapear la
condición de negocio del mundo real de la siguiente manera:
function canWeMoveOn() {
return ![Link]();
}
Este retorno es sintácticamente idéntico, pero más legible. No obstante,
ten cuidado al devolver booleanos; después del retorno, necesitarás una
sentencia if. Para ello, puedes aplicar la Receta 14.4, "Sustitución de las
sentencias Switch/Case/Elseif".
En este breve ejemplo, puedes identificar si un número es par o impar:
boolean isEven(int num) {
if(num % 2 == 0) {
return true;
} else {
return false;
}
}
Al devolver la implementación exacta, tu código es más limpio:
boolean isEven(int numberToCheck) {
// You decouple the what (to check for even or odd)
// with how (the algorithm)
return (numberToCheck % 2 == 0);
}
Puedes buscar en las bibliotecas de código declaraciones "devolver
verdadero" e intentar sustituirlas cuando sea posible.
Recetas relacionadas
Receta 6.4, "Eliminar dobles negativos"
Receta 14.2, "Cambiar el nombre de las variables de indicador para
eventos"
Receta 14.4, "Sustitución de las sentencias Switch/Case/Elseif"
Receta 14.10, "Reescribir código de flechas anidado"
Receta 14.11, "Evitar que se devuelvan valores booleanos en
las comprobaciones de condiciones"
Receta 14.12, "Modificar la comparación con booleanos"
Receta 14.17, "Eliminar booleanos gratuitos"
Receta 24.2, "Tratar con valores de verdad"
14.12 Modificar la comparación con
booleanos
Problema
Comparas con booleanos y realizas lanzamientos mágicos, y luego
obtienes resultados inesperados.
Solución
No compares con un valor verdadero. O eres verdadero o falso, o no
debes comparar.
Debate
Comparar contra una constante booleana oculta los lanzamientos mágicos
en algunos lenguajes que tratan con valores veraces y falsos (ver Receta
24.2, "Tratar con valores veraces"). No siguen el principio de menor
sorpresa (ver Receta 5.6, "Congelar constantes mutables") ni el principio
de fallo rápido (ver Capítulo 13). Nunca debes mezclar booleanos con
objetos booleanos castables, ya que muchos lenguajes castean valores a
dominios que cruzan booleanos.
Aquí tienes un ejemplo en un script bash:
#!/bin/bash
if [ false ]; then
echo "True"
else
echo "False"
fi
# this evaluates to true since
# "false" is a non-empty string
if [ false ] = true; then
echo "True"
else
echo "False"
fi
# this also evaluates to true
Para evitar este problema, puedes utilizar la constante explícita false:
#!/bin/bash
if false ; then
echo "True"
else
echo "False"
fi
# this evaluates to false
Es práctica común utilizar muchos valores no booleanos como booleanos,
pero debes ser muy estricto cuando utilices booleanos. Muchos lenguajes
tienen problemas con los valores de verdad. Incluso los lenguajes de
scripting.
Recetas relacionadas
Receta 24.2, "Tratar con valores de verdad"
14.13 Extraer de ternarios largos
Problema
Tienes un código muy largo en condiciones ternarias.
Solución
No utilices ternarios para ejecutar código. Debes leerlos como una
fórmula matemática.
Debate
Las condiciones ternarias largas son difíciles de leer y rompen la
reutilización y la comprobabilidad del código. Puedes aplicar la Receta
10.7, "Extraer un método a un objeto". Cuando se utiliza una condición
ternaria en código que contiene varias funciones, puede resultar difícil
determinar a qué función afecta la condición. Esto puede dificultar la
identificación y corrección de defectos, así como la comprensión del
funcionamiento general del código.
Mira el siguiente ejemplo:
const invoice = isCreditCard ?
prepareInvoice();
fillItems();
validateCreditCard();
addCreditCardTax();
fillCustomerDataWithCreditCard();
createCreditCardInvoice()
:
prepareInvoice();
fillItems();
addCashDiscount();
createCashInvoice();
// The intermediate results are not considered
// The value of the invoice is the result of
// the last execution
Después de aplicar la receta del método del extracto, puedes leerla como
una fórmula matemática:
const invoice = isCreditCard ?
createCreditCardInvoice() :
createCashInvoice();
Esto es más compacto:
if (isCreditCard) {
const invoice = createCreditCardInvoice();
} else {
const invoice = createCashInvoice();
}
Mejor aún con el polimorfismo :
const invoice = [Link]();
Los linters pueden detectar grandes bloques de código. No importa dónde
tengas largas líneas de código, siempre puedes refactorizar en métodos
funcionales de nivel superior y más cortos.
Recetas relacionadas
Receta 10.7, "Extraer un método a un objeto"
Receta 11.1, "Romper métodos demasiado largos"
14.14 Convertir funciones no
polimórficas en polimórficas
Problema
En tienes métodos que realizan la misma acción pero que no son
intercambiables.
Solución
Forzar el polimorfismo para la extensibilidad.
Debate
POLIMORFISMO
Dos objetos son polimórficos respecto a un conjunto de métodos si tienen la
misma firma y realizan la misma acción (quizá con una implementación
diferente).
Puedes conseguir un polimorfismo parcial si dos objetos responden a la
misma invocación de método de la misma forma semántica con
implementaciones posiblemente diferentes. Con el polimorfismo,
favoreces la extensibilidad, reduces el acoplamiento y evitas montones de
ifs.
En este ejemplo los nombres no son polimórficos aunque consigan el
mismo comportamiento:
class Array {
public function arraySort() {
}
}
class List {
public function listSort() {
}
}
class Stack {
public function stackSort() {
}
}
Tras renombrar las funciones, puedes conseguir el polimorfismo:
interface Sortable {
public function sort();
}
class Array implements Sortable {
public function sort() {
// Implementation of the sort() method for Array
}
}
class List implements Sortable {
public function sort() {
// Implementation of the sort() method for List
}
}
class Stack implements Sortable {
public function sort() {
// Implementation of the sort() method for Stack
}
}
Se trata de un error semántico. Podrías añadir una advertencia para
nombres de métodos similares en clases polimórficas. Nombrar es muy
importante (consulta el Capítulo 7, "Nombrar"), y debes hacerlo basándote
en conceptos y no en tipos accidentales.
Recetas relacionadas
Receta 14.4, "Sustitución de las sentencias Switch/Case/Elseif"
14.15 Cambiar la comparación
igualitaria
Problema
Tienes un código que compara la igualdad de los atributos.
Solución
No exportes y compares; oculta la comparación en un único método.
Debate
La comparación de atributos se utiliza mucho en el código. Debes
centrarte en el comportamiento y las responsabilidades. Es
responsabilidad de un objeto compararse con otros objetos. Los
optimizadores prematuros (ver Capítulo 16) te dirán que esto es menos
performante, pero debes pedirles pruebas reales y contrastar la solución
más mantenible sin romper la encapsulación ni crear código duplicado.
Aquí puedes ver un ejemplo de una gran base de código en la que una
regla de negocio (compara mayúsculas y minúsculas o insensible) está
duplicada en muchos lugares:
if ([Link] == 'Broad Street') { }
if ([Link] == 'Bourbon St') { }
// 24601 usages in a big system
// Comparisons are case sensitive
Al delegar la responsabilidad de la comparación en un único lugar, evitas
toda duplicación de código y permites el cambio de regla en un único
punto:
if ([Link]('Broad Street') { }
if ([Link]('Bourbon St') { }
// 24601 usages in a big system
function isAtStreet(street) {
// You can change comparisons to
// case sensitive in just one place.
}
Puedes detectar la comparación de atributos mediante árboles sintácticos.
Puede haber buenos usos para los tipos primitivos, como con muchas
otras recetas. Necesitas poner las responsabilidades en un único lugar,
incluida la comparación. Si cambian algunas de tus reglas de negocio,
necesitas cambiar un único punto.
Recetas relacionadas
Receta 4.2, "Reificar datos primitivos"
Receta 13.6, "Redefinir Hash e Igualdad"
Receta 14.12, "Modificar la comparación con booleanos"
Receta 17.8, "Evitar la envidia de funciones"
14.16 Reificar las condiciones
empresariales codificadas
Problema
Tu código tiene condiciones codificadas.
Solución
Traslada las reglas empresariales duras a la configuración.
Debate
Cuando aprendiste a programar, probablemente llegaste a la conclusión
de que hardcoding es siempre una mala idea. Si decides utilizar el
desarrollo dirigido por pruebas (TDD) (véase la Receta 4.8, "Eliminar
propiedades innecesarias"), esto siempre es así porque la metodología
favorece las soluciones rápidas y sencillas antes que las especulativas,
pero TDD también te aconseja eliminar rápidamente estas
refactorizaciones con el tercer paso opcional de generalizar tras obtener
pruebas sólidas. Si tu código tiene condiciones hardcodeadas
inexplicables, tienes que investigar la razón y, si es una regla de negocio
válida, extraerla con una función reveladora de intenciones. Si la
condición se expresa mediante un ajuste global, también puedes
eliminarlo utilizando el código de la Receta 10.2, "Eliminar
ajustes/configuraciones y conmutadores de funciones".
Mira el siguiente ejemplo del mundo real favoreciendo a un cliente con
reglas especiales:
if (currentExposure > 0.15 && customer != "Very Special Customer")
{
// Be extra careful not to liquidate
liquidatePosition();
}
Aquí tienes una solución más declarativa, que te obliga a especificar estos
clientes:
[Link](0.15);
// This follows the "Tell, don't ask" principle
Puedes buscar condiciones primarias hardcoded (relacionadas con tipos
primitivos), pero podrías tener más falsos positivos que problemas reales.
Si realizas revisiones del código, presta especial atención a este tipo de
hardcoding.
Recetas relacionadas
Receta 10.2, "Eliminar ajustes/configuraciones y conmutadores de
funciones"
Receta 14.5, "Sustituir las condiciones if codificadas con colecciones"
14.17 Eliminar booleanos gratuitos
Problema
Tienes una función que contiene varias sentencias de retorno y todas
devuelven el mismo valor.
Solución
Comprueba cuidadosamente tus expresiones booleanas y refactorízalas.
Debate
Diseñar una función para que devuelva siempre un valor fijo puede dar
lugar a una mala legibilidad del código y ocultar posibles defectos. Sin
embargo, si se hace esta elección de diseño, no debería tener un impacto
negativo en la funcionalidad del programa. Si esto ocurre
sistemáticamente en toda la lógica de la función, es probable que se trate
de un error.
He aquí un ejemplo sencillo:
if a > 0 and True:
// This code was left on debugging and passed
// by mistake during code reviews
print("a is positive")
else:
print("a is not positive")
Así es como queda después de simplificarlo:
if a > 0:
print("a is positive")
else:
print("a is not positive")
Las expresiones booleanas deben ser fáciles de leer y entender.
Recetas relacionadas
Receta 14.11, "Evitar que se devuelvan valores booleanos en
las comprobaciones de condiciones"
Receta 14.12, "Modificar la comparación con booleanos"
14.18 Reescribir ternarios anidados
Problema
Tienes muchas condiciones ternarias anidadas.
Solución
Cambia los ternarios anidados por ifs con retorno anticipado.
Debate
El anidamiento siempre es un problema debido a la complejidad añadida,
y puedes solucionarlo con polimorfismo (véase la Receta 14.14, "Convertir
funciones no polimórficas en polimórficas") o con retornos
anticipados. Aquí tienes un ejemplo con cinco ternarios anidados y un
predeterminado:
const getUnits = secs => (
secs <= 60 ? 'seconds' :
secs <= 3600 ? 'minutes' :
secs <= 86400 ? 'hours' :
secs <= 2592000 ? 'days' :
secs <= 31536000 ? 'months' :
'years'
)
Utilizar ifs hace que el código sea más declarativo:
const getUnits = secs => {
if (secs <= 60) return 'seconds';
if (secs <= 3_600) return 'minutes';
if (secs <= 86_400) return 'hours';
if (secs <= 2_592_000) return 'days';
if (secs <= 31_536_000) return 'months';
return 'years'
}
// This is using 'Numeric Separators' notation from JavaScript
// to favor readability.
// The underscores are ignored by the JavaScript engine
// and do not affect the value of the number.
Esto es aún más humano y declarativo:
const getUnits = secs => {
if (secs <= 60) return 'seconds';
if (secs <= 60 * 60) return 'minutes';
if (secs <= 24 * 60 * 60) return 'hours';
if (secs <= 30 * 24 * 60 * 60) return 'days';
if (secs <= 12 * 30 * 24 * 60 * 60) return 'months';
return 'years'
}
// You can read the premature optimization chapter to find out
// if this brings a considerable performance penalty
También puedes utilizar un mapa o pequeños objetos polimórficos
(consulta la Receta 4.1, "Crear pequeños objetos"):
const timeUnits = {
60: 'seconds',
3_600: 'minutes',
86_400: 'hours',
2_592_000: 'days',
31_536_000: 'months',
};
const getUnits = secs => {
const unit = [Link](timeUnits)
.find(([limit]) => secs <= limit)?.[1] || 'years';
return unit;
}
Los linters pueden detectar esta complejidad utilizando árboles de
análisis sintáctico, ya que es necesario tratar la complejidad accidental
para mejorar la legibilidad del código.
Recetas relacionadas
Receta 6.13, "Evitar el infierno de las devoluciones de llamada"
Receta 14.5, "Sustituir las condiciones if codificadas con colecciones"
Capítulo 15. Nulo
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y
comentarios: translation-feedback@[Link]
No pude resistir la tentación de poner una referencia nula, simplemente
porque era muy fácil de implementar. Esto ha provocado innumerables
errores, vulnerabilidades y caídas del sistema, que probablemente han
causado mil millones de dólares de dolor y daños en los últimos cuarenta
años.
Tony Hoare
15.0 Introducción
La mayoría de los programadores utilizan mucho los nulos. Es cómodo,
eficaz y rápido. Y, sin embargo, los programadores han sufrido un montón
de problemas relacionados con su uso. Este capítulo destaca los
problemas del uso de nulos y cómo resolverlos.
Nulo es una bandera. Representa distintas situaciones según el contexto
en el que se utilice e invoque. Esto produce el error más grave en el
desarrollo de software: acoplar una decisión oculta en el contrato entre
un objeto y quien lo utiliza. También rompe el principio de biyección al
representar varios elementos del dominio con la misma entidad y
obligarte a hacer interpretaciones contextuales. Todos los objetos deben
ser lo más específicos posible y tener una única responsabilidad (véase la
Receta 4.7, "Reificar las validaciones de cadenas"), pero el objeto menos
cohesionado de cualquier sistema es el comodín: null, que se asigna a
varios conceptos diferentes en el mundo real.
15.1 Crear objetos nulos
Problema
Utiliza nulo.
Solución
Null es esquizofrénico y no existe en el mundo real. El creador de Null y
ganador del Premio Turing, Tony Hoare , se arrepintió de ello, y los
programadores de todo el mundo sufrieron por ello. Puedes sustituir null
utilizando objetos null.
Debate
Los programadores utilizan null como diferentes indicadores. Esto puede
indicar diferentes condiciones, como la ausencia de un valor, un valor
indefinido, un error, etc. La semántica múltiple provoca acoplamiento y
errores. El uso de null tiene muchos problemas, como el acoplamiento
entre los que llaman y los que envían, el desajuste entre los que llaman y
los que envían, y la contaminación if/switch/case. Además, null no es
polimórfico con los objetos reales, por lo que se produce el error de
excepción del puntero nulo. Por último, null no existe en el mundo real.
Por tanto, viola el principio de biyección.
PATRÓN DE OBJETO NULO
El patrón de objeto nulo sugiere crear un objeto especial llamado "objeto nulo"
que se comporta como un objeto normal, pero que casi no tiene funcionalidad.
Su ventaja es que puedes llamar con seguridad a métodos del objeto nulo sin
tener que comprobar las referencias nulas con un if (Ver Capítulo 14, "Ifs").
En este ejemplo, hay un cupón o null (sin cupón):
class CartItem {
constructor(price) {
[Link] = price;
}
}
class DiscountCoupon {
constructor(rate) {
[Link] = rate;
}
}
class Cart {
constructor(selecteditems, discountCoupon) {
[Link] = selecteditems;
[Link] = discountCoupon;
}
subtotal() {
return [Link]((previous, current) =>
previous + [Link], 0);
}
total() {
if ([Link] == null)
return [Link]();
else
return [Link]() * (1 -
[Link]);
}
}
cart = new Cart([
new CartItem(1),
new CartItem(2),
new CartItem(7)
], new DiscountCoupon(0.15)]);
// 10 - 1.5 = 8.5
cart = new Cart([
new CartItem(1),
new CartItem(2),
new CartItem(7)
], null);
// 10 - null = 10
Al introducir el patrón de objeto nulo, puedes evitar la comprobación "si"
(que a veces puedes pasar por alto):
class CartItem {
constructor(price) {
[Link] = price;
}
}
class DiscountCoupon {
constructor(rate) {
[Link] = rate;
}
discount(subtotal) {
return subtotal * (1 - [Link]);
}
}
class NullCoupon {
discount(subtotal) {
return subtotal;
}
}
class Cart {
constructor(selecteditems, discountCoupon) {
[Link] = selecteditems;
[Link] = discountCoupon;
}
subtotal() {
return [Link](
(previous, current) => previous + [Link], 0);
}
total() {
return [Link]([Link]());
}
}
cart = new Cart([
new CartItem(1),
new CartItem(2),
new CartItem(7)
], new DiscountCoupon(0.15));
// 10 - 1.5 = 8.5
cart = new Cart([
new CartItem(1),
new CartItem(2),
new CartItem(7)
], new NullCoupon());
// 10 - nullObject = 10
La mayoría de los linters pueden mostrar el uso de nulos y avisarte, y hay
lenguajes como Typescript que no tienen nulos en absoluto. El lenguaje
Rust tiene un tipo "Opción" que representa Some(valor) o None. Kotlin
tiene un sistema de "tipo anulable" que permite a los desarrolladores
especificar si un valor puede ser nulo o no. Los nulos también están
presentes en las bases de datos relacionales, representando muchas cosas
diferentes al mismo tiempo.
EXCEPCIÓN DE PUNTERO NULO
Una excepción de puntero nulo es un error común que se produce cuando un
programa intenta acceder o utilizar un puntero nulo, que es una variable o
referencia de objeto que no apunta a ninguna dirección de memoria ni a
ninguna instancia de objeto.
Ver también
"Referencias nulas: El error del billón de dólares" por Tony Hoare
15.2 Eliminar el encadenamiento
opcional
Problema
Tienes código barriendo un nulo bajo la alfombra durante una cascada de
llamadas a funciones.
Solución
Evita los nulos y los valores indefinidos. Si los evitas, nunca necesitarás
opcionales.
Debate
El encadenamiento opcional, los opcionales, la coalescencia y muchas
otras soluciones te ayudan a barrer los infames nulos bajo la alfombra. No
hay necesidad de utilizarlas una vez que tu código sea maduro, robusto y
no tenga más nulos.
ENCADENAMIENTO OPCIONAL
El encadenamiento opcional te permite acceder a propiedades anidadas de un
objeto sin tener que comprobar la existencia de cada propiedad de la cadena. Sin
ella, si intentas acceder a una propiedad de un objeto que no existe, arrojará un
error.
Aquí puedes ver el operador de encadenamiento opcional:
const user = {
name: 'Hacker'
};
if (user?.credentials?.notExpired) {
[Link]();
}
[Link]?.();
// Seems compact but it is hacky and has lots
// of potential NULLs and Undefined
Como siempre, puedes hacerlo explícito. Este código es menos compacto
pero más declarativo:
function login() {}
const user = {
name: 'Hacker',
credentials: { expired: false }
};
if (![Link]) {
login();
}
// Also compact
// User is a real user or a polymorphic NullUser
// Credentials are always defined.
// Can be an instance of InvalidCredentials
// Assuming you eliminated nulls from the code
if ([Link] !== undefined) {
functionDefinedOrNot();
}
// This is also wrong.
// Explicit undefined checks are a similar problem
Puedes conseguir un comportamiento similar utilizando el operador Elvis
(?:):
a ?: b
Es la abreviatura de
if (a != null) a else b
Por ejemplo, puedes reescribir:
val shipTo = address?: "No address specified"
Para:
val shipTo = if (address != null) address else "No address
specified"
Son características del lenguaje; puedes detectarlas y eliminarlas con la
Receta 15.1, "Crear objetos nulos". Muchos desarrolladores se sienten
seguros contaminando su código con código que trata los nulos. Esto es
más seguro que no tratar los nulos en absoluto, pero tarde o temprano se
te escapará alguna comprobación. Los valores nulos, verdaderos y falsos
son siempre problemáticos y tienes que apuntar más alto y hacer un
código más limpio (consulta la Receta 24.2, "Tratar valores verdaderos").
CONSEJO
Lo bueno: elimina todos los nulos de tu código. Lo malo: utilizar
encadenamientos opcionales. Lo feo: no tratar los nulos en absoluto.
Recetas relacionadas
Receta 10.4, "Eliminar la astucia del código"
Receta 14.6, "Cambiar las condiciones booleanas a cortocircuito"
Receta 14.9, "Cómo evitar los cortocircuitos"
Receta 15.1, "Crear objetos nulos"
Receta 24.2, "Tratar con valores de verdad"
Ver también
"Encadenamiento opcional" en [Link]
"Valor Nulo" en [Link]
15.3 Convertir atributos opcionales en
una colección
Problema
Necesitas para modelar un atributo opcional.
Solución
Las colecciones son polimórficas y una herramienta fantástica para
modelar la opcionalidad. Modela atributos opcionales con una colección.
Debate
Si necesitas modelar algo que pueda faltar, algunos lenguajes elegantes te
proporcionarán opcional, anulable y muchas otras soluciones incorrectas
que tratan con nulos. Pero fíjate en que las colecciones vacías y las
colecciones no vacías son polimórficas.
Aquí tienes un ejemplo de correo electrónico opcional:
class Person {
constructor(name, email) {
[Link] = name;
[Link] = email;
}
email() {
return [Link];
// might be null
}
}
// You cannot safely use [Link]()
// You need to check for null explicitly
Esto es lo que parece cuando conviertes el correo electrónico en una
colección (posiblemente vacía) de correos electrónicos:
class Person {
constructor(name, emails) {
[Link] = name;
[Link] = emails;
// emails should always be a collection.
// even an empty one
// You can check it here
if ([Link] > 1) {
throw new Error("Emails collection can have at most one
element.");
}
}
emails() {
return [Link];
}
// You can mutate the emails since they are not essential
addEmail(email) {
[Link](email);
}
removeEmail(email) {
const index = [Link](email);
if (index !== -1) {
[Link](index, 1);
}
}
}
// You can iterate the [Link]()
// in a loop without checking for null
Puedes detectar los atributos anulables y cambiarlos cuando sea
necesario, ya que se trata de una generalización del patrón de objetos
nulos (consulta la Receta 15.1, "Crear objetos nulos").
CONSEJO
Puedes comprobar en la nueva cardinalidad de la colección para asegurarte de
que cumple los requisitos (antes era 0 ó 1).
Recetas relacionadas
Receta 15.1, "Crear objetos nulos"
Receta 15.2, "Eliminar el encadenamiento opcional"
Receta 17.7, "Eliminar argumentos opcionales"
15.4 Utilizar Objetos Reales para Nulo
Problema
En necesitas para crear objetos nulos que sean reales.
Solución
No abuses de los patrones de diseño, incluido el patrón de objeto nulo.
Busca objetos nulos reales en la biyección y crea esos objetos en su lugar.
Debate
Abusar del patrón de objetos nulos (ver Receta 15.1, "Crear objetos
nulos") produce clases vacías, contamina los espacios de nombres
(ver Receta 18.4, "Eliminar clases globales") y crea comportamientos
duplicados. Puedes crear objetos nulos instanciando clases de objetos
reales. El patrón de objetos nulos es una gran alternativa a los nulos e ifs,
y la estructura del patrón te indica que crees una jerarquía. Sin embargo,
esto no es necesario; en su lugar, necesitas que los objetos reales sean
polimórficos respecto a los objetos nulos. La herencia no es la única forma
de lograr el polimorfismo (consulta la Receta 14.4, "Sustitución de las
sentencias switch/case/elseif"). Una solución sencilla es crear un objeto
real que se comporte como un objeto nulo. La Tabla 15-1 enumera
algunos objetos nulos conocidos.
Clase Objeto nulo
Número 0
Cadena ""
Clase Objeto nulo
Matriz []
Tabla 15-1. Objetos nulos conocidos
Este ejemplo crea un NullAddress especial:
abstract class Address {
public abstract String city();
public abstract String state();
public abstract String zipCode();
}
// Using inheritance for null objects is a mistake
// You should use interfaces (when available)
public class NullAddress extends Address {
public NullAddress() { }
public String city() {
return Constants.EMPTY_STRING;
}
public String state() {
return Constants.EMPTY_STRING;
}
public String zipCode() {
return Constants.EMPTY_STRING;
}
public class RealAddress extends Address {
private String zipCode;
private String city;
private String state;
public RealAddress(String city, String state, String zipCode) {
[Link] = city;
[Link] = state;
[Link] = zipCode;
}
public String zipCode() {
return zipCode;
}
public String city() {
return city;
}
public String state() {
return state;
}
}
Éste es el aspecto cuando utilizas una instancia real del objeto Address:
// There are just "addresses"
public class Address {
private String zipCode;
private String city;
private String state;
public Address(String city, String state, String zipCode) {
// Looks anemic :(
[Link] = city;
[Link] = state;
[Link] = zipCode;
}
public String zipCode() {
return zipCode;
}
public String city() {
return city;
}
public String state() {
return state;
}
Address nullAddress = new Address(
Constants.EMPTY_STRING,
Constants.EMPTY_STRING,
Constants.EMPTY_STRING);
// or
Address nullAddress = new Address("", "", "");
// This is the null object
// You should NOT assign it to a singleton, static, or global
// It behaves like a null object. That's enough
// No premature optimizations
Crear clases de objetos nulos es a veces un síntoma de sobrediseño. Esto
suele ocurrir cuando puedes crear y utilizar un objeto real. Este objeto
real nunca debe ser global, singleton (ver Receta 17.2, "Sustitución de
Singletons"), o static.
Recetas relacionadas
Receta 14.4, "Sustitución de las sentencias Switch/Case/Elseif"
Receta 15.1, "Crear objetos nulos"
Receta 17.2, "Sustituir Singletons"
Receta 18.1, "Reificar funciones globales"
Receta 18.2, "Reificar funciones estáticas"
Receta 19.9, "Migrar clases vacías"
15.5 Representar lugares desconocidos
sin utilizar nulos
Problema
Utilizas valores especiales de para indicar los datos que faltan. Pero
puedes cometer errores.
Solución
No utilices valores nulos para lugares reales.
Debate
Si marcas algún valor para indicar que te faltan datos, violarás el
principio de fallo rápido y obtendrás resultados inesperados. Tienes que
modelar el lugar desconocido con polimorfismo (consulta la Receta 14.14,
"Convertir funciones no polimórficas en polimórficas"). La Isla Nula es un
lugar ficticio, situado en las coordenadas 0°N 0°E, en la intersección del
Primer Meridiano y el ecuador en el Océano Atlántico. El nombre de "Isla
Nula" proviene del hecho de que esta ubicación representa el punto en el
que muchos sistemas GPS colocan cualquier dato que tenga coordenadas
de ubicación que falten o no sean válidas. En realidad, no hay ninguna
masa continental en este lugar, y se encuentra en medio del océano. Este
punto se ha convertido en una referencia popular para los sistemas de
información geográfica (SIG) y el software cartográfico, ya que sirve para
filtrar errores en los datos de localización.
Mira el siguiente ejemplo utilizando los valores cero como caso especial:
class Person(val name: String, val latitude: Double, val longitude:
Double)
fun main() {
val people = listOf(
Person("Alice", 40.7128, -74.0060), // New York City
Person("Bob", 51.5074, -0.1278), // London
Person("Charlie", 48.8566, 2.3522), // Paris
Person("Tony Hoare", 0.0, 0.0) // Null Island
)
for (person in people) {
if ([Link] == 0.0 && [Link] == 0.0) {
println("${[Link]} lives on Null Island!")
} else {
println("${[Link]} lives at " +
"(${[Link]}, ${[Link]}).")
}
}
}
Cuando modelas explícitamente la indisponibilidad de los datos, no sabes
dónde vive Tony:
abstract class Location {
abstract fun calculateDistance(other: Location): Double
abstract fun ifKnownOrElse(knownAction: (Location) -> Unit,
unknownAction: () -> Unit)
}
class EarthLocation(val latitude: Double, val longitude: Double) :
Location() {
override fun calculateDistance(other: Location): Double {
val earthRadius = 6371.0
val latDistance = [Link](
latitude - (other as EarthLocation).latitude)
val lngDistance = [Link](
longitude - [Link])
val a = sin(latDistance / 2) * sin(latDistance / 2) +
cos([Link](latitude)) *
cos([Link]([Link])) *
sin(lngDistance / 2) * sin(lngDistance / 2)
val c = 2 * atan2(sqrt(a), sqrt(1 - a))
return earthRadius * c
}
override fun ifKnownOrElse(knownAction:
(Location) -> Unit, unknownAction: () -> Unit) {
knownAction(this)
}
}
class UnknownLocation : Location() {
override fun calculateDistance(other: Location): Double {
throw IllegalArgumentException(
"Cannot calculate distance from an unknown location.")
}
override fun ifKnownOrElse(knownAction:
(Location) -> Unit, unknownAction: () -> Unit) {
unknownAction()
}
}
class Person(val name: String, val location: Location)
fun main() {
val people = listOf(
Person("Alice", EarthLocation(40.7128, -74.0060)), // New York
City
Person("Bob", EarthLocation(51.5074, -0.1278)), // London
Person("Charlie", EarthLocation(48.8566, 2.3522)), // Paris
Person("Tony", UnknownLocation()) // Unknown location
)
val rio = EarthLocation(-22.9068, -43.1729) // Rio de Janeiro
coordinates
for (person in people) {
[Link](
{ location -> println([Link]" is " +
[Link](rio) +
" kilometers { println("${[Link]} "
+ "is at an unknown location.") }
)
}
}
La función ifKnownOrElse es una mónada que resuelve el problema
utilizando el polimorfismo con un objeto nulo. No utilices null para
representar objetos reales (consulta la Receta 15.4, "Utilizar objetos reales
para null").
MÓNADAS
Una mónada proporciona una forma estructurada de encapsular y manipular
funciones. Te permite encadenar operaciones, manejando las funciones y sus
efectos secundarios de forma coherente y predecible, por ejemplo cuando
trabajas con valores opcionales.
Recetas relacionadas
Receta 15.1, "Crear objetos nulos"
Receta 15.4, "Usar objetos reales para nulos"
Receta 17.5, "Convertir los valores de los indicadores especiales 9999 en
normales"
Ver también
"Isla Nula" en Wikipedia
"Isla Nula" en Google Maps
Capítulo 16. Optimización prematura
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y
comentarios: translation-feedback@[Link]
Los programadores pierden enormes cantidades de tiempo pensando o
preocupándose por la velocidad de las partes no críticas de sus programas,
y estos intentos de eficiencia tienen en realidad un fuerte impacto negativo
cuando se tienen en cuenta la depuración y el mantenimiento. Deberíamos
olvidarnos de las pequeñas eficiencias, digamos del 97% de las veces: la
optimización prematura es la raíz de todos los males.
Donald Knuth, "Programación estructurada con declaraciones go to"
16.0 Introducción
La optimización prematura es un gran problema en la industria. A todo
desarrollador le fascina la complejidad computacional y la belleza de los
algoritmos más rápidos. Decidir dónde y cuándo hacer una optimización
es una de las diferencias entre un desarrollador junior y uno senior. Las
optimizaciones no son gratuitas, ya que añaden mucha complejidad
accidental y debes aplicarlas con mucho cuidado. Las soluciones
complejas alejan tu modelo del mundo real porque añaden muchas capas
oscuras entre el modelo y los objetos de negocio, violando el principio de
biyección. También perjudican la legibilidad. Sólo debes aplicarlas bajo
fuertes evidencias fácticas.
COMPLEJIDAD COMPUTACIONAL
La complejidad computacional estudia los recursos necesarios para resolver
problemas computacionales. Los más importantes son el tiempo y la memoria.
Mide y compara la eficacia de los algoritmos y sistemas computacionales en
relación con estos recursos.
Los modernos asistentes de inteligencia artificial te ayudan a optimizar tu
código. Puedes convertirte en un centauro tecnológico, dejar las
herramientas de optimización accidentales a los asistentes artificiales y
centrarte en los problemas esenciales del dominio.
16.1 Evitar las identificaciones en los
objetos
Problema
Tienes identificadores, claves primarias y referencias que no existen en el
mundo real.
Solución
Elimina los identificadores y enlaza directamente con tus objetos.
Debate
Los ID son accidentales y no existen en el mundo real, a menos que los
necesites para exportar tus objetos y crear una referencia global. Por
tanto, no necesitas enlazar objetos internamente utilizando IDs. No son
atributos válidos para ningún objeto, ya que las referencias están siempre
fuera del objeto (ningún objeto debe conocer su identificación). Siguiendo
el principio de biyección, debes evitarlos y utilizar identificadores sólo si
necesitas proporcionar una referencia externa (accidental). Los casos más
comunes de identificadores externos son las bases de datos, las API y las
serializaciones.
CLAVES PRIMARIAS
En el contexto de las bases de datos, una clave primaria es un identificador
único para un registro o fila concretos de una tabla. Sirve para identificar de
forma única cada registro de una tabla y permite una búsqueda y ordenación
eficaces de los datos. Una clave primaria puede ser una sola columna o una
combinación de columnas que, al combinarse, forman un valor único para cada
registro de la tabla. Normalmente, una clave primaria se crea con una tabla y la
utilizan como referencia otras tablas de una base de datos que tengan relaciones
con esa tabla.
Si necesitas declarar una clave, utiliza siempre claves oscuras (algo no
relacionado con pequeños enteros consecutivos) como los GUID,
y si tienes miedo de crear un gran grafo de relaciones utiliza proxies o
lazy loading (buscar el objeto completo relacionado sólo cuando lo
necesites). Necesitas pruebas contundentes de una penalización real del
rendimiento para añadir esta complejidad accidental a tu código.
IDENTIFICADOR ÚNICO GLOBAL
Un GUID (identificador único global) es un identificador único utilizado en
sistemas informáticos para asignar recursos como archivos, objetos o entidades
en una red. Los GUID se generan mediante algoritmos que garantizan su
unicidad.
Aquí tienes un dominio escolar con las
entidades Teacher, School y Student:
class Teacher {
static getByID(id) {
// This is coupled to the database
// Thus violating separation of concerns
}
constructor(id, fullName) {
[Link] = id;
[Link] = fullName;
}
}
class School {
static getByID(id) {
// go to the coupled database
}
constructor(id, address) {
[Link] = id;
[Link] = address;
}
}
class Student {
constructor(id, firstName, lastName, teacherId, schoolId) {
[Link] = id;
[Link] = firstName;
[Link] = lastName;
[Link] = teacherId;
[Link] = schoolId;
}
school() {
return [Link]([Link]);
}
teacher() {
return [Link]([Link]);
}
}
Esto es lo que parece cuando se referencian en el mundo real:
class Teacher {
constructor(fullName) {
[Link] = fullName;
}
}
class School {
constructor(address) {
[Link] = address;
}
}
class Student {
constructor(firstName, lastName, teacher, school) {
[Link] = firstName;
[Link] = lastName;
[Link] = teacher;
[Link] = school;
}
}
// The ids are no longer needed since they don't exist in the real
world.
// If you need to expose a School to an external API or a database,
// another object (not school)
// will keep the mapping externalId<->school and so on
Se trata de una política de diseño. Puedes hacer que los linters y los
objetos de negocio te avisen si defines un atributo o una función que
incluya el ID de secuencia. Los ID no son necesarios para el software
orientado a objetos. Haces referencia a objetos (esencial) y nunca a IDs
(accidental). Si necesitas proporcionar una referencia fuera del ámbito de
tu sistema (APIs, interfaces, serializaciones) utiliza IDs oscuros y sin
sentido, como los GUIDs. Puedes utilizar el patrón de diseño de
repositorios o algo similar.
PATRÓN DE DISEÑO DE REPOSITORIOS
El patrón de diseño de repositorio proporciona una capa de abstracción entre
la lógica de negocio de la aplicación y la capa de almacenamiento de datos, lo
que permite una arquitectura más flexible y fácil de mantener.
Recetas relacionadas
Receta 3.6, "Eliminar DTOs"
Receta 16.2, "Eliminar la optimización prematura"
Ver también
"Identificador Único Universal" en Wikipedia
16.2 Eliminar la optimización
prematura
Problema
Tú tienes código especulativo optimizado sin pruebas empíricas.
Solución
No especules sobre cosas que podrían no ocurrir. Haz optimizaciones
utilizando pruebas de escenarios del mundo real.
Debate
La optimización prematura es una práctica muy desacertada y habitual
que hace que el código sea más complejo y difícil de mantener. Perjudica
la legibilidad y la comprobabilidad y provoca acoplamientos
accidentales. Debes optimizar tu código sólo después de que tenga una
buena cobertura y un punto de referencia concluyente del escenario del
mundo real. Si tienes que decidir entre dos implementaciones, debes
elegir la más legible, aunque el punto de referencia diga que funciona mal
en un ciclo con mil ejecuciones. Lo más probable es que no llames a la
función mil veces. En su lugar, puedes utilizar la técnica de desarrollo
dirigido por pruebas (ver Receta 4.8, "Eliminar propiedades
innecesarias"), ya que siempre favorece la solución más sencilla.
He aquí un ejemplo en el que se utiliza la caché para una situación en la
que el rendimiento de la base de datos es aceptable:
class Person {
ancestors() {
cachedResults =
[Link]().relativesCache(
[Link]);
if (cachedResults != null) {
return ([Link]([Link])).getAllParents();
}
return database().getAllParents([Link]);
}
}
Como no vas a ejecutar este código mil veces, puedes simplificarlo:
class Person {
ancestors() {
return
[Link]().concat([Link]());
}
meAndAncestors() {
return [Link]().push(this);
}
}
Se trata de un antipatrón (ver Receta 5.5, "Eliminar la inicialización
perezosa"). No puede ser detectado por herramientas mecánicas
(todavía). Debes aplazar las decisiones sobre rendimiento hasta que los
modelos funcionales estén lo suficientemente maduros. Donald Knuth
creó/compiló los algoritmos y estructuras de datos con mejor rendimiento
en su libro El arte de programar ordenadores. Y haciendo gala de una
gran sabiduría, te advirtió que no abusaras de ellos.
Recetas relacionadas
Receta 10.4, "Eliminar la astucia del código"
Ver también
"Programación estructurada con declaraciones go to" de Donald Knuth en
ACM
"Optimización prematura" en C2 Wiki
16.3 Eliminar las optimizaciones
prematuras bit a bit
Problema
Tienes código microoptimizado con operadores bit a bit.
Solución
No utilices operadores bit a bit a menos que tu modelo de negocio sea la
lógica bit a bit.
OPERADORES BIT A BIT
Los operadores bit a bit manipulan los bits individuales de los números. Tu
ordenador los utiliza para realizar operaciones lógicas de bajo nivel entre bits,
como AND, OR y XOR. Funcionan en el dominio de los números enteros, que es
diferente del dominio booleano.
Debate
No tienes que confundir los enteros con los booleanos. Son
completamente diferentes en la biyección. Lamentablemente, muchos
lenguajes de programación los mezclan por ingenio y optimización
prematura, rompiendo la barrera natural entre la implementación
accidental y los problemas esenciales del dominio. Esta situación acarrea
un montón de problemas imprevistos con los valores truthy y falsy
(ver Receta 24.2, "Tratar con valores truthy") y también rompe la
mantenibilidad. Sólo debes optimizar el código basándote en pruebas y
utilizar siempre el método científico, la evaluación comparativa, y
mejorar el código sólo si es realmente necesario. Asume el coste de la
modificabilidad y la mantenibilidad cuando utilices operadores bit a
bit. El siguiente código funciona en muchos lenguajes, pero es un hack:
const nowInSeconds = ~~([Link]() / 1000)
// The double bitwise NOT operator ~~
// is a bitwise operation that performs a bitwise
// negation followed by a bitwise negation again.
// This operation effectively truncates any decimal places
// converting the result to an integer.
Esto está más claro:
const nowInSeconds = [Link]([Link]() / 1000)
Como siempre, si tu dominio es el tiempo real o el software de misión
crítica, puedes sacrificar la legibilidad por el cambio, pero si encuentras
este código en una solicitud de pull o en una revisión de código, tienes que
entender el razonamiento. Si no está justificado, debes hacer un rollback y
cambiarlo a lógica normal.
Recetas relacionadas
Receta 10.4, "Eliminar la astucia del código"
Receta 16.2, "Eliminar la optimización prematura"
Receta 16.5, "Cambiar las optimizaciones estructurales"
Receta 22.1, "Eliminar bloques de excepción vacíos"
Receta 24.2, "Tratar con valores de verdad"
16.4 Reducir la sobregeneralización
Problema
Tienes un código con una sobregeneralización prematura.
Solución
No hagas generalizaciones más allá del conocimiento real. Tienes que ser
un experto en aprender y no en adivinar el futuro.
Debate
La sobregeneralización es siempre un caso especial de violación de la
biyección (como se define en el Capítulo 2), ya que estás modelando
entidades que aún no has visto en el mundo real. Refactorizar no es sólo
mirar el código estructural; tienes que refactorizar el comportamiento y
comprobar si realmente necesita una abstracción.
Este ejemplo hace una inferencia sobre algo antes de tener pruebas
suficientes:
fn validate_size(value: i32) {
validate_integer(value);
}
fn validate_years(value: i32) {
validate_integer(value);
}
fn validate_integer(value: i32) {
validate_type(value, :integer);
validate_min_integer(value, 0);
}
Es una solución más compacta:
fn validate_size(value: i32) {
validate_type(value, Type::Integer);
validate_min_integer(value, 0);
}
fn validate_years(value: i32) {
validate_type(value, Type::Integer);
validate_min_integer(value, 0);
}
// Duplication is accidental, therefore you should not abstract it
El desarrollo de software es una actividad de pensamiento, y dispones de
herramientas automatizadas para ayudarte y asistirte.
Recetas relacionadas
Receta 10.1, "Eliminar código repetido"
16.5 Cambiar las optimizaciones
estructurales
Problema
Tienes optimizaciones estructurales sobre la complejidad temporal y
espacial basadas en escenarios no realistas.
Solución
No optimices las estructuras de datos hasta que tengas un punto de
referencia de un escenario de uso real.
Debate
Las optimizaciones estructurales perjudican la legibilidad porque te
alejan de la biyección. Si necesitas hacer optimizaciones estructurales
porque tienes pruebas sólidas, debes seguir cubriendo tus escenarios con
pruebas. Escribe código legible (y posiblemente sin rendimiento). Haz una
prueba de rendimiento real con datos de usuario reales. (No, iterar tu
código 100.000 veces puede que no sea un caso de uso real.) Si tienes datos
concluyentes, tienes que mejorar los cuellos de botella del punto de
referencia utilizando el principio de Pareto (ver"Capítulo 1" en el Capítulo
1). Ataca el peor 20% de los problemas que causan el 80% del
mal rendimiento.
En la universidad y en los cursos online, la gente suele aprender
algoritmos, estructuras de datos y complejidad computacional antes que
buenas reglas de diseño. Tiendes a sobrestimar los (posibles) problemas
de rendimiento y a subestimar la legibilidad del código y la vida útil del
software. La optimización prematura suele venir sin pruebas de que estés
resolviendo problemas reales, y necesitas mejorar quirúrgicamente tu
código cuando los hechos te digan que tienes un problema real.
Observa este problema en acción con un ejemplo del mundo real:
for (k = 0; k < 3 * 3; ++k) {
const i = [Link](k / 3);
const j = k % 3;
[Link](i + ' ' + j);
}
// This cryptic piece of code iterates a
// two-dimensional array
// You don't have proof this will be useful
// in real contexts
He aquí cómo reescribirlo completamente:
for (outerIterator = 0; outerIterator< 3; outerIterator++) {
for (innerIterator = 0; innerIterator< 3; innerIterator++) {
[Link](outerIterator + ' ' + innerIterator);
}
}
// This is a readable double for-loop
// 3 is a small number
// No performance issues (for now)
// You will wait for real evidence
Si escribes código empresarial y no código de bajo nivel, tienes que dejar
de optimizar para máquinas y empezar a optimizar para lectores
humanos y mantenedores de código. Además, evita los lenguajes de
programación diseñados para la optimización prematura (Go, Rust, C++) y
favorece las opciones de código robusto, de alto nivel y limpio.
Recetas relacionadas
Receta 10.4, "Eliminar la astucia del código"
Receta 16.2, "Eliminar la optimización prematura"
16.6 Retirar los botes de ancla
Problema
En tienes el código por si lo necesitas más adelante.
Solución
No dejes código para uso futuro.
Debate
Los anclajes aportan complejidad y acoplamiento accidentales y son
también un ejemplo de código muerto. Tienes que eliminar el código
muerto y dejar sólo el código cubierto y realmente probado. He aquí un
ejemplo de barco ancla:
final class DatabaseQueryOptimizer {
public function selectWithCriteria($tableName, $criteria) {
// Make some optimizations manipulating criteria
}
private function sqlParserOptimization(SQLSentence $sqlSentence)
: SQLSentence {
// Parse the SQL converting it to a string
// and then working with their nodes as strings and lots of regex
// This was a very costly operation overcoming real SQL benefits.
// But since you made too much work we decide to keep the code.
}
}
Este es el aspecto que tiene cuando lo quitas:
final class DatabaseQueryOptimizer {
public function selectWithCriteria($tableName, $criteria) {
// Make some optimizations manipulating criteria
}
}
Utilizando algunas pruebas de mutación (ver Receta 5.1, "Cambiar var a
const") variantes puedes eliminar el código muerto y ver si fallan algunas
pruebas. Necesitas tener una buena cobertura para confiar en esta
solución. El código muerto siempre es un problema, y puedes utilizar
técnicas modernas de desarrollo como TDD (ver Receta 4.8, "Eliminar
propiedades innecesarias") para asegurarte de que todo el código está
vivo.
Recetas relacionadas
Receta 10.9, "Eliminar objetos poltergeist"
Receta 12.1, "Eliminar código muerto"
16.7 Extraer cachés de objetos de
dominio
Problema
Las cachés son elegantes porque aparentemente resuelven los problemas
de rendimiento mágicamente, pero tienen un coste oculto.
Solución
Retira el alijo hasta que tengas pruebas concretas y estés dispuesto a
pagar los costes de su uso.
Debate
CACHÉ
Una caché almacena temporalmente los objetos a los que se accede con
frecuencia para acceder a ellos más rápidamente. Puedes utilizarla para mejorar
el rendimiento de las aplicaciones de software reduciendo el número de accesos
a recursos costosos. Al almacenar los datos en la memoria caché, el software
puede evitar la sobrecarga de acceder a dispositivos de almacenamiento más
lentos y, en su lugar, recuperar los objetos directamente de la caché.
Las cachés crean mucho acoplamiento, ya que no existen en el mundo
real, y dificultan los cambios. Rompen el determinismo y dañan la
comprobabilidad y la mantenibilidad. Cuando programas una solución
con caché, tu código se vuelve errático a menos que encapsules la
solución con mucho cuidado. La invalidación de la caché es un problema
muy difícil y la mayoría de las veces subestimado por los desarrolladores
de cachés. Si tienes un punto de referencia concluyente y estás dispuesto a
pagar por cierto acoplamiento, puedes poner un objeto en medio. Ten
cuidado y añade pruebas unitarias para todos tus escenarios de
invalidación. La experiencia demuestra que te enfrentarás a ellos de
forma incremental. Busca una metáfora de caché del mundo real en
el MAPPER (como se define en el Capítulo 2); si la encuentras, puedes
modelarla.
Aquí tienes una caché intrusiva incrustada en el objeto Book:
final class Book {
private $cachedBooks;
public function getBooksFromDatabaseByTitle(string $title) {
if (!isset($this->cachedBooks[$title])) {
$this->cachedBooks[$title] =
$this->doGetBooksFromDatabaseByTitle($title);
}
return $this->cachedBooks[$title];
}
private function doGetBooksFromDatabaseByTitle(string $title) {
return globalDatabase()->selectFrom('Books', 'WHERE TITLE =
' . $title);
}
}
Puedes colocar la caché fuera del objeto de dominio Book:
final class Book {
// Just Book related Stuff
}
interface BookRetriever {
public function bookByTitle(string $title);
}
final class DatabaseLibrarian implements BookRetriever {
public function bookByTitle(string $title) {
// Go to the database (hopefully not a global)
}
}
final class HotSpotLibrarian implements BookRetriever {
// You always look for real-life metaphors
private $inbox;
private $realRetriever;
public function bookByTitle(string $title) {
if ($this->inbox->includesTitle($title)) {
// You are lucky. Someone has just returned the book
copy.
return $this->inbox->retrieveAndRemove($title);
} else {
return $this->realRetriever->bookByTitle($title);
}
}
}
Esto es un olor de diseño y puedes imponerlo mediante una política. Las
cachés deben ser funcionales e inteligentes; así podrás gestionar la
invalidación. Las cachés de propósito general sólo son adecuadas para
objetos de bajo nivel, como sistemas operativos, archivos y secuencias; no
debes almacenar en caché objetos de dominio con ellas.
Recetas relacionadas
Receta 13.6, "Redefinir Hash e Igualdad"
Receta 16.2, "Eliminar la optimización prematura"
16.8 Eliminar eventos de devolución de
llamada basados en la implementación
Problema
Tienes código acoplado entre el activador y la acción en una devolución
de llamada.
Solución
Nombra tus funciones de acuerdo con lo que ocurrió en el evento.
Debate
Las devoluciones de llamada siguen el patrón de diseño del observador,
cuya intención es desvincular un evento de la acción. Si estableces este
vínculo directamente, tu código será menos mantenible y estará más
relacionado con la implementación. Como norma, debes dar a los eventos
el nombre de "lo que ha ocurrido", no de "lo que debes hacer".
Mira el evento de este ejemplo:
const Item = ({name, handlePageChange)} =>
<li onClick={handlePageChange}>
{name}
</li>
// handlePageChange is coupled with what you decide to do
// instead of what really happened
//
// You cannot reuse this kind of callback
Esto es más conciso y menos acoplado:
const Item = ({name, onItemSelected)} =>
<li onClick={onItemSelected}>
{name}
</li>
// onItemSelected will be called just when an item was selected.
// Parent can decide what to do (or do nothing)
// You defer the decision
Puedes detectar este problema y aplicar la receta durante las revisiones
de código entre compañeros. Los nombres son muy importantes. Debes
retrasar la definición de los nombres asociados a la implementación hasta
el último momento.
PATRÓN DE DISEÑO OBSERVADOR
El patrón de diseño observador define una dependencia de uno a muchos entre
objetos; por ejemplo, cuando un objeto cambia de estado, todos sus objetos
dependientes son notificados y actualizados automáticamente, sin una
referencia directa. Te suscribes a los eventos publicados y el objeto modificado
emite una alerta sin saber quién está suscrito.
Recetas relacionadas
Receta 17.13, "Eliminar el código de negocio de la interfaz de usuario"
16.9 Eliminar consultas de los
constructores
Problema
Tienes métodos que acceden a una base de datos en un constructor.
Solución
Los constructores deben construir (y probablemente inicializar) objetos.
Desacopla tu mecanismo de persistencia de tus objetos de dominio.
Debate
Los efectos secundarios son siempre una mala práctica. Además, debes
evitar acoplar la base de datos con tus objetos de negocio, ya que la
persistencia es accidental y no está presente en el MAPPER. Debes
desacoplar la lógica esencial del negocio de la persistencia accidental. En
las clases de persistencia, ejecuta las consultas en funciones que no sean
constructores/destructores. Al tratar con código heredado, a menudo te
encontrarás con que la base de datos no está correctamente separada de
los objetos de negocio. Los constructores nunca deben tener efectos
secundarios porque, según el principio de responsabilidad única
(ver Receta 4.7, "Reificar las validaciones de cadenas"), sólo deben
construir objetos válidos.
Aquí hay un constructor Person que llama explícitamente a la base de
datos:
public class Person {
int childrenCount;
public Person(int id) {
connection = new DatabaseConnection();
childrenCount = [Link](
"SELECT COUNT(CHILDREN) FROM PERSON WHERE ID = " . id);
}
}
Así es como queda después de desacoplarlo:
public class Person {
int childrenCount;
public Person(int id, int childrenCount) {
[Link] = childrenCount;
// You can assign the number in the constructor
// Accidental database is decoupled
// You can test the object
}
}
La separación de preocupaciones es clave, y el acoplamiento es tu
principal enemigo a la hora de diseñar software robusto (consulta la
Receta 8.3, "Eliminar comentarios lógicos").
Recetas relacionadas
Receta 16.10, "Eliminar código de los destructores"
16.10 Eliminar código de los
destructores
Problema
Tienes código que desasigna cosas en tus destructores.
Solución
No utilices destructores. Y no escribas código funcional en ellos.
Debate
Utilizar destructores de código crea problemas de acoplamiento y produce
resultados inesperados y, muy probablemente, fugas de memoria. Debes
evitarlos siguiendo la regla del cero y dejar que el recolector de basura
trabaje por ti. Un destructor de clase es un método especial que se llama
cuando un objeto se destruye o sale del ámbito. En el pasado, las
máquinas virtuales no tenían recolectores de basura, y los destructores se
encargaban de limpiar los recursos que el objeto había adquirido durante
su vida, como cerrar los archivos abiertos o liberar la memoria asignada
en el montón. Hoy en día, la destrucción de objetos y la liberación de
recursos son automáticas en la mayoría de los lenguajes de programación
modernos.
REGLA DEL CERO
La regla del cero sugiere que evites escribir código para cosas que el lenguaje de
programación o las bibliotecas existentes pueden hacer por sí solos. Si hay un
comportamiento que puede implementarse sin escribir código, entonces
deberías basarte en el código existente.
Aquí tienes un destructor explícito que desasigna un recurso de archivo:
class File {
public:
File(const std::string& filename) {
file_ = fopen(filename.c_str(), "r");
}
~File() {
if (file_) {
fclose(file_);
}
}
private:
FILE* file_;
};
Puedes añadir una advertencia en el destructor sobre si un File sigue
abierto:
class File {
public:
File() : file_(nullptr) {}
bool Open(const std::string& filename) {
if (file_) {
fclose(file_);
}
file_ = fopen(filename.c_str(), "r");
return (file_ != nullptr);
}
bool IsOpen() const {
return (file_ != nullptr);
}
void Close() {
if (file_) {
fclose(file_);
file_ = nullptr;
}
}
~File() {
// Instead of closing the file you throw an exception
// if it is open (which is an invalid scenario)
if (file_) {
throw std::logic_error(
"File is still open after reaching its destructor");
}
}
private:
FILE* file_;
};
En muchos lenguajes , existen interfaces específicas para transmitir la
intención de "cerrabilidad", y que deben utilizarse si realmente quieres
liberar recursos, por ejemplo, Closable en Java o la
interfaz Disposable en C#.
Los recolectores de basura pueden advertirte cuando escribes código en
destructores. Como excepciones notables, en código muy crítico de bajo
nivel no puedes permitirte un recolector de basura debido a su pequeña
penalización de rendimiento y consumo de recursos. Otra excepción para
los recolectores de basura son los sistemas en tiempo real, porque la
mayoría de los recolectores automáticos se ejecutan en un momento
aleatorio y producen retrasos arbitrarios en el comportamiento en tiempo
real. En otros casos, escribir código en destructores es síntoma de
optimización prematura. Necesitas comprender el ciclo de vida de tus
objetos y gestionar los eventos con precisión.
RECOLECTOR DE BASURA
Los lenguajes de programación utilizan un recolector de basura para gestionar
automáticamente la asignación y desasignación de memoria. Funciona
identificando y eliminando de la memoria los objetos que el programa ya no
utiliza, liberando así la memoria utilizada.
Recetas relacionadas
Receta 16.9, "Eliminar consultas de los constructores"
Capítulo 17. Acoplamiento
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y
comentarios: translation-feedback@[Link]
Dos partes de un sistema informático están acopladas si un cambio en una
puede provocar un cambio en la otra.
Neal Ford y otros, Arquitectura de software: The Hard Parts (O'Reilly
2021)
17.0 Introducción
El acoplamiento es el grado de interdependencia entre tus objetos. Un
acoplamiento alto significa que los cambios en un objeto pueden tener un
impacto significativo en los demás, mientras que un acoplamiento
bajo significa que los objetos son relativamente independientes y los
cambios en uno de ellos tienen poco impacto en los demás. Un
acoplamiento alto puede dificultar la introducción de cambios en el
software sin consecuencias no deseadas. La mayor parte del trabajo en un
gran sistema de software tiene un acoplamiento bajo accidental. Los
sistemas con alto acoplamiento son más difíciles de entender y mantener,
las interacciones entre objetos son más complejas y los cambios causan
efectos dominó en toda la base de código. Un sistema acoplado con
propiedades emergentes deseables puede ser fascinante, mientras que
uno mal acoplado puede convertirse en una pesadilla de mantenimiento.
17.1 Explicitar los supuestos ocultos
Problema
Tienes código con suposiciones ocultas no explícitas en tu solución y que
afectan al comportamiento de tu sistema.
Solución
Mantén tu código explícito.
Debate
El software consiste en contratos, y los contratos ambiguos son una
pesadilla. Las suposiciones ocultas son creencias o expectativas
subyacentes que no se indican explícitamente en el código. Siguen
estando presentes y pueden influir en el comportamiento del software.
Diversos factores pueden dar lugar a suposiciones, como requisitos
incompletos, presunciones incorrectas sobre el usuario o el entorno,
limitaciones del lenguaje o las herramientas de programación, y malas
decisiones accidentales.
Aquí puedes ver una suposición oculta (incorrecta) sobre las unidades de
medida:
tenCentimeters = 10
tenInches = 10
tenCentimeters + tenInches
# 20
# This error is based on the hidden assumption of a unit (any)
# and caused the Mars Climate Orbiter failure
Si lo haces explícito, podrás gestionar una excepción temprana y ocuparte
de la conversión:
class Unit:
def __init__(self, name, symbol):
[Link] = name
[Link] = symbol
class Measure:
def __init__(self, scalar, unit):
[Link] = scalar
[Link] = unit
def __str__(self):
return f"{[Link]} {[Link]}"
centimetersUnit = Unit("centimeters", "cm")
inchesUnit = Unit("inches", "in")
tenCentimeters = Measure(10, centimetersUnit)
tenInches = Measure(10, inchesUnit)
tenCentimeters + tenInches
# error until a conversion factor is introduced,
# in this case, the conversion is constant
# inches = centimeters / 2.54
Las suposiciones ocultas pueden ser difíciles de identificar y pueden
provocar defectos, vulnerabilidades de seguridad y problemas de
usabilidad. Para mitigar estos riesgos, debes ser consciente de las
suposiciones y los prejuicios. Involúcrate con los usuarios para
comprender sus necesidades y expectativas, y prueba a fondo tu software
en varios escenarios para descubrir suposiciones ocultas y casos de
perímetro.
Recetas relacionadas
Receta 6.8, "Sustituir números mágicos por constantes"
Ver también
Consulta el debate sobre el desastre del Orbitador Climático de Marte en
la sección "El único principio de diseño de software" del capítulo 2.
17.2 Sustitución de Singletons
Problema
Tienes singletons en tu código.
Solución
Los singletons pueden crear muchos problemas, y la mayor parte de la
comunidad de desarrolladores considera que los singletons son
antipatrones. Puedes sustituirlos por objetos únicos contextuales.
Debate
Los singletons son un claro ejemplo de acoplamiento global
y optimización prematura(véase el Capítulo 16, "Optimización
prematura"). Dificultan la comprobabilidad, introducen un acoplamiento
estrecho entre clases y plantean problemas en entornos multihilo. En el
pasado, muchos desarrolladores utilizaban singletons como punto de
acceso global bien conocido para el acceso a la base de datos, la
configuración, la configuración del entorno y el registro.
Aquí puedes ver una definición típica de singleton en la que God es único
según ciertas religiones:
class God {
private static $instance = null;
private function __construct() {
}
public static function getInstance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
}
En su lugar, puedes utilizar un objeto contextual:
interface Religion {
// Define common behavior for religions
}
final class God {
// Different religions have different beliefs
}
final class PolytheisticReligion implements Religion {
private $gods;
public function __construct(Collection $gods) {
$this->gods = $gods;
}
}
final class MonotheisticReligion implements Religion {
private $godInstance;
public function __construct(God $onlyGod) {
$this->godInstance = $onlyGod;
}
}
// According to Christianity and some other religions,
// there's only one God.
// This does not hold for other religions.
$christianGod = new God();
$christianReligion = new MonotheisticReligion($christianGod);
// Under this context God is unique.
// You cannot create or change a new one.
// This is a scoped global.
$jupiter = new God();
$saturn = new God();
$mythologicalReligion = new PolytheisticReligion([$jupiter,
$saturn]);
// Gods are unique (or not) according to context
// You can create test religions with or without unicity
// This is less coupled since you break the direct reference to the
God class
// God class's single responsibility is to create gods. Not to manage
them
Los Singletons son un antipatrón de diseño y debes evitarlos por
política. Puedes añadir reglas de linter para patrones
como getInstance() para que los nuevos desarrolladores no puedan
infectar el código con este antipatrón. La Tabla 17-1 muestra un resumen
de los problemas conocidos con los singletons.
Problema Descripción
Violación de la biyección Los solteros no existen en el mundo real.
Acoplamiento estrecho Proporcionan un punto de acceso global
difícil de romper.
Aplicación accidental Está demasiado relacionado con la
aplicación y no imita el comportamiento
del mundo real.
Comprobabilidad dura Es difícil escribir pruebas unitarias
cuando hay singletons.
No guarda memoria Los recolectores de basura modernos
tratan mejor los objetos volátiles que los
permanentes.
Rompe la inyección de Es más difícil desvincular las
dependencia dependencias.
Violación del contrato de Cuando pides a una clase que cree una
instanciación instancia, esperas una nueva.
Violación rápida No deberías poder crear nuevas
instancias, y deberían fallar en lugar de
darte una instancia antigua.
Problema Descripción
Acoplamiento de Utilizas getInstance() en lugar del
aplicación método new().
Más difícil de aplicar la TDD se ocupa del acoplamiento y tienes
técnica de desarrollo que sortearlo al crear nuevas pruebas.
dirigido por pruebas
(TDD)
Los conceptos únicos son Consulta el ejemplo de código anterior.
contextuales La unicidad depende del ámbito. Y nunca
debe ser global.
Problemas del entorno Muchos singletons no son ni thread-safe
multihilo ni reentrantes y provocan
comportamientos inesperados.
Acumular estado basura Ejecutar varias pruebas puede hinchar el
singleton, y como no se recoge, la basura
permanece.
Violación de la La única responsabilidad de la Clase es
responsabilidad única de crear instancias, no gestionarlas.
clase / separación de
intereses
Amigo fácil para el punto Una vez que tengas un singleton, puedes
de entrada empezar a contaminar esta cómoda
referencia global con más objetos.
El infierno de la Una clase depende de un objeto
dependencia singleton, y ese objeto singleton depende
de otro objeto singleton, y así
sucesivamente.
Falta de flexibilidad Una vez creado un objeto singleton, no se
puede sustituir ni modificar.
Problema Descripción
Dificultad en la gestión Gestionar el ciclo de vida de un singleton
del ciclo de vida puede ser complicado y provocar fugas
de memoria o una utilización innecesaria
de recursos.
Tabla 17-1. Algunos problemas conocidos con los singletons
Recetas relacionadas
Receta 10.4, "Eliminar la astucia del código"
Receta 12.5, "Eliminar abusos de patrones de diseño"
17.3 Romper Objetos de Dios
Problema
Tienes un objeto que sabe demasiado o hace demasiado.
Solución
No asignes demasiadas responsabilidades a un solo objeto.
Debate
OBJETOS DE DIOS
Los objetos Dios tienen una cantidad excesiva de responsabilidades o control
sobre todo el sistema. Estos objetos suelen ser grandes y complejos, con una
cantidad significativa de código y lógica. Violan el principio de responsabilidad
única (ver Receta 4.7 , "Reificar validaciones de cadenas") y el concepto de
separación de preocupaciones (ver Receta 8.3, "Eliminar comentarios lógicos").
Los objetos Dios tienden a convertirse en un cuello de botella en la arquitectura
del software, haciendo que el sistema sea difícil de mantener, escalar y probar.
Los objetos Dios tienen cohesión e invitan a mucho acoplamiento,
conflictos de fusión y otros problemas de mantenimiento. También violan
el principio de biyección al tener más responsabilidades que las entidades
del mundo real. Tienes que adherirte al principio de responsabilidad
única y dividir las responsabilidades. Ejemplos notables son las viejas
bibliotecas rutinarias de software.
He aquí un ejemplo de objeto Dios:
class Soldier {
run() {}
fight() {}
driveGeneral() {}
clean() {}
fire() {}
bePromoted() {}
serialize() {}
display() {}
persistOnDatabase() {}
toXML() {}
jsonDecode() {}
// ...
}
En lugar de eso, divídelo, dejando sólo las responsabilidades esenciales:
class Soldier {
run() {}
fight() {}
clean() {}
}
En cuanto al resto de funciones de Soldier, puedes encapsular cada una
en una clase lógica específica. Los linters pueden contar los métodos y
advertir de un umbral de posibles objetos Dios. Como ejemplo notable, los
objetos cuya única responsabilidad es aportar un punto de entrada, como
las fachadas, pueden ser objetos Dios. Las bibliotecas estaban bien en los
años 60, pero en la programación orientada a objetos, distribuirás
responsabilidades entre muchos objetos.
PATRÓN DE FACHADA
El patrón de diseño de fachada proporciona una interfaz simplificada a un
sistema o subsistema complejo. Se utiliza para ocultar la complejidad de un
sistema y proporcionar una interfaz más sencilla para que la utilicen los clientes.
También se comporta como un mediador entre el cliente y el subsistema,
protegiendo al cliente de los detalles de la implementación del subsistema.
Las clases constantes son un caso especial. Estas clases tienen poca
cohesión y mucho acoplamiento, lo que viola el principio de
responsabilidad única (véase la Receta 4.7, "Reificar las validaciones de
cadenas"). Aquí puedes ver una clase con demasiadas constantes no
relacionadas:
public static class GlobalConstants
{
public const int MaxPlayers = 10;
public const string DefaultLanguage = "en-US";
public const double Pi = 3.14159;
}
Puedes dividirlo en clases más pequeñas y luego añadir el
comportamiento pertinente a cada una de ellas:
public static class GameConstants
{
public const int MaxPlayers = 10;
}
public static class LanguageConstants
{
public const string DefaultLanguage = "en-US";
}
public static class MathConstants
{
public const double Pi = 3.14159;
}
Puedes decir a tus linters que te adviertan de demasiadas definiciones
constantes contra un umbral preestablecido, ya que encontrar las
responsabilidades correctas es una de tus tareas principales al diseñar
software.
Recetas relacionadas
Receta 6.8, "Sustituir números mágicos por constantes"
Receta 10.2, "Eliminar ajustes/configuraciones y conmutadores de
funciones"
Receta 11.6, "Romper demasiados atributos"
Receta 17.4, "Romper el cambio divergente"
17.4 Romper el cambio divergente
Problema
En cambias algo en una clase y luego también tienes que cambiar algo no
relacionado en la misma clase.
Solución
Las clases deben tener una sola responsabilidad y una sola razón para
cambiar. Divide la clase divergente.
Debate
Las clases divergentes tienen baja cohesión, alto acoplamiento y
duplicación de código. Estas clases violan el principio de responsabilidad
única (véase la Receta 4.7, "Reificar las validaciones de cadenas"). Creas
clases para cumplir responsabilidades; si un objeto hace demasiadas
cosas, podría cambiar en distintas direcciones y deberías extraer otra
clase.
En el siguiente ejemplo, el objeto Webpage tiene demasiadas
responsabilidades:
class Webpage {
renderHTML() {
[Link]();
[Link]();
[Link]();
[Link]();
[Link]();
[Link]();
}
// RSS format might change
}
Esto es lo que parece si divides las responsabilidades:
class Webpage {
renderHTML() {
[Link]();
[Link]();
(new RSSFeed()).render();
}
// HTML render can change
}
class RSSFeed {
render() {
[Link]();
[Link]();
[Link]();
// ...
}
// RSS format might change
// Might have unitary tests
// etc.
}
Puedes detectar automáticamente las clases grandes o hacer un
seguimiento de los cambios. Las clases deben seguir el principio de
responsabilidad única (véase la Receta 4.7, "Reificar las validaciones de
cadenas") y tener una sola razón para cambiar. Si evolucionan de
distintas formas, están haciendo demasiado.
Recetas relacionadas
Receta 11.5, "Eliminar el exceso de métodos"
Receta 11.6, "Romper demasiados atributos"
Receta 11.7, "Reducir las listas de importación "
Receta 17.3, "Romper objetos de Dios"
Ver también
"Cambio divergente" en Refactoring Guru
17.5 Convertir valores de indicadores
especiales 9999 en normales
Problema
Utilizas constantes como Maxint para marcar los ID no válidos, pensando
que nunca llegarás a ella.
Solución
No juntes identificadores reales con otros no válidos.
Debate
Los ID no válidos representados con otros válidos violan el principio de
bijección; podrías llegar al ID no válido antes de lo que crees. Tampoco
utilices nulos para los ID no válidos, ya que provocan banderas de
acoplamiento del llamante a las funciones. Debes modelar los casos
especiales con objetos polimórficos especiales (véase la Receta 14.14,
"Convertir funciones no polimórficas en polimórficas") y evitar 9999, -1,
y 0 ya que son objetos de dominio válidos y producen acoplamiento de
implementación. En los primeros tiempos de la informática, los tipos de
datos eran estrictos. Luego inventaron "el error del billón de dólares"
(null-ver Capítulo 15) y crecieron y modelaron escenarios especiales con
valores especiales.
El siguiente ejemplo utiliza un número de bandera especial (9999):
#define INVALID_VALUE 9999
int main(void)
{
int id = get_value();
if (id == INVALID_VALUE)
{
return EXIT_FAILURE;
// id is a flag and also a valid domain value
}
return id;
}
int get_value()
{
// something bad happened
return INVALID_VALUE;
}
// returns EXIT_FAILURE (1)
Éste es el aspecto que tiene cuando eliminas la necesidad de un valor
válido especial (9999) y lo sustituyes por un valor no válido (-1):
int main(void)
{
int id = get_value();
if (id < 0)
{
printf("Error: Failed to obtain value\n");
return EXIT_FAILURE;
}
return id;
}
int get_value()
{
// something bad happened
return -1; // Return a negative value to indicate error
}
Mejor aún si tu lenguaje admite excepciones (consulta el Capítulo 22,
"Excepciones"):
// No INVALID_VALUE defined
int main(void)
{
try {
int id = get_value();
return id;
} catch (const char* error) {
printf("%s\n", error);
return EXIT_FAILURE;
}
}
int get_value()
{
// something bad happened
throw "Error: Failed to obtain value";
}
// returns EXIT_FAILURE (1)
Los identificadores externos suelen asignarse a números o cadenas. Pero
si falta el identificador externo, no intentes utilizar un número o una
cadena como referencia (no válida).
Recetas relacionadas
Receta 15.1, "Crear objetos nulos"
Receta 25.2, "Cambiar los identificadores secuenciales"
Receta 15.5, "Representar ubicaciones desconocidas sin utilizar nulos"
17.6 Quitar la escopeta quirúrgica
Problema
Un solo cambio funcional de conlleva múltiples cambios de código en tu
software.
Solución
Aísla los cambios y sigue el principio de no repetirse (DRY) (consulta
la Receta 4.7, "Reificar las validaciones de cadenas").
CIRUGÍA DE ESCOPETA
La cirugía escopeta describe una situación en la que un único cambio en el
código base requiere múltiples cambios en diferentes partes del sistema. Ocurre
cuando los cambios en una parte de la base de código afectan a muchas otras
partes del sistema. Es análogo a disparar una escopeta: una sola ráfaga puede
alcanzar varios objetivos a la vez, igual que un solo cambio de código puede
afectar a varias partes del sistema.
Debate
Si tienes una mala asignación de responsabilidades o duplicación de
código, puede que tengas que hacer demasiadas modificaciones en tu
modelo cuando cambie algún comportamiento del mundo real. Esto es
señal de una mala biyección, como se define en el Capítulo 2. Suele
ocurrir cuando has copiado y pegado código en todo el sistema.
Imagina que necesitas hacer algunos cambios en el siguiente código:
final class SocialNetwork {
function postStatus(string $newStatus) {
if (!$user->isLogged()) {
throw new Exception('User is not logged');
}
// ...
}
function uploadProfilePicture(Picture $newPicture) {
if (!$user->isLogged()) {
throw new Exception('User is not logged');
}
// ...
}
function sendMessage(User $recipient, Message $messageSend) {
if (!$user->isLogged()) {
throw new Exception('User is not logged');
}
// ...
}
}
Esto es lo que parece cuando extraes la lógica a un solo lugar:
final class SocialNetwork {
function postStatus(string $newStatus) {
$this->assertUserIsLogged();
// ...
}
function uploadProfilePicture(Picture $newPicture) {
$this->assertUserIsLogged();
// ...
}
function sendMessage(User $recipient, Message $messageSend) {
$this->assertUserIsLogged();
// ...
}
function assertUserIsLogged() {
if (!$this->user->isLogged()) {
throw new Exception('User is not logged');
// This is just a simplification
// Operations should be defined as objects with
preconditions etc.
}
}
}
Algunos linters modernos, así como las herramientas de aprendizaje
automático generativo, pueden detectar patrones repetidos (no sólo
código repetido), y además, al realizar tus revisiones de código, puedes
detectar fácilmente este problema y pedir una refactorización. Añadir
una nueva función debería ser sencillo si tu modelo se corresponde 1:1
con el mundo real y tus responsabilidades están en los lugares correctos.
Debes estar alerta ante pequeños cambios que abarquen varias clases.
17.7 Eliminar argumentos opcionales
Problema
Tienes argumentos opcionales en tus funciones.
Solución
Los argumentos opcionales generan un acoplamiento oculto en aras de un
código más compacto.
Debate
Cuando tienes argumentos opcionales, acoplas el método de llamada a
valores opcionales accidentales, generando resultados inesperados,
efectos secundarios y efectos dominó. En lenguajes con argumentos
opcionales pero limitados a tipos básicos, tienes que establecer una
bandera y añadir un if accidental. Como solución, tienes que hacer
explícitos los argumentos y utilizarparámetros con nombre si tu lenguaje
los admite.
Aquí hay una política de validación opcional:
final class Poll {
function _construct(
array $questions,
bool $annonymousAllowed = false,
$validationPolicy = 'Normal') {
if ($validationPolicy == 'Normal') {
$validationPolicy = new NormalValidationPolicy();
}
// ...
}
}
// Valid
new Poll([]);
new Poll([], true);
new Poll([], true , new NormalValidationPolicy());
new Poll([], , new StrictValidationPolicy());
Cuando haces explícito el argumento no hay supuestos ocultos:
final class Poll {
function _construct(
array $questions,
AnonyomousStrategy $anonymousStrategy,
ValidationPolicy $validationPolicy) {
// ...
}
}
// invalid
new Poll([]);
new Poll([], new AnonyomousInvalidStrategy());
new Poll([], , new StrictValidationPolicy());
// Valid
new Poll([], new AnonyomousInvalidStrategy(), new
StrictValidationPolicy());
La detección es fácil si el lenguaje admite argumentos opcionales. Siempre
hay que ser explícito y favorecer la legibilidad frente a las llamadas a
funciones más cortas (y más acopladas).
Recetas relacionadas
Receta 17.10, "Mover los argumentos por defecto al final"
Receta 21.3, "Eliminar la advertencia/desactivación estricta"
17.8 Evitar la envidia de funciones
Problema
Tienes un objeto que utiliza demasiados métodos de otro objeto.
Solución
Rompe la dependencia y refactoriza el comportamiento.
ENVIDIA DE CARACTERÍSTICAS
La envidia de características se produce cuando un objeto está más interesado
en el comportamiento de otro objeto que en el suyo propio, utilizando
excesivamente los métodos de otro objeto.
Debate
La envidia de características crea una dependencia y un acoplamiento
significativos, perjudicando la reutilización del código y la
comprobabilidad. Suele ser un síntoma de una mala asignación de
responsabilidades. Tienes que encontrar las responsabilidades
mediante MAPPER y trasladar los métodos a la clase adecuada.
Este candidato define cómo imprimir su dirección:
class Candidate {
void printJobAddress(Job job) {
[Link]("This is your position address");
[Link]([Link]().street());
[Link]([Link]().city());
[Link]([Link]().ZipCode());
}
}
La responsabilidad de la impresión corresponde al trabajo relacionado:
class Job {
void printAddress() {
[Link]("This is your job position address");
[Link]([Link]().street());
[Link]([Link]().city());
[Link]([Link]().ZipCode());
// You might even move this responsibility directly to the
address!
// Some address information is relevant to a job for package
tracking
}
}
class Candidate {
void printJobAddress(Job job) {
[Link]();
}
}
Aquí tienes otro ejemplo en el que el área del rectángulo se calcula con
una fórmula externa:
function area(rectangle) {
return [Link] * [Link];
// Notice you are sending consecutive messages to
// the same object and doing calculations
}
Esto es lo que parece utilizando la separación de intereses:
class Rectangle {
constructor(width, height) {
[Link] = height;
[Link] = width;
}
area() {
return [Link] * [Link];
}
}
Algunos linters pueden detectar un patrón secuencial de colaboraciones
con otro objeto.
Recetas relacionadas
Receta 3.1, "Convertir objetos anémicos en objetos ricos"
Receta 6.5, "Cambiar las responsabilidades equivocadas"
Receta 14.15, "Cambiar la comparación de igualdad"
Receta 17.16, "Romper la intimidad inapropiada"
17.9 Eliminar al intermediario
Problema
En tienes un objeto intermediario que crea una indirección innecesaria.
Solución
Retira el intermediario.
Debate
Los objetos intermediarios rompen la ley de Demeter (ver Receta 3.8,
"Eliminar Getters"), añadiendo complejidad e indirección innecesaria y
creando clases vacías. Deberías eliminarlos. He aquí un ejemplo en el que
el cliente tiene una dirección (que a su vez tiene un código postal). La
clase Address es un modelo anémico y su única responsabilidad es
devolver el código postal:
public class Client {
Address address;
public ZipCode zipCode() {
return [Link]();
}
}
public class Address {
// Middleman
private ZipCode zipCode;
public ZipCode zipCode() {
return new ZipCode('CA90210');
}
}
public class Application {
ZipCode zipCode = [Link]();
}
Aquí el cliente expone la dirección:
public class Client {
public ZipCode zipCode() {
// Can also store it
return new ZipCode('CA90210');
}
}
public class Application {
ZipCode zipCode = [Link]();
}
De forma similar a su opuesto(Receta 10.6, "Romper cadenas largas de
colaboraciones"), puedes detectar este olor utilizando árboles de análisis
sintáctico.
Recetas relacionadas
Receta 10.6, "Romper largas cadenas de colaboraciones"
Receta 10.9, "Eliminar objetos poltergeist"
Receta 19.9, "Migrar clases vacías"
Ver también
"Eliminar intermediarios" en [Link]
"Middle Man" en C2 Wiki
"Eliminar intermediarios" en JetBrains
17.10 Mover los argumentos por
defecto al final
Problema
Tienes argumentos por defecto en medio de la lista de argumentos.
Solución
Las firmas de las funciones no deben dar lugar a errores. Intenta no
utilizar argumentos por defecto (véase la Receta 17.7, "Eliminar
argumentos opcionales"), pero si realmente los necesitas, no utilices
argumentos opcionales antes de los obligatorios.
Debate
Los argumentos por defecto pueden fallar inesperadamente, violando el
principio de fail fast. También perjudican la legibilidad, ya que tienes que
pensar mucho en la ordenación de los parámetros. Tienes que colocar tus
argumentos opcionales en último lugar o eliminarlos utilizando la Receta
17.7 relacionada , "Eliminar argumentos opcionales".
Aquí puedes ver el color opcional antes del modelo:
function buildCar($color = "red", $model) {
//...
}
// First argument with optional argument
buildCar("Volvo");
// Runtime error: Too few arguments to function buildCar()
Moviéndolo a la última posición evitas el problema:
function buildCar($model, $color = "Red", ){...}
buildCar("Volvo");
// Works as expected
def functionWithLastOptional(a, b, c='foo'):
print(a)
print(b)
print(c)
functionWithLastOptional(1, 2) // Prints 1, 2, foo
def functionWithMiddleOptional(a, b='foo', c):
print(a)
print(b)
print(c)
functionWithMiddleOptional(1, 2)
# SyntaxError: non-default argument follows default argument
Muchos linters pueden aplicar esta regla, ya que puedes derivarla de la
firma de la función. Además, muchos compiladores la prohíben
directamente. Intenta ser estricto al definir las funciones para evitar el
acoplamiento entre la persona que llama y el valor opcional definido por
el método.
Recetas relacionadas
Receta 9.5, "Unificar el orden de los parámetros"
Receta 17.7, "Eliminar argumentos opcionales"
Ver también
"Los argumentos de método con valores por defecto deben ser los
últimos" en Sonar Source
17.11 Evitar el efecto dominó
Problema
Tú haces pequeñosl cambios en tu código y notas demasiados problemas
inesperados.
Solución
Si los pequeños cambios tienen un gran impacto, necesitas desacoplar tu
sistema.
Debate
El efecto dominó es un problema que puedes abordar utilizando muchas
de las recetas de este libro. Para evitarlo, siempre tienes que desacoplar
los cambios que cubren la funcionalidad existente con pruebas y luego
refactorizar y aislar lo que está cambiando.
He aquí un objeto popular acoplado a una implementación accidental
para obtener la hora actual:
class Time {
constructor(hour, minute, seconds) {
[Link] = hour;
[Link] = minute;
[Link] = seconds;
}
now() {
// call operating system
}
}
// Adding a time zone will have a big ripple effect
// Changing now() to consider time zone will also create the effect
Este es el aspecto cuando eliminas el método now:
class Time {
constructor(hour, minute, seconds, timezone) {
[Link] = hour;
[Link] = minute;
[Link] = seconds;
[Link] = timezone;
}
// Removed now() since it is invalid without context
}
class RelativeClock {
constructor(timezone) {
[Link] = timezone;
}
now(timezone) {
var localSystemTime = [Link]();
var localSystemTimezone = [Link]();
// Do some math translating time zones
// ...
return new Time(..., timezone);
}
}
No es fácil detectar los problemas antes de que se produzcan. Las pruebas
de mutación (ver Receta 5.1, "Cambiar var por const") y el análisis de la
causa raíz de los puntos únicos de fallo pueden ayudar. Existen múltiples
estrategias para hacer frente a los sistemas heredados y acoplados. Debes
enfrentarte a este problema antes de que te explote en la cara.
PUNTO ÚNICO DE FALLO
Un punto único de fallo se refiere a un componente o parte de un sistema que, si
fallara, haría que todo el sistema fallara o dejara de estar disponible. El sistema
depende de este componente o parte, y sin él, nada puede funcionar
correctamente. Los buenos diseños intentan tener componentes redundantes
para evitar este efecto dominó.
Recetas relacionadas
Receta 10.6, "Romper largas cadenas de colaboraciones"
17.12 Eliminar métodos accidentales en
objetos de negocio
Problema
Tienes código de persistencia, serialización, visualización, importación,
registro o exportación en tus objetos de dominio.
Solución
Elimina todos los métodos accidentales. Pertenecen a dominios diferentes,
por lo que deberían estar en objetos diferentes.
Debate
Acoplar objetos a problemas accidentales hace que el software sea menos
mantenible y menos legible, por lo que no debes mezclar
comportamientos accidentales y esenciales. Debes desacoplar los objetos
de negocio. Separa los problemas accidentales: traslada la persistencia, el
formato y la serialización a objetos especiales y mantén el protocolo
esencial utilizando la bijección.
Aquí tienes un car hinchado de protocolo accidental. Los coches y los
accidentes deben estar siempre separados:
class car:
def __init__(self, company, color, engine):
self._company = company
self._color = color
self._engine = engine
def goTo(self, coordinate):
[Link](coordinate)
def startEngine(self):
## code to start engine
[Link]()
def display(self):
## displaying is accidental
print ('This is a', self._color, [Link])
def to_json(self):
## serializing is accidental
return "json"
def update_on_database(self):
## persistence is accidental
[Link](this)
def get_id(self):
## identifiers are accidental
return [Link];
def from_row(self, row):
## persistence is accidental
return [Link](row);
def forkCar(self):
## concurrency is accidental
ConcurrencySemaphoreSingleton.get_instance().fork_cr(this)
Esto es lo que parece cuando despojas a los métodos:
class car:
def __init__(self,company,color,engine):
self._company = company
self._color = color
self._engine = engine
def goTo(self, coordinate):
[Link](coordinate)
def startEngine(self):
## code to start engine
self._engine.start()
Es difícil (pero no imposible) crear reglas de linting basadas en la
nomenclatura que proporcionen pistas sobre nombres sospechosos. Como
excepciones, algunos frameworks te obligan a inyectar código sucio en
objetos, por ejemplo, identificadores. Puede que estés muy acostumbrado
a ver objetos comerciales contaminados. Esto es normal. Debes
reconsiderar las consecuencias y el acoplamiento de estos diseños.
17.13 Eliminar el Código de Negocio de
la Interfaz de Usuario
Problema
Tienes validaciones de entrada en tu interfaz de usuario.
Solución
Siempre debes crear objetos adecuados en tus backends y trasladar las
validaciones a tus objetos de dominio.
Debate
Las UI son accidentales. Validar las reglas de negocio en la interfaz de
usuario conlleva problemas de seguridad y una posible duplicación de
código. Este tipo de acoplamiento daña la comprobabilidad
y extensibilidad de las API, microservicios, etc., haciendo que los objetos
de negocio sean anémicos y mutables. La duplicación de código es un
aviso de optimización prematura. Construir un sistema con validaciones
de interfaz de usuario puede evolucionar hacia el consumo de una API o
componente externo; necesitas validar objetos en el backend y enviar
buenos mensajes de validación a los componentes cliente.
Aquí tienes un ejemplo de validaciones en la IU:
<script type="text/javascript">
function checkForm(form)
{
if([Link] == "") {
alert("Error: Username cannot be blank!");
[Link]();
return false;
}
re = /^\w+$/;
if() {
alert("Error: Username must contain only letters,"
+ " numbers and underscores!");
[Link]();
return false;
}
if([Link] != "" && [Link] == [Link]) {
if([Link] < 8) {
alert("Error: Password must contain at least eight
characters!");
[Link]();
return false;
}
if([Link] == [Link]) {
alert("Error: Password must be different from Username!");
[Link]();
return false;
}
re = /[0-9]/;
if() {
alert("Error: password must contain at least one number (0-
9)!");
[Link]();
return false;
}
re = /[a-z]/;
if() {
alert("Error: password must contain at least"
+ " one lowercase letter (a-z)!");
[Link]();
return false;
}
re = /[A-Z]/;
if() {
alert("Error: password must contain at least"
+ " one uppercase letter (A-Z)!");
[Link]();
return false;
}
} else {
alert("Error: Please check that you've entered"
+" and confirmed your password!");
[Link]();
return false;
}
alert("You entered a valid password: " + [Link]);
return true;
}
</script>
<form ... onsubmit="return checkForm(this);">
<p>Username: <input type="text" name="username"></p>
<p>Password: <input type="password" name="pwd1"></p>
<p>Confirm Password: <input type="password" name="pwd2"></p>
<p><input type="submit"></p>
</form>
Este es el aspecto después de mover el código de validación a tus objetos
de negocio:
<script type="text/javascript">
// Send a post to a backend
// Backend has domain rules
// Backend has test coverage and rich models
// It is more difficult to inject code in a backend
// Validations will evolve on your backend
// Business rules and validations are shared with every consumer
// UI / REST / Tests / Microservices ... etc. etc.
// No duplicated code
function checkForm(form)
{
const url = "[Link]
const data = { };
const other_params = {
headers : { "content-type" : "application/json; charset=UTF-
8" },
body : [Link](data),
method : "POST",
mode : "cors"
};
fetch(url, other_params)
.then(function(response) {
if ([Link]) {
return [Link]();
} else {
throw new Error("Could not reach the API: " +
[Link]);
}
}).then(function(data) {
[Link]("message").innerHTML =
[Link];
}).catch(function(error) {
[Link]("message").innerHTML =
[Link];
});
return true;
}
</script>
Como excepciones notables, si tienes pruebas fehacientes de cuellos de
botella graves en el rendimiento, necesitas duplicar automáticamente tu
lógica empresarial en el frontend; no puedes saltarte la parte del backend.
No debes hacerlo manualmente porque te olvidarás de hacerlo. Utiliza
TDD (ver Receta 4.8, "Eliminar propiedades innecesarias"), donde pones
todo el comportamiento de tu lógica de negocio en tus objetos de dominio.
Recetas relacionadas
Receta 3.1, "Convertir objetos anémicos en objetos ricos"
Receta 3.6, "Eliminar DTOs"
Receta 6.13, "Evitar el infierno de las devoluciones de llamada"
Receta 6.14, "Generar buenos mensajes de error"
Receta 16.8, "Eliminar eventos de devolución de llamada basados en la
implementación"
17.14 Cambiar el acoplamiento a las
clases
Problema
En tienes clases globales y las utilizas como puntos de entrada.
Solución
Rompe el acoplamiento y utiliza objetos como interfaces en lugar de
clases, ya que son más fácilmente sustituibles.
Debate
Las clases globales crean acoplamiento e impiden la extensibilidad;
además, son difíciles de simular (véase la Receta 20.4, "Sustituir
simulaciones por objetos reales"). Puedes utilizar interfaces o rasgos (si
están disponibles) e inversión de dependencias (consulta la Receta 12.4,
"Eliminar interfaces de un solo uso") para favorecer el acoplamiento laxo.
Aquí tienes el acoplamiento:
public class MyCollection {
public bool HasNext { get; set;} // implementation details
public object Next(); // implementation details
}
public class MyDomainObject sum(MyCollection
anObjectThatCanBeIterated) {
// Tight coupling
}
// We cannot fake or mock this method
// since it always expects an instance of MyCollection
Esto es lo que parece cuando te burlas de él:
public interface Iterator {
public bool HasNext { get; set;}
public object Next();
}
public Iterator Reverse(Iterator iterator) {
var list = new List<int>();
while ([Link]) {
[Link](0, [Link]());
}
return new ListIterator(list);
}
public class MyCollection implements Iterator {
public bool HasNext { get; set;} // Implementation details
public object Next(); // Implementation details
}
public class myDomainObject {
public int sum(Iterator anObjectThatCanBeIterated) {
// Loose coupling
}
}
// Can use any Iterator (even a mocked one as long as it adheres to
the protocol)
Puedes utilizar casi cualquier linter para encontrar referencias a clases.
No debes abusar de él, ya que muchos usos legítimos pueden dar lugar a
falsos positivos. Las dependencias a interfaces hacen que un sistema esté
menos acoplado y, por tanto, sea más extensible y comprobable. Las
interfaces cambian con menos frecuencia que las implementaciones
concretas. Algunos objetos implementan muchas interfaces, y declarar
qué parte depende de qué interfaz hace que el acoplamiento sea más
granular y el objeto más cohesionado.
Recetas relacionadas
Receta 20.4, "Sustituir Mocks por Objetos Reales"
ACOPLAMIENTO SUELTO
El acoplamiento débil pretende minimizar la interdependencia de los distintos
objetos de un sistema. Tienen un conocimiento mínimo unos de otros, y los
cambios realizados en un componente no afectan a otros componentes del
sistema, evitando un efecto dominó.
17.15 Refactorizar cúmulos de datos
Problema
Tienes algunos objetos que siempre están juntos.
Solución
Crea objetos primitivos cohesionados; sus partes siempre viajarán juntas.
Debate
GRUPO DE DATOS
En los grupos de datos, el mismo grupo de objetos pasa con frecuencia de una
parte a otra de un programa. Esto puede aumentar la complejidad, reducir la
capacidad de mantenimiento y aumentar el riesgo de errores. Los cúmulos de
datos suelen producirse cuando intentas pasar objetos relacionados sin
encontrar un objeto adecuado que represente esa relación en la bijección.
Los cúmulos de datos tienen mala cohesión, obsesión primitiva y mucho
código duplicado; duplicas la validación compleja en varios sitios,
perjudicando la legibilidad y la mantenibilidad. Puedes aplicar la
refactorización "Extraer clase" y la Receta 4.1, "Crear objetos pequeños". Si
dos o más objetos primitivos están pegados, con lógica de negocio y reglas
repetidas entre ellos, necesitas encontrar el concepto existente de la
biyección.
Aquí tienes un ejemplo de grupo de datos:
public class DinnerTable
{
public DinnerTable(Person guest, DateTime from, DateTime to)
{
Guest = guest;
From = from;
To = to;
}
private Person Guest;
private DateTime From;
private DateTime To;
}
Así es como queda después de reificarlo:
public class TimeInterval
{
public TimeInterval(DateTime from, DateTime to)
{
if (from >= to)
{
throw new ArgumentException
("Invalid time interval: 'from' must be earlier
than 'to'.");
}
From = from;
To = to;
}
}
public class DinnerTable
{
public DinnerTable(Person guest, DateTime from, DateTime to)
{
Guest = guest;
Interval = new TimeInterval(from, to);
}
}
Aquí tienes una versión aún mejor y más compacta en la que pasas
directamente el intervalo:
public DinnerTable(Person guest, Interval reservationTime)
{
Guest = guest;
Interval = reservationTime;
}
Debes agrupar el comportamiento en el lugar adecuado y ocultar los datos
primitivos.
Recetas relacionadas
Receta 3.1, "Convertir objetos anémicos en objetos ricos"
Receta 4.2, "Reificar datos primitivos"
Receta 4.3, "Reificar matrices asociativas"
17.16 Romper la intimidad inapropiada
Problema
Tú tienes dos clases con demasiada interdependencia.
Solución
Rompe las clases.
Debate
INTIMIDAD INAPROPIADA
La intimidad inadecuada se produce cuando dos clases o componentes se han
vuelto excesivamente dependientes entre sí, creando un acoplamiento estrecho
que hace que el código sea difícil de mantener, modificar o ampliar.
Cuando dos clases interactúan demasiado entre sí, están demasiado
acopladas. Esto es un signo de asignación incorrecta de responsabilidades
y de baja cohesión, lo que dificulta la mantenibilidad y la extensibilidad.
Aquí tienes dos clases entrelazadas:
class Candidate {
void printJobAddress(Job job) {
[Link]("This is your position address");
[Link]([Link]().street());
[Link]([Link]().city());
[Link]([Link]().zipCode());
if ([Link]().country() == [Link]()) {
[Link]("It is a local job");
}
}
Esto es lo que parece cuando rompes el acoplamiento:
final class Address {
void print() {
[Link]([Link]);
[Link]([Link]);
[Link]([Link]);
}
bool isInCounty(Country country) {
return [Link] == country;
}
class Job {
void printAddress() {
[Link]("This is your position address");
[Link]().print());
if ([Link]().isInCountry([Link]()) {
[Link]("It is a local job");
}
}
}
class Candidate {
void printJobAddress(Job job) {
[Link]();
}
}
Algunos linters calculan las relaciones de clase del grafo y la dependencia
del protocolo. Analizando el grafo de colaboración, puedes deducir reglas
y sugerencias. Si dos clases están demasiado relacionadas y no se hablan
mucho entre sí, puede que tengas que dividirlas, fusionarlas o
refactorizarlas. Las clases deben saber lo menos posible las unas de las
otras.
Recetas relacionadas
Receta 17.8, "Evitar la envidia de funciones"
Ver también
"Intimidad inapropiada" en C2 Wiki
17.17 Convertir objetos fungibles
Problema
Tú distingues dos objetos cuando tu modelo parcial del mundo real no lo
requiere.
Solución
Respeta el MAPPER y haz que lo que es fungible en el mundo real sea
fungible en tu modelo.
Debate
OBJETOS FUNGIBLES
Los objetos fungibles son intercambiables o idénticos en valor, calidad y
características. Cualquier instancia particular de un objeto fungible puede
sustituirse por cualquier otra instancia del mismo objeto sin pérdida de valor o
calidad. La fungibilidad es la propiedad de un bien o mercancía cuyas unidades
individuales son esencialmente intercambiables y cada una de cuyas partes es
indistinguible de cualquier otra.
Puede que hayas oído hablar mucho de las NFT. Vamos a utilizar este
concepto de fungibilidad en tu código. La fungibilidad es una propiedad
que debes imponer a la biyección. Un objeto debe ser fungible en tu
modelo si lo es en el mundo real. Tienes que identificar los elementos
fungibles en tus dominios y modelarlos como intercambiables. En
software, puedes sustituir los objetos fungibles por otros. Al mapear tus
objetos con los reales, es fácil olvidarse del modelo parcial y sobrediseñar.
Aquí tienes un no fungible Person:
public class Person implements Serializable {
private final String firstName;
private final String lastName;
public Person(String firstName, String lastName) {
[Link] = firstName;
[Link] = lastName;
}
}
[Link](new Person('John', 'Doe'));
No es importante modelar la Person en este contexto:
public class Person {
}
[Link](new Person());
// The identity is irrelevant for queue simulation
Necesitas entender el modelo para comprobar si es correcto o no. Crea
objetos fungibles para lo que sea realmente fungible. Esto parece fácil,
pero requiere habilidades de diseño.
Recetas relacionadas
Receta 4.8, "Eliminar propiedades innecesarias"
Capítulo 18. Globales
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y
comentarios: translation-feedback@[Link]
Escribe módulos de código tímido que no revelen nada innecesario a otros
módulos y que no dependan de las implementaciones de otros módulos.
David Thomas y Andrew Hunt, El programador pragmático: Tu viaje
hacia la maestría
18.0 Introducción
La mayoría de los lenguajes modernos admiten funciones, clases y
atributos globales. Hay un coste oculto cuando utilizas cualquiera de estos
artefactos. Incluso cuando crees un objeto utilizando new(), introducirás
un acoplamiento estrecho a una clase global a menos que apliques alguna
de las siguientes recetas.
18.1 Reificar las funciones globales
Problema
Tienes funciones globales que puedes llamar desde cualquier sitio.
Solución
Las funciones globales conllevan mucho acoplamiento. Limita su alcance.
Debate
Desalentados por la programación orientada a objetos, muchos lenguajes
mixtos admiten funciones globales. Crean acoplamiento y perjudican la
legibilidad, ya que son difíciles de rastrear. A medida que aumenta el
acoplamiento, la mantenibilidad y la comprobabilidad se vuelven más
problemáticas. Puedes empezar envolviendo la función en un objeto de
contexto. Por ejemplo, puedes encontrar acceso a recursos externos,
acceso a bases de datos, singletons (ver Receta 17.2, "Sustitución de
singletons"), clases globales, tiempo y recursos del sistema operativo.
Este ejemplo llama a un método de una base de datos global :
class Employee {
function taxesPayedUntilToday() {
return database()->select(
"SELECT TAXES FROM EMPLOYEE".
" WHERE ID = " . $this->id() .
" AND DATE < " . currentDate());
}
}
Al hacer que la persistencia sea contextual, puedes desacoplar la base de
datos de la lógica de cálculo:
final class EmployeeTaxesCalculator {
function taxesPayedUntilToday($context) {
return $context->selectTaxesForEmployeeUntil(
$this->socialSecurityNumber,
$context->currentDate());
}
}
Muchos lenguajes modernos evitan las funciones globales. Para las
permisivas, se pueden aplicar reglas de ámbito y comprobarlas
automáticamente. La programación estructurada considera perjudiciales
las funciones globales. Sin embargo, puedes observar que algunas malas
prácticas traspasan las fronteras de los paradigmas.
Recetas relacionadas
Receta 5.7, "Eliminar los efectos secundarios"
Receta 18.4, "Eliminar clases globales"
18.2 Reificar las funciones estáticas
Problema
Tienes funciones estáticas que están acopladas a clases.
Solución
No utilices funciones estáticas. Son globales y utilitarias. En su lugar, crea
métodos de instancia y habla con los objetos.
Debate
FUNCIONES ESTÁTICAS
Una función estática pertenece a una clase y no a una instancia de esa clase.
Esto significa que se puede llamar a un método estático sin crear un objeto de la
clase.
Las clases mapean conceptos (o ideas) del mundo real, y las funciones
estáticas violan el principio de biyección. Las funciones estáticas también
provocan acoplamiento. Hacen que el código sea más difícil de probar, ya
que las clases son más difíciles de burlar (ver Receta 20.4, "Sustituir burlas
por objetos reales") que las instancias. La única responsabilidad de una
clase (ver Receta 4.7, "Reificar validaciones de cadenas") es crear
instancias. Puedes delegar métodos en instancias o crear objetos sin
estado, siguiendo las responsabilidades del mundo real. No los llames
ayudantes o utilidades.
Aquí tienes una clase con formato de método estático:
class DateStringHelper {
static format(date) {
return [Link]('yyyy-MM-dd');
}
}
[Link](new Date());
Puedes atraer la responsabilidad hacia un objeto concreto:
class DateToStringFormatter {
constructor(date) {
[Link] = date;
}
englishFormat() {
return [Link]('yyyy-MM-dd');
}
}
new DateToStringFormatter(new Date()).englishFormat();
Puedes aplicar una política para evitar los métodos estáticos (todos los
métodos de clase excepto los constructores). Las clases son globales
disfrazadas. Contaminar su protocolo con métodos de
utilidades/ayudantes/bibliotecas rompe la cohesión y genera
acoplamiento. Debes extraer las funciones estáticas con refactorizaciones.
En la mayoría de los lenguajes no puedes manipular las clases y utilizarlas
polimórficamente, por lo que no puedes burlarlas (véase la Receta 20.4,
"Sustituir burlas por objetos reales") ni introducirlas en las pruebas. Por
tanto, tienes una referencia global demasiado difícil de desacoplar.
Recetas relacionadas
Receta 7.2, "Renombrar y romper ayudantes y utilidades"
Receta 20.1, "Probar métodos privados"
18.3 Sustituir GoTo por código
estructurado
Problema
Tienes frases GoTo en tu código.
Solución
No utilices nunca GoTo. Estructura tu código sin saltos globales o locales.
Debate
GoTo se considera perjudicial desde hace al menos 50 años. La sentencia
perjudica la legibilidad y hace que el código sea difícil de seguir. Puedes
utilizar código estructurado en su lugar y utilizar excepciones si es
necesario. Se abusó mucho de los GoTo en lenguajes populares como
BASIC hace varias décadas, y los lenguajes de bajo nivel los utilizan como
control de salto.
Aquí tienes una función con una instancia GoTo:
int i = 0;
start:
if (i < 10)
{
[Link](i);
i++;
goto start;
}
Aquí tienes el mismo algoritmo escrito de forma más estructurada:
for (int i = 0; i < 10; i++)
{
[Link](i);
}
Los problemas de GoTo se reconocieron hace décadas. Pero el problema
sigue presente en lenguajes modernos como GoLang, PHP, Perl, C#, etc.
Por suerte, la mayoría de los programadores evitan las sentencias GoTo
utilizando la programación estructurada.
Recetas relacionadas
Receta 15.1, "Crear objetos nulos"
Ver también
"Declaración Go To Considerada Perjudicial" por Edgar Dijkstra
18.4 Eliminar clases globales
Problema
Utiliza las clases como punto de acceso global.
Soluciones
No utilices tus clases como punto de acceso global. Utiliza en su lugar
objetos especializados.
Debate
Las clases son globales o semiglobales si están delimitadas por espacios de
nombres, paquetes o módulos. Como ocurre con otros globales, el mayor
problema es el acoplamiento. Contaminan el espacio de nombres si tienes
muchas juntas. También son una vía libre para los métodos estáticos, las
constantes estáticas y los singletons(ver Receta 17.2, "Sustitución de
Singletons"). Puedes utilizar espacios de nombres, calificadores de
módulos o similares para evitar la contaminación por espacios de
nombres. Mantén siempre los nombres globales lo más cortos posible.
Recuerda que la única responsabilidad de una clase (ver Receta 4.7,
"Reificar las validaciones de cadenas") es crear instancias, no ser un punto
de acceso global.
ESPACIOS DE NOMBRES
Los espacios de nombres se utilizan para organizar elementos de código como
clases, funciones y variables en grupos lógicos, evitando conflictos de nombres y
proporcionando una forma de identificarlos de forma única dentro de un ámbito
concreto. Ayudan a crear código modular y fácil de mantener, agrupando
funciones relacionadas.
Aquí tienes un punto de acceso global con un método estático:
final class StringUtilHelper {
static function formatYYYYMMDD($dateToBeFormatted): string {
}
}
Puedes hacer scope de la clase y también trasladar el método estático a
una instancia:
namespace Dates;
final class DateFormatter {
// class DateFormatter is no longer global
public function formatYYYYMMDD(\DateTime $dateToBeFormatted):
string {
}
// function is not static since class single responsibility
// is to create instances and not be a library of utils
}
Esto es mucho mejor cuando conviertes el DateFormatter en un objeto
método:
namespace Dates;
final class DateFormatter {
private $date;
public function __construct(\DateTime $dateToBeFormatted) {
$this->date = $dateToBeFormatted;
}
public function formatYYYYMMDD(): string {
}
}
Al invocar la clase de ámbito, tendrás una relación clara entre
módulos/espacios de nombres:
use Dates\DateFormatter;
// Since DateFormatter is no longer global you need full qualification
// You can even solve name collisions
$date = new DateTime('2022-12-18');
$dateFormatter = new DateFormatter($date);
$formattedDate = $dateFormatter->formatYYYYMMDD();
Todos los singletons (ver Receta 17.2, "Sustitución de singletons") son
también puntos globales de acceso a :
class Singleton { }
final class DatabaseAccessor extends Singleton { }
Puedes acceder a la base de datos en un ámbito restringido sin invocar la
clase:
namespace OracleDatabase;
class DatabaseAccessor {
// Database is not a singleton and it is namespace scoped
}
Puedes utilizar casi cualquier linter o crear reglas de dependencia para
buscar malas referencias a clases. Debes restringir las clases a dominios
pequeños y exponer sólo fachadas al exterior. Esto reduce enormemente
el acoplamiento.
Recetas relacionadas
Receta 18.1, "Reificar funciones globales"
Receta 18.2, "Reificar funciones estáticas"
Receta 19.9, "Migrar clases vacías"
18.5 Modificar la creación de la fecha
global
Problema
utiliza new Date() en tu código.
Solución
Evita crear fechas vacías. Proporciona un contexto explícito que explique
cuál es tu fuente de tiempo.
Debate
Crear una fecha sin contexto crea acoplamiento y suposiciones ocultas en
sistemas globales. Muchos sistemas se ejecutan en un entorno de nube en
el que la zona horaria no siempre es explícita. He aquí un ejemplo clásico
de uso:
var today = new Date();
En su lugar, debes hacerlo explícito:
var ouagadougou = new Location();
var today = [Link](ouagadougou);
function testGivenAYearHasPassedAccruedInterestsAre10() {
var mockTime = new MockedDate(new Date(2021, 1, 1));
var domainSystem = new TimeSystem(mockTime);
// ..
[Link](new Date(2022, 1, 1));
// … You set up the yearly interest rate
assertEquals(10, [Link]());
}
Deberías prohibir las funciones globales en las políticas porque te obligan
a acoplarte a fuentes de tiempo enchufables accidentales
y . [Link]() [Link]() , y otras llamadas globales al sistema crean
acoplamiento. Puesto que las pruebas deben tener un control total del
entorno, deberías poder configurar fácilmente el tiempo, moverlo de un
lado a otro, etc.
Date y Time las clases sólo deben crear instancias inmutables. No es
responsabilidad suya dar el tiempo real. Esto viola el principio de
responsabilidad única (véase la Receta 4.7, "Reificar las validaciones de
cadenas"). El paso del tiempo siempre es despreciado por los
programadores. Esto hace que los objetos sean mutables y conduce a
diseños pobres y acoplados.
PRUEBAS DE CONTROL AMBIENTAL COMPLETO
Control total del entorno es la capacidad de tener un control total sobre el
entorno en el que se ejecutan las pruebas. Implica crear un entorno controlado y
predecible que permita que las pruebas se ejecuten de forma coherente e
independiente de factores externos. Debes tener especialmente en cuenta las
dependencias externas, la simulación de red, el aislamiento de la base de datos,
el control del tiempo y muchos otros.
Recetas relacionadas
Receta 4.5, "Reificar marcas de tiempo"
Receta 18.2, "Reificar funciones estáticas"
Capítulo 19. Jerarquías
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y
comentarios: translation-feedback@[Link]
Un aspecto de una clase era actuar como repositorio de código e
información compartida por todas las instancias (objetos) de esa clase. En
términos de eficiencia, es una buena idea porque se minimiza el espacio de
almacenamiento y los cambios pueden hacerse en un único lugar. Sin
embargo, resulta muy tentador utilizar este hecho para justificar la
creación de tu jerarquía de clases basada en código compartido en lugar de
comportamientos compartidos.. Crea siempre jerarquías basadas en
comportamientos compartidos.
David West, Pensamiento objetual
19.0 Introducción
La herencia de clases suele utilizarse erróneamente para la reutilización
del código debido a razones históricas. Deberías preferir la composición,
pero no es tan obvio y requiere más experiencia. La composición es
dinámica y puedes cambiarla fácilmente, probarla, reutilizarla, etc.,
dando flexibilidad a tus diseños. En este capítulo encontrarás recetas para
minimizar el acoplamiento accidental que añades al utilizar jerarquías.
19.1 Romper la herencia profunda
Problema
Tienes jerarquías profundas para reutilizar el código.
Solución
Encuentra el protocolo y aplana la jerarquía favoreciendo la composición
sobre la herencia.
Debate
La reutilización de la subclasificación estática crea más acoplamiento que
la reutilización de la composición dinámica. Las jerarquías profundas
tienen mala cohesión y clases base frágiles. Traen consigo la sustitución
de métodos y violan el principio de sustitución de Liskov (uno de los
fundamentos de SOLID). Tienes que romper las clases y componerlas. En
el pasado, algunos artículos y libros recomendaban utilizar clases como
especialización para reutilizar el código, pero la composición es una
forma más eficaz y extensible de compartir el comportamiento.
PRINCIPIO DE SUSTITUCIÓN DE LISKOV
El principio de sustitución de Liskov afirma que si una función o método está
diseñado para trabajar con objetos de una clase concreta, entonces también
debería funcionar con objetos de cualquier subclase de esa clase sin causar
ningún comportamiento inesperado. Es la "L" de SOLID (ver Receta 4.7, "Reificar
las validaciones de cadenas").
He aquí una jerarquía profunda que representa a las focas según la
clasificación científica:
class Animalia:
class Chordata(Animalia):
class Mammalia(Chordata):
class Carnivora(Mammalia):
class Pinnipedia(Carnivora):
class Phocidae(Pinnipedia):
class Halichoerus(Phocidae):
class GreySeal(Halichoerus):
Éste es su aspecto después de comprimirlos hasta descubrir el
comportamiento de cada clase en la jerarquía:
class GreySeal:
def eat(self): # find the common behavior in the hierarchy
def sleep(self): # find the common behavior in the hierarchy
def swim(self): # find the common behavior in the hierarchy
def breed(self): # find the common behavior in the hierarchy
La foca gris aparece en la portada de este libro. Muchos linters informan
de la profundidad del árbol de herencia (DIT). Puedes cuidar tus
jerarquías y romperlas a menudo. Aquí utilizas la clasificación por una
razón accidental (cómo cargas a los usuarios de tus servidores):
class Server:
@abstractmethod
def calculate_cost(self):
pass
class DedicatedServer(Server):
def calculate_cost(self):
# Example: cost based on CPU and RAM usage
return [Link] * 10 + [Link] * 5
class HourlyChargedServer(Server):
def calculate_cost(self):
# Example: cost based on CPU and RAM usage multiplied by
hours
return ([Link] * 5 + [Link] * 2) * [Link]
# Once the server is created you cannot dynamically change the
charging method
# If you create a new ChargingMethod it will impact your server
hierarchy
Aquí utilizas la composición para cambiar dinámicamente el método de
carga:
class Server:
def calculate_cost(self):
return [Link].calculate_cost([Link], [Link])
def change_charging_method(self, charging):
[Link] = charging
class ChargingMethod():
@abstractmethod
def calculate_cost(self, cpu, ram):
pass
class MonthlyCharging(Charging):
def calculate_cost(self, cpu, ram):
return cpu * 10 + ram * 5
class HourlyCharging(Charging):
def calculate_cost(self, cpu, ram):
return (cpu * 5 + ram * 2) * [Link]
# You can unit test the charging methods in isolation
# You can create new charging methods without impacting the servers
La composición te da más flexibilidad, capacidad de prueba y
reutilización, y también favorece el principio abierto-cerrado (véase la
Receta 14.3, "Reificar variables booleanas"), ya que no cambias tu
jerarquía de dominio y sólo cambias el objeto delegado.
COMPOSICIÓN
La composición permite que los objetos se compongan de otros objetos como
partes o componentes. Construyes objetos complejos combinando otros más
sencillos (consulta la Receta 4.1 , "Crear objetos pequeños"), formando
una relación"tiene-un" en lugar de las clásicas "es-un" o "se comporta-como-un"
(consulta la Receta 19.4, "Sustituir la relación "es-un" por el comportamiento").
Recetas relacionadas
Receta 19.2, "Romper las jerarquías Yo-Yo"
Receta 19.3, "Romper la subclasificación para reutilizar el código"
Receta 19.4, "Sustituir la relación "is-a" por un comportamiento"
Receta 19.7, "Hacer clases concretas finales"
Receta 19.11, "Eliminar atributos protegidos"
19.2 Romper las jerarquías Yo-Yo
Problema
Cuando buscas un método concreto implementación, tienes que ir de un
lado a otro, subiendo y bajando en la jerarquía como un yoyó.
Solución
No crees jerarquías profundas. Compáctalas.
Debate
EL PROBLEMA DEL YO-YO
El problema del yo-yo se produce cuando necesitas navegar por las clases y
métodos de una jerarquía de clases para comprender o modificar el código, lo
que dificulta el mantenimiento y la ampliación de la base de código.
Las jerarquías profundas crean subclasificaciones para la reutilización
del código y perjudican la legibilidad. Programar a través de pequeñas
diferencias hace que las clases sean menos cohesivas. Debes favorecer la
composición sobre la herencia y refactorizar las jerarquías profundas.
He aquí un ejemplo de jerarquía especializada:
abstract class Controller { }
class BaseController extends Controller { }
class SimpleController extends BaseController { }
class ControllerBase extends SimpleController { }
class LoggedController extends ControllerBase { }
class RealController extends LoggedController { }
Utilizando interfaces, favoreces la delegación y evitas el problema del yo-
yo:
interface ControllerInterface { }
abstract class Controller implements ControllerInterface { }
final class LoggedControllerDecorator implements ControllerInterface
{ }
final class RealController implements ControllerInterface { }
Cualquier linter puede comprobar los sospechosos según un umbral de
profundidad máxima. Muchos programadores novatos reutilizan el
código mediante jerarquías. Esto conlleva jerarquías altamente acopladas
y de baja cohesión. Johnson y Foote establecieron en su artículo de 1988 la
utilidad de esta receta. Los desarrolladores han aprendido mucho a partir
de ahí. Debes refactorizar y aplanar esas jerarquías.
Recetas relacionadas
Receta 19.1, "Romper la herencia profunda"
Receta 19.3, "Romper la subclasificación para reutilizar el código"
Ver también
"Diseño de clases reutilizables" de Ralph E. Johnson y Brian Foote
19.3 Romper la subclasificación para
reutilizar el código
Problema
Tienes una relación "is-a" y reutilizas el código mediante la
subclasificación.
Solución
Favorece la composición frente a la herencia. Rompe el protocolo
utilizando la delegación y reutiliza los pequeños objetos delegados.
Debate
La relación "se comporta-como" es un error común del software basado
en la implementación y, por tanto, esta subclasificación tiene una
motivación incorrecta. Sólo debes utilizar la herencia cuando encuentres
una relación "se comporta como" entre dos objetos.
El siguiente ejemplo muestra el problema clásico "es-un":
public class Rectangle {
int length;
int width;
public Rectangle(int length, int width) {
[Link] = length;
[Link] = width;
}
public int area() {
return [Link] * [Link];
}
}
public class Square extends Rectangle {
public Square(int size) {
super(size, size);
}
public int area() {
return [Link] * [Link];
}
}
public class Box extends Rectangle {
}
Square y Box no son verdaderos Rectangles desde el punto de vista del
comportamiento. Violan el principio de sustitución de Liskov (véase la
receta 19.1, "Romper la herencia profunda"). Puedes refactorizarlo
aplicando la receta del siguiente modo:
abstract public class Shape {
abstract public int area();
}
public final class Rectangle extends Shape {
int length;
int width;
public Rectangle(int length, int width) {
[Link] = length;
[Link] = width;
}
public int area() {
return [Link] * [Link];
}
}
public final class Square extends Shape {
// No longer subclassifies Rectangle
int size;
public Square(int size) {
[Link] = size;
}
public int area() {
return [Link] * [Link];
}
}
public final class Box {
// No longer subclassifies a Shape
Square shape;
public Box(int length, int width) {
[Link] = new Rectangle(length, width);
}
public int area() {
return [Link]();
}
}
La herencia se utiliza normalmente para modelar una relación "es-una",
en la que una subclase representa una versión más especializada de su
superclase. En este caso, un Square no es una versión especializada de
un Rectangle. Aunque un cuadrado es un tipo de rectángulo en el
mundo real, la relación entre ellos se describe más exactamente como una
relación "tiene-una", donde un Square tiene una forma Rectangle.
El problema surge al intentar representar un cuadrado utilizando la
jerarquía de clases Rectangle. Un cuadrado se define como un
rectángulo con lados iguales, por lo que cambiar la longitud de una
instancia de Rectangle para representar un cuadrado contradice la
naturaleza de un rectángulo, donde la longitud y la anchura pueden tener
valores diferentes. El overriding puede emitir advertencias al
subclasificar métodos concretos. Las jerarquías profundas (más de tres
niveles) también huelen a subclasificación incorrecta. Como excepción, si
una jerarquía sigue el principio "se comporta como", entonces es
segura. En los sistemas heredados es muy común tener jerarquías
profundas y overriding de métodos; debes refactorizarlos y subclasificar
sólo por razones esenciales, no de implementación.
Recetas relacionadas
Receta 19.2, "Romper las jerarquías Yo-Yo"
19.4 Sustituir la relación "es-un" por un
comportamiento
Problema
Las escuelas a menudo enseñan que la herencia representa una relación
"es-una".
Solución
Piensa en el protocolo y el comportamiento y olvídate de la herencia
accidental.
Debate
Los modelos "is-a" no siguen el principio de bijección, lo que genera un
comportamiento inesperado, contamina el código con anulaciones de
subclase y viola el principio de sustitución de Liskov (véase la Receta 19.1,
"Romper la herencia profunda").
No debes aplicar el principio de biyección de esta forma literal. La
relación de subclase suena muy bien cuando lees "es-una" en voz alta.
Pero "se comporta-como-una" debe ser la guía para la biyección. Siempre
debes pensar en términos de "se comporta-como-uno" y preferir la
composición a la herencia. Una relación "se comporta-como" procede del
mundo de los datos. Quizá aprendiste los diagramas entidad-relación con
el diseño estructurado y el modelado de datos, pero ahora necesitas
pensar en términos de comportamiento. El comportamiento es esencial, y
los datos son accidentales.
DIAGRAMAS ENTIDAD-RELACIÓN (ERD)
Los diagramas entidad-relación son una representación visual de los datos de
una base de datos. En un diagrama ERD, las entidades se representan mediante
rectángulos, mientras que las relaciones entre entidades se representan
mediante líneas que conectan los rectángulos.
He aquí un ejemplo clásico:
class ComplexNumber {
protected double realPart;
protected double imaginaryPart;
public ComplexNumber(double realPart, double imaginaryPart) {
[Link] = realPart;
[Link] = imaginaryPart;
}
}
class RealNumber extends ComplexNumber {
public RealNumber(double realPart) {
super(realPart, 0);
}
public void setImaginaryPart(double imaginaryPart) {
[Link]("Cannot set imaginary part for a real
number.");
}
}
Puedes refactorizarlo de la siguiente manera
class Number {
protected double value;
public Number(double value) {
[Link] = value;
}
}
class ComplexNumber extends Number {
protected double imaginaryPart;
public ComplexNumber(double realPart, double imaginaryPart) {
super(realPart);
[Link] = imaginaryPart;
}
}
class RealNumber extends Number {
}
Todo número real "es" un número complejo (según las matemáticas). Un
número entero "es" un número real (según las matemáticas). Un número
real no "se comporta como" un número complejo. No se puede
hacer [Link](), por lo que no es complejo según la
biyección.
Recetas relacionadas
Receta 19.3, "Romper la subclasificación para reutilizar el código"
Receta 19.6, "Renombrar clases aisladas"
Receta 19.11, "Eliminar atributos protegidos"
Ver también
"Problema del círculo-elipse" en Wikipedia
19.5 Eliminar clases anidadas
Problema
Tienes anidadas o clases pseudo-privadas que ocultan detalles de
implementación.
Solución
No utilices clases anidadas. No existen en el mundo real.
Debate
Las clases anidadas rompen la biyección (como se define en el Capítulo 2)
porque no se corresponden con los conceptos del mundo real. Son difíciles
de probar y reutilizar, y su ámbito oculto aporta complejidad al espacio
de nombres (ver Receta 18.4, "Eliminar clases globales"). Puedes hacer
pública la clase y mantener la nueva clase bajo tu propio espacio de
nombres/módulo o utilizar una fachada (véase la Receta 17.3, "Romper los
objetos de Dios") para exponer lo importante y ocultar lo irrelevante.
Algunos lenguajes te permiten crear conceptos privados que sólo pueden
utilizarse internamente, pero son más difíciles de probar, depurar y
reutilizar.
Aquí tienes un ejemplo de clase anidada:
class Address {
String description = "Address: ";
public class City {
String name = "Doha";
}
}
public class Main {
public static void main(String[] args) {
Address homeAddress = new Address();
[Link] homeCity = [Link] City();
[Link]([Link] + [Link]);
}
}
// The output is "Address: Doha"
//
// If you change privacy to 'private class City'
//
// You get an error " [Link] has private access in Address"
Así es como queda después de promocionarlo:
class Address {
String description = "Address: ";
}
class City {
String name = "Doha";
}
public class Main {
public static void main(String[] args) {
Address homeAddress = new Address();
City homeCity = new City();
[Link]([Link] + [Link]);
}
}
// The output is "Address: Doha"
//
// Now you can reuse and test the City concept
Muchos lenguajes están hinchados de funciones complejas. Rara vez
necesitas estas nuevas características extravagantes. Necesitas mantener
un conjunto mínimo de conceptos para evitar la complejidad accidental y
ocuparte de los esenciales.
Ver también
"Clases internas de Java" en W3Schools
19.6 Renombrar clases aisladas
Problema
Tus clases son globales pero tienen abreviaturas en sus nombres.
Solución
No utilices abreviaturas en las subclases. Si tus clases son globales, utiliza
nombres totalmente cualificados.
Debate
Las abreviaturas perjudican la legibilidad y favorecen los errores. Debes
cambiar el nombre de tus clases para proporcionar contexto y utilizar
módulos, espacios de nombres o nombres totalmente cualificados. He
aquí un ejemplo de clases abreviadas para el Rover de Marte
Perseverancia:
abstract class PerserveranceDirection {
}
class North extends PerserveranceDirection {}
class East extends PerserveranceDirection {}
class West extends PerserveranceDirection {}
class South extends PerserveranceDirection {}
// Subclasses have short names and are meaningless outside the
hierarchy
// If you reference East, you might mistake it for the cardinal point
Este es su aspecto con un contexto completo:
abstract class PerserveranceDirection { }
class PerserveranceDirectionNorth extends PerserveranceDirection {}
class PerserveranceDirectionEast extends PerserveranceDirection {}
class PerserveranceDirectionWest extends PerserveranceDirection {}
class PerserveranceDirectionSouth extends PerserveranceDirection {}
// Subclasses have fully qualified names
La detección automática no es una tarea fácil. Podrías aplicar políticas
locales de nomenclatura para las subclases, y tienes que elegir los
nombres sabiamente. Si tu lenguaje lo admite, utiliza módulos, espacios
de nombres (véase la Receta 18.4, "Eliminar clases globales") y ámbitos
locales.
CONSEJO
Algunos lenguajes proporcionan espacios de nombres o módulos en los que
puedes utilizar nombres cortos dentro de un ámbito determinado para evitar
colisiones.
Recetas relacionadas
Receta 19.3, "Romper la subclasificación para reutilizar el código"
19.7 Hacer Clases de Hormigón Finales
Problema
Tienes clases concretas con subclases.
Solución
Haz que tus clases concretas sean finales. Desplaza tu jerarquía.
Debate
Las clases concretas son malos padres y violan el principio de sustitución
de Liskov (ver Receta 19.1, "Romper la herencia profunda"). Sobrescribir
métodos a una clase concreta es siempre un error, ya que las subclases
deben ser especializaciones. Debes refactorizar las jerarquías y favorecer
la composición. Las clases hoja deben ser concretas y las clases no hoja,
abstractas.
He aquí un ejemplo de Stack:
class Stack extends ArrayList {
public void push(Object value) { ... }
public Object pop() { ... }
}
// Stack does not behave like an ArrayList
// Besides pop, push, top it also implements (or overrides)
// get, set, add, remove, and clear
// Stack elements can be arbitrary accessed
// Both classes are concrete
Ambos pueden heredar de la clase Collection:
abstract class Collection {
public abstract int size();
}
final class Stack extends Collection {
private Object[] contents;
public Stack(int maxSize) {
contents = new Object[maxSize];
}
public void push(Object value) { ... }
public Object pop() { ... }
public int size() {
return [Link];
}
}
final class ArrayList extends Collection {
private Object[] contents;
public ArrayList(Object[] contents) {
[Link] = contents;
}
public int size() {
return [Link];
}
}
Sobrescribir un método concreto huele claramente. Puedes imponer esta
política en la mayoría de las clases (consulta la Receta 5.2, "Declarar
variables para que sean variables"). Las clases abstractas deben tener sólo
unos pocos métodos concretos. Puedes comprobar un umbral predefinido
para los infractores. La subclasificación accidental es la primera opción
obvia y atractiva para los desarrolladores noveles. Los desarrolladores
más maduros encuentran en cambio oportunidades de composición. La
composición es dinámica, múltiple, enchufable, más comprobable, más
mantenible y menos acoplada que la herencia. Sólo subclasifica una
entidad si sigue la relación "se comporta-como-una" (véase la Receta 19.4,
"Sustituir la relación "es-una" por el comportamiento"). Después de
subclasificar, la clase padre debe ser abstracta.
Recetas relacionadas
Receta 19.3, "Romper la subclasificación para reutilizar el código"
Ver también
"Composición sobre herencia" en Wikipedia
19.8 Definir explícitamente la herencia
de clases
Problema
Tus clases son abstractas, finales o indefinidas, pero no las marcas
explícitamente.
Solución
Si tu lenguaje dispone de la herramienta adecuada, tus clases deben ser
abstractas o finales y entonces el compilador puede aplicar estas reglas de
negocio por ti.
Debate
La subclasificación para la reutilización del código presenta muchos
problemas. Necesitas declarar todas tus clases hoja como finales y el resto
como abstractas. Estas palabras clave también ayudan a hacer explícitos
tus diseños. Gestionar las jerarquías y la composición es la principal tarea
de un buen diseñador de software, y mantener las jerarquías sanas es
crucial para favorecer la cohesión y evitar el acoplamiento.
Todas estas clases carecen de una declaración final explícita:
public class Vehicle
{
// Class is not a leaf. Therefore it should be abstract
// An abstract method that only declares, but does not define the
start
// functionality because each vehicle uses a different starting
mechanism
abstract void start();
}
public class Car extends Vehicle
{
// Class is a leaf. Therefore it should be final
}
public class Motorcycle extends Vehicle
{
// Class is a leaf. Therefore it should be final
}
Puedes detectar problemas de jerarquía haciendo cumplir la notación:
abstract public class Vehicle
{
// The class is not a leaf. Therefore it must be abstract
// An abstract method that only declares, but does not define the
start
// functionality because each vehicle uses a different starting
mechanism
abstract void start();
}
final public class Car extends Vehicle
{
// The class is a leaf. Therefore it is final
}
final public class Motorcycle extends Vehicle
{
// The class is a leaf. Therefore it is final
}
Revisa tus clases y empieza a calificarlas como abstractas o finales. No hay
casos válidos para dos clases concretas en los que una subclasifique a la
otra.
Recetas relacionadas
Receta 12.3, "Refactorizar clases con una subclase"
Receta 19.2, "Romper las jerarquías Yo-Yo"
Receta 19.3, "Romper la subclasificación para reutilizar el código"
Receta 19.11, "Eliminar atributos protegidos"
Ver también
"Diseño de clases reutilizables" de Ralph E. Johnson y Brian Foote
19.9 Migrar clases vacías
Problema
Tienes clases sin comportamiento. Pero las clases sirven para encapsular
el comportamiento.
Solución
Elimina todas las clases vacías.
Debate
Las clases vacías violan la biyección, ya que no existen objetos sin
comportamiento en el mundo real. Ejemplos notables son las excepciones
innecesarias o las clases de jerarquía media. Contaminan los espacios de
nombres. Debes eliminar estas clases y sustituirlas por objetos. Muchos
desarrolladores siguen pensando que las clases son depósitos de datos y
confunden conceptos de comportamiento diferentes con la devolución
de datos diferentes.
Aquí tienes una clase ShopItem vacía:
class ShopItem {
code() { }
description() { }
}
class BookItem extends ShopItem {
code() { return 'book' }
description() { return 'some book'}
}
// Concrete class has no real behavior, just returns different 'data'
Esto es lo que parece cuando lo refactorizas:
class ShopItem {
constructor(code, description) {
// validate code and description
this._code = code;
this._description = description;
}
code() { return this._code }
description() { return this._description }
// Add more functions to avoid anemic classes
// Getters are also code smells, so you need to iterate more
}
bookItem = new ShopItem('book', 'some book');
// create more items
Varios linters te advierten sobre las clases vacías. También puedes crear
tus propios guiones utilizando la metaprogramación (consulta el Capítulo
23, "Metaprogramación"). Las clases son lo que hacen, su
comportamiento, y las clases vacías no hacen nada.
Recetas relacionadas
Receta 3.1, "Convertir objetos anémicos en objetos ricos"
Receta 3.6, "Eliminar DTOs"
Receta 12.3, "Refactorizar clases con una subclase"
Receta 18.4, "Eliminar clases globales"
Receta 22.2, "Eliminar excepciones innecesarias"
19.10 Retrasar la clasificación
prematura
Problema
Tienes abstracciones antes de haber visto suficientes conexiones
concretas.
Solución
No adivines lo que te deparará el futuro.
Debate
Es difícil hacer predicciones, especialmente sobre el futuro. Es un
problema habitual en la industria del software. Ten en cuenta que las
primeras impresiones erróneas conducen a malos diseños. Debes esperar
a tener ejemplos concretos para generalizar y refactorizar sólo cuando
tengas pruebas suficientes. La clasificación aristotélica es un gran
problema en informática. Los desarrolladores de software tienden a
clasificar y nombrar las cosas antes de reunir suficientes conocimientos y
contexto. Tiendes a clasificar objetos antes de comprender plenamente su
comportamiento, características, requisitos o relaciones.
Consulta el siguiente ejemplo de Song:
class Song {
constructor(title, artist) {
[Link] = title;
[Link] = artist;
}
play() {
[Link](`Playing ${[Link]} by ${[Link]}`);
}
}
Cuando descubres las canciones clásicas, puedes tener la tentación de
subclasificarlas en Songs:
class ClassicalSong extends Song {
constructor(title, artist, composer) {
super(title, artist);
[Link] = composer;
}
listenCarefully() {
[Link](`I am listening to ${[Link]} by $
{[Link]}`);
}
}
const goldberg = new ClassicalSong
("The Goldberg Variations", "Glenn Gould", "Bach");
Ahora también puedes tener canciones pop:
class PopSong extends Song {
constructor(title, artist, album) {
super(title, artist);
[Link] = album;
}
danceWhileListening() {
[Link](`I am dancing with ${[Link]}`);
}
}
const theTourist = new PopSong("The Tourist", "Radiohead", "OK,
Computer");
Estás especulando sobre el futuro de todos los géneros. ¿Qué ocurre
cuando los mezclas?
class ClassicalPopSong extends ClassicalSong {
constructor(title, artist, composer, album) {
super(title, artist, composer);
[Link] = album;
}
danceWhileListening() {
[Link](`${[Link]} is a classical song with a pop twist`);
}
}
const classicalPopSong = new ClassicalPopSong(
"Popcorn Concerto", "Classical Pop Star", "Beethoven);
Una clase abstracta con una sola subclase indica una clasificación
prematura. Cuando trabajes con clases, nombra las abstracciones en
cuanto aparezcan. Elige buenos nombres basándote en el
comportamiento; no nombres tus abstracciones hasta que nombres
tus subclases concretas.
Recetas relacionadas
Receta 19.3, "Romper la subclasificación para reutilizar el código"
19.11 Eliminar atributos protegidos
Problema
Tienes atributos protegidos en tus clases.
Solución
Haz que los atributos sean privados.
Debate
ATRIBUTOS PROTEGIDOS
Un atributo protegido es una variable de instancia o propiedad de una clase a la
que sólo se puede acceder dentro de la clase o de sus subclases. Los atributos
protegidos son una forma de restringir el acceso a determinados datos dentro de
una jerarquía de clases, permitiendo al mismo tiempo que las subclases accedan
a esos datos y los modifiquen si es necesario.
Los atributos protegidos son estupendos para encapsular y controlar el
acceso a las propiedades. Pero con frecuencia son una advertencia de otro
problema. Los atributos protegidos son una pista para detectar la
subclasificación con fines de reutilización del código y la violación del
principio de sustitución de Liskov (véase la receta 19.1, "Romper la
herencia profunda"). Como en muchas otras recetas, favorece la
composición, no subclasifiques atributos y extrae el comportamiento a
objetos separados. O utiliza rasgos si están disponibles en el lenguaje que
elijas.
RASGOS
Los rasgos definen un conjunto de características o comportamientos comunes
que pueden compartir varias clases. Un rasgo es esencialmente un conjunto de
métodos que pueden ser reutilizados por distintas clases sin necesidad de que
hereden de una superclase común. Proporciona un mecanismo de reutilización
de código más flexible que la herencia, ya que permite a las clases heredar
comportamientos de múltiples fuentes.
Aquí tienes un ejemplo de atributos protegidos:
abstract class ElectronicDevice {
protected $battery;
public function __construct(Battery $battery) {
$this->battery = $battery; // battery is inherited to all
devices
}
}
abstract class IDevice extends ElectronicDevice {
protected $operatingSystem; // operating system is inherited to
all devices
public function __construct(Battery $battery, OperatingSystem
$ios) {
$this->operatingSystem = $ios;
parent::__construct($battery)
}
}
final class IPad extends IDevice {
public function __construct(Battery $battery, OperatingSystem
$ios) {
parent::__construct($battery, $ios)
}
}
final class IPhone extends IDevice {
private $phoneModule:
public function __construct(Battery $battery,
OperatingSystem $ios,
PhoneModule $phoneModule) {
$this->phoneModule = $phoneModule;
parent::__construct($battery, $ios);
}
}
Esto es lo que parece cuando los refactorizas:
interface ElectronicDevice { }
interface PhoneCommunication { }
final class IPad implements ElectronicDevice {
private $operatingSystem; // The attributes are duplicated
private $battery;
// If you have too much duplicated behavior you should extract
them
public function __construct(Battery $battery, OperatingSystem
$ios) {
$this->operatingSystem = $ios;
$this->battery = $battery;
}
}
final class IPhone implements ElectronicDevice, PhoneCommunication {
private $phoneModule;
private $operatingSystem;
private $battery;
public function __construct(Battery $battery,
OperatingSystem $ios,
PhoneModule $phoneModule) {
$this->phoneModule = $phoneModule;
$this->operatingSystem = $ios;
$this->battery = $battery;
}
}
En los idiomas que admiten atributos protegidos puedes evitarlos por
política o tener un aviso del problema. Los atributos protegidos son otra
herramienta que debes utilizar con cuidado. Cada aparición es un
problema potencial, y debes ser muy exigente con los atributos y la
herencia.
Recetas relacionadas
Receta 19.3, "Romper la subclasificación para reutilizar el código"
19.12 Completar implementaciones
vacías
Problema
Tienes métodos vacíos en la jerarquía para futuras implementaciones que
no fallen.
Solución
Completa los métodos con una excepción o una posible solución.
Debate
Los métodos vacíos violan el principio "fail fast" al proporcionar una
solución funcional (pero potencialmente errónea). Deberías lanzar un
error indicando que la implementación no está completa. Crear una
implementación vacía puede parecer bien y puedes saltar a problemas
más interesantes. Pero el código que quede no fallará rápido, así que
depurarlo será un problema mayor.
Aquí tienes un ejemplo de aplicación vacía:
class MerchantProcessor {
processPayment(amount) {
// no default implementation
}
}
class MockMerchantProcessor extends MerchantProcessor {
processPayment(amount) {
// Empty implementation to comply with the compiler
// Won't do anything
}
}
Esto es más declarativo y fallará rápido:
class MerchantProcessor {
processPayment(amount) {
throw new Error('Should be overridden');
}
}
class MockMerchantProcessor extends MerchantProcessor {
processPayment(amount) {
throw new Error('Will be implemented when needed');
}
}
También puedes sustituirlo por una aplicación real:
class MockMerchantProcessor extends MerchantProcessor {
processPayment(amount) {
[Link]('Mock payment processed: $${amount}');
}
}
ADVERTENCIA
Como el código vacío es válido a veces, sólo una buena revisión por pares
encontrará estos problemas.
Ser perezoso y aplazar ciertas decisiones es aceptable, pero es crucial ser
explícito al respecto.
Recetas relacionadas
Receta 20.4, "Sustituir Mocks por Objetos Reales"
Receta 19.9, "Migrar clases vacías"
Capítulo 20. Prueba
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y
comentarios: translation-feedback@[Link]
Ninguna cantidad de pruebas puede demostrar que un software es correcto,
una sola prueba puede demostrar que un software es incorrecto.
Amir Ghahrai
20.0 Introducción
Trabajar sin cobertura de pruebas automatizada en décadas anteriores
era todo un reto, ya que los desarrolladores tenían que depender en gran
medida de las pruebas y la depuración manuales para detectar y
solucionar los problemas de su software. Las pruebas manuales consisten
en someter el software a una serie de pruebas diseñadas para comprobar
su funcionalidad, rendimiento y estabilidad. Este proceso lleva mucho
tiempo y es propenso al error humano, ya que los probadores pueden
pasar por alto ciertos escenarios o defectos críticos.
Sin pruebas automatizadas, los desarrolladores tenían que dedicar una
cantidad considerable de tiempo a depurar y solucionar problemas, lo
que podía ralentizar el proceso de desarrollo y retrasar el lanzamiento de
nuevas funciones o actualizaciones. También era difícil garantizar
resultados coherentes y fiables en distintas plataformas y entornos. Los
desarrolladores tenían que probar manualmente su software en varios
sistemas operativos, navegadores y configuraciones de hardware, lo que
podía dar lugar a defectos inesperados y problemas de compatibilidad. No
había garantías de que la resolución de un problema o el desarrollo de
una nueva función evitaran que los escenarios conocidos se rompieran en
el futuro, y los usuarios finales estaban acostumbrados a experimentar
fallos inesperados en funciones que ya funcionaban.
Hoy en día, escribir pruebas es una disciplina obligatoria para los buenos
desarrolladores. Puedes cambiar el software cuando quieras si has escrito
pruebas para la funcionalidad anterior. Hay muchos libros y cursos sobre
cómo desarrollar buen código, pero no tantos sobre cómo escribir buenas
pruebas. Esperemos que puedas aplicar las recetas de este capítulo.
20.1 Probar métodos privados
Problema
Tienes que probar los métodos privados de .
Solución
No pruebes tus métodos privados. Extráelos.
Debate
Todo desarrollador se ha enfrentado alguna vez al reto de escribir una
prueba para una función o método interno que proporciona un soporte
importante para funcionalidades de nivel superior. No puedes probar
directamente los métodos porque romperías el encapsulamiento del
método y tampoco quieres copiarlo o hacerlo público. Por regla general,
no hagas públicos tus métodos para probarlos ni utilices
la metaprogramación para eludir la protección (consulta el Capítulo 23,
"Metaprogramación"). Si tu método es trivial, no necesitas probarlo. Si tu
método es complicado, necesitas convertirlo en un objeto método
(ver Receta 10.7, "Extraer un método a un objeto"). No traslades el
cómputo privado a los ayudantes (consulta la Receta 7.2, "Renombrar y
romper ayudantes y utilidades") ni utilices métodos estáticos (consulta
la Receta 18.2, "Reificar funciones estáticas").
Este ejemplo prueba el tiempo que tarda la luz en viajar desde una
estrella lejana:
final class Star {
private $distanceInParsecs;
public function timeForLightReachingUs() {
return $this->convertDistanceInParsecsToLightYears($this-
>distanceInParsecs);
}
private function
convertDistanceInParsecsToLightYears($distanceInParsecs) {
return 3.26 * $distanceInParsecs;
// Function is using an argument that is already available
// since it has private access to $distanceInParsecs.
// This is another smell indicator.
// You cannot test this function directly since it is private.
}
}
Este es el aspecto que tiene cuando creas un convertidor:
final class Star {
private $distanceInParsecs;
public function timeToReachLightToUs() {
return (new ParsecsToLightYearsConverter())
->convert($this->distanceInParsecs);
}
}
final class ParsecsToLightYearsConverter {
public function convert($distanceInParsecs) {
return 3.26 * $distanceInParsecs;
}
}
final class ParsecsToLightYearsConverterTest extends TestCase {
public function testConvert0ParsecsReturns0LightYears() {
$this->assertEquals(0, (new ParsecsToLightYearsConverter())-
>convert(0));
}
// You can add lots of tests and rely on this object
// So you don't need to test Star conversions
// You can't yet test Star public timeToReachLightToUs()
// This is a simplified scenario
}
Sólo puedes encontrar abuso de la metaprogramación en algunos
frameworks unitarios (ver Capítulo 23, "Metaprogramación"). Con esta
guía, debes elegir siempre la solución del método objeto.
Recetas relacionadas
Receta 7.2, "Renombrar y romper ayudantes y utilidades"
Receta 10.7, "Extraer un método a un objeto"
Receta 18.2, "Reificar funciones estáticas"
Receta 23.1, "Eliminar el uso de la metaprogramación"
Receta 23.2, "Reificar funciones anónimas"
Ver también
¿Debo probar el sitio web de Métodos Privados?
20.2 Añadir descripciones a las
afirmaciones
Problema
Tienes un montón de buenas afirmaciones y utilizas la descripción por
defecto para indicar el fallo mientras omites el motivo.
Solución
Utiliza afirmaciones con descripciones declarativas y significativas.
Debate
Cuando falla una aserción, necesitas comprender rápidamente por qué ha
fallado. Añadir descripciones informativas opcionales es una estrategia
estupenda para no perder el tiempo. También puedes añadir algunas
guías para la resolución de problemas. Es el sustituto perfecto de los
comentarios en el código. Las descripciones de las aserciones son el lugar
donde explicas por qué esperas un resultado concreto, y eso a su vez te
permite tomar decisiones explícitas de diseño o implementación.
Este ejemplo compara dos colecciones:
public function testNoNewStarsAppeared()
{
$expectedStars = $this->historicStarsOnFrame();
$observedStars = $this->starsFromObservation();
// These sentences get a very large collection
$this->assertEquals($expectedStars, $observedStars);
// If something fails you will have a very hard time debugging
}
Este es el aspecto que tiene cuando añades la descripción en la
afirmación:
public function testNoNewStarsAppeared(): void
{
$expectedStars = $this->historicStarsOnFrame();
$observedStars = $this->starsFromObservation();
// These sentences get a very large collection
$newStars = array_diff($expectedStars, $observedStars);
$this->assertEquals($expectedStars, $observedStars,
'There are new stars ' . print_r($newStars, true));
// Now you can see EXACTLY why the assertion failed with a clear
and
// declarative message
}
Dado
que assert (o assertTrue, [Link], [Link], assert_tru
e, XCTAssertTrue, ASSERT_TRUE), assertDescription
(expect(true).toBe(false,
'message')), assertEquals("message", true, false),
y ASSERT_EQ((true, false) << "message";), etc. son a veces
funciones diferentes con un número distinto de argumentos, puedes
ajustar las políticas para favorecer la versión con el mensaje de ayuda. Sé
respetuoso con el lector de tus afirmaciones, ¡sobre todo porque podrías
ser tú mismo!
Ver también
xUnit: descripción de assert obsoleta
20.3 Migrar assertTrue a aserciones
específicas
Problema
Tienes aserciones con booleanos en tus pruebas.
Solución
No utilices assertTrue() a menos que compruebes un booleano.
Debate
Las afirmaciones booleanas dificultan el seguimiento de errores. Cada
aserción booleana es una oportunidad para hacer una aserción más
específica. Deberías comprobar si puedes reescribir mejor la condición
booleana y favorecer assertEquals. Al aseverar contra un booleano, tus
motores de pruebas no pueden ayudarte mucho. Sólo te dicen que algo ha
fallado. El seguimiento de errores se hace más difícil.
Se trata de una afirmación sobre una condición booleana de igualdad:
final class RangeUnitTest extends TestCase {
function testValidOffset() {
$range = new Range(1, 1);
$offset = $range->offset();
$this->assertTrue(10 == $offset);
// No functional essential description :(
// Accidental description provided by tests is very bad
}
}
Al fallar, el marco de la unidad te lo mostrará:
1 Test, 1 failed
Failing asserting true matches expected false :(
() <-- no business description :(
<Click to see difference> - Two booleans
(and a diff comparator will show you two booleans)
Se trata de una afirmación más descriptiva:
final class RangeUnitTest extends TestCase {
function testValidOffset() {
$range = new Range(1, 1);
$offset = $range->offset();
$this->assertEquals(10, $offset, 'All pages must have 10 as
offset');
// Expected value should always be first argument
// You add a functional essential description
// to complement accidental description provided by tests
}
}
Al fallar, el marco de la unidad te lo mostrará:
1 Test, 1 failed
Failing asserting 0 matches expected 10
All pages must have 10 as offset <-- business description
<Click to see difference>
(and a diff comparator will help you and it will be a great help
for complex objects like objects or jsons)
Esta mejora del código no tiene ningún beneficio para la informática, ya
que ambas expresiones son equivalentes. Pero las comprobaciones de
aserciones más específicas mejoran mucho el mantenimiento del software
y la colaboración en equipo. Intenta reescribir tus aserciones booleanas y
solucionarás los fallos mucho más rápido.
Recetas relacionadas
Receta 14.3, "Reificar variables booleanas"
Receta 14.12, "Modificar la comparación con booleanos"
20.4 Sustituir simulacros por objetos
reales
Problema
Tus pruebas utilizan objetos simulados en lugar de reales.
Solución
Sustituye los simulacros por objetos reales siempre que sea posible.
Debate
OBJETOS SIMULADOS
Un objeto simulado imita el comportamiento de un objeto real para probar o
simular su comportamiento. Puedes utilizarlo para probar componentes de
software que dependan de otros componentes, como APIs o bibliotecas externas.
La simulación es una gran ayuda para probar el comportamiento. Pero,
como ocurre con muchas otras herramientas, puedes abusar de ellas. Los
simulacros añaden complejidad accidental, son más difíciles de mantener
y te dan una falsa sensación de seguridad. Te encontrarás construyendo
una solución paralela con objetos reales y simulacros, lo que dificultará el
mantenimiento. Por regla general, sólo debes simular entidades no
empresariales.
El siguiente ejemplo utiliza un simulacro sobre un objeto comercial:
class PaymentTest extends TestCase
{
public function
testProcessPaymentReturnsTrueOnSuccessfulPayment()
{
$paymentDetails = array(
'amount' => 123.99,
'card_num' => '4111-1111-1111-1111',
'exp_date' => '03/2013',
);
$payment = $this->getMockBuilder('Payment')
->setConstructorArgs(array())
->getMock();
// You should not mock a business object!
$authorizeNet = new AuthorizeNetAIM(
$payment::API_ID, $payment::TRANS_KEY);
// This is an external and coupled system.
// You have no control over it so tests become fragile
$paymentProcessResult = $payment->processPayment(
$authorizeNet, $paymentDetails);
$this->assertTrue($paymentProcessResult);
}
}
Puedes sustituir el simulacro de negocio por objetos reales y simular
dependencias externas:
class PaymentTest extends TestCase
{
public function
testProcessPaymentReturnsTrueOnSuccessfulPayment()
{
$paymentDetails = array(
'amount' => 123.99,
'card_num' => '4111-1111-1111-1111',
'exp_date' => '03/2013',
);
$payment = new Payment(); // Payment is a real one
$response = new \stdClass();
$response->approved = true;
$response->transaction_id = 123;
$authorizeNet = $this->getMockBuilder('\AuthorizeNetAIM')
->setConstructorArgs(array($payment::API_ID,
$payment::TRANS_KEY))
->getMock();
// External system is mocked
$authorizeNet->expects($this->once())
->method('authorizeAndCapture')
->will($this->returnValue($response));
$paymentProcessResult = $payment->processPayment(
$authorizeNet, $paymentDetails);
$this->assertTrue($paymentProcessResult);
}
}
Se trata de un patrón arquitectónico. No será fácil crear una regla de
detección automática. Comprobarás que hacer mocks de problemas
accidentales (serialización, bases de datos, API) es una muy buena
práctica para evitar el acoplamiento. Los mocks, como muchos otros
dobles de prueba, son herramientas excelentes.
Recetas relacionadas
Receta 19.12, "Completar implementaciones vacías"
20.5 Afinar las afirmaciones genéricas
Problema
Tienes pruebas con afirmaciones demasiado genéricas.
Solución
Las afirmaciones de las pruebas deben ser precisas; no deben ser
demasiado vagas ni específicas.
Debate
No hagas pruebas débiles para crear una falsa sensación de cobertura.
Las aserciones genéricas te dan una falsa sensación de seguridad. Debes
comprobar el caso correcto, hacer una aserción para un caso funcional y
evitar probar la implementación. Esto es una prueba vaga:
square = Square(5)
assert [Link]() != 0
# This will lead to false negatives since it does not cover all cases
Esta prueba es más precisa:
square = Square(5)
assert [Link]() = 25
# Assertion should be precise
Con las técnicas de pruebas de mutación (ver Receta 5.1, "Cambiar var por
const") puedes encontrar estos errores en tus pruebas. Deberías utilizar
técnicas de desarrollo como el desarrollo dirigido por pruebas (TDD)
(consulta la Receta 4.8, "Eliminar propiedades innecesarias") que solicitan
casos de negocio concretos y hacen afirmaciones concretas basadas en tu
dominio.
Recetas relacionadas
Receta 20.4, "Sustituir Mocks por Objetos Reales"
Receta 20.6, "Cómo eliminar las pruebas con escamas"
20.6 Eliminar pruebas defectuosas
Problema
Tienes pruebas que no son deterministas.
Solución
No dependas de cosas que tu prueba no pueda controlar, como bases de
datos externas o recursos en Internet. Si tus pruebas fallan
aleatoriamente, tienes que arreglarlas.
Debate
Si tus pruebas no son deterministas, empiezas a perder confianza, lo que
te baja la moral. Puedes sentir que pierdes el tiempo añadiendo o
ejecutando pruebas. Las pruebas deben tener un control ambiental total
(ver Receta 18.5, "Cambiar la creación de fechas globales"). No debe haber
espacio para comportamientos erráticos ni grados de libertad. Debes
eliminar todo acoplamiento de las pruebas. Las pruebas frágiles,
intermitentes, esporádicas o erráticas son habituales en muchas
organizaciones. Sin embargo, minan la confianza de los desarrolladores.
Las pruebas defectuosas son pruebas demasiado sensibles a los cambios
en el entorno o el sistema que se está probando. Por ejemplo, una prueba
puede fallar debido a un cambio en el hardware subyacente, la
conectividad de red o las dependencias de software. Las pruebas
defectuosas pueden ser problemáticas porque requieren un
mantenimiento frecuente y pueden no reflejar con exactitud la verdadera
funcionalidad del sistema.
PRUEBAS DEFECTUOSAS
Las pruebasdefectuosas o erráticas producen resultados incoherentes o
impredecibles. Puede ser difícil trabajar con este tipo de pruebas porque pueden
pasar o fallar inesperadamente, lo que dificulta determinar si el código que se
está probando funciona correctamente.
Aquí tienes un ejemplo de prueba errática:
public abstract class SetTest {
protected abstract Set<String> constructor();
@Test
public final void testAddEmpty() {
Set<String> colors = [Link]();
[Link]("green");
[Link]("blue");
assertEquals("{green. blue}", [Link]());
// This is fragile since it depends on the set order
// and mathematical sets are by definition not sorted
}
}
Esto es más determinista:
public abstract class SetTest {
protected abstract Set<String> constructor();
@Test
public final void testAddEmpty() {
Set<String> colors = [Link]();
[Link]("green");
assertEquals("{green}", [Link]());
}
@Test
public final void testEntryAtSingleEntry() {
Set<String> colors = [Link]("red");
Boolean redIsPresent = [Link]("red");
assertEquals(true, redIsPresent);
}
}
La detección de pruebas erráticas puede hacerse con las estadísticas de
ejecución de pruebas. Es muy difícil poner algunas pruebas en
mantenimiento porque estás eliminando una red de seguridad. Las
pruebas frágiles muestran el acoplamiento del sistema y un
comportamiento no determinista o errático. Los desarrolladores dedican
mucho tiempo y esfuerzo a luchar contra estos falsos positivos.
Recetas relacionadas
Receta 20.5, "Perfeccionar aserciones genéricas"
Receta 20.12, "Reescribir pruebas en función de fechas"
20.7 Modificar afirmaciones de
números flotantes
Problema
Tienes afirmaciones con números flotantes.
Solución
No compares los números de los flotadores.
Debate
Afirmar que dos números flotantes son iguales es un problema muy
difícil. Al comparar dos números flotantes en una prueba, pueden surgir
varios problemas debido a la forma en que se representan y almacenan
los números en coma flotante en la memoria del ordenador. Estos
problemas pueden dar lugar a resultados inesperados y dificultar la
redacción de pruebas fiables y precisas.
Los números flotantes pueden estar sujetos a errores de redondeo.
Aunque dos cálculos deban producir el mismo valor, pueden acabar con
resultados ligeramente diferentes debido a errores de redondeo, lo que
provoca falsos negativos o falsos positivos en tus pruebas. Esto puede dar
lugar a pruebas frágiles. Por regla general, debes evitar los números
flotantes a menos que tengas verdaderos problemas de rendimiento, ya
que se trata de un caso de optimización prematura(consulta el Capítulo
16, "Optimización prematura"). Puedes utilizar números de precisión
arbitraria y, si necesitas comparar flotantes, compara con tolerancia.
Comparar números flotantes es un viejo problema de informática. La
solución habitual es utilizar comparaciones umbral.
Este ejemplo compara dos números flotantes:
[Link](0.0012f, 0.0012f); // Deprecated
[Link](0.0012f == 0.0012f); // Not JUnit - Smell
Esta es la forma recomendada de comparar dos números flotantes:
float LargeThreshold = 0.0002f;
float SmallThreshold = 0.0001f;
[Link](0.0012f, 0.0014f, LargeThreshold); // true
[Link](0.0012f, 0.0014f, SmallThreshold); // false -
Assertion Fail
[Link](12 / 10000, 12 / 10000); // true
[Link](12 / 10000, 14 / 10000); // false
Puedes añadir una comprobación en assertEquals() en tus marcos de
pruebas para evitar la comprobación de flotantes. Siempre debes evitar
comparar flotantes.
Recetas relacionadas
Receta 24.3, "Cambiar números flotantes a decimales"
20.8 Cambiar los datos de prueba por
datos realistas
Problema
Estás utilizando datos falsos en tus pruebas.
Solución
Utiliza escenarios de casos reales y datos reales si es posible.
Debate
Los datos falsos son una violación de la biyección definida en el Capítulo
2; conducen a casos de uso de prueba incorrectos y perjudican la
legibilidad. Debes utilizar datos reales y utilizar el MAPPER para mapear
entidades y datos reales. En el pasado, los desarrolladores solían falsear
los datos del dominio y probaban con datos abstractos. Desarrollaban
utilizando un modelo en cascada, lejos de los usuarios reales. Las pruebas
de aceptación del usuario cobraron importancia con las técnicas de
biyección y MAPPER, el diseño orientado al dominio y el TDD.
DISEÑO BASADO EN EL DOMINIO
El diseño orientado al dominio se centra en alinear el diseño de los sistemas de
software con el dominio del negocio o del problema, haciendo que el código sea
más expresivo, mantenible y estrechamente vinculado a los requisitos del
negocio.
Utilizando metodologías ágiles, necesitas probar con datos del mundo
real. Si encuentras un error en un sistema de producción, añade un caso
que cubra el error exacto con datos reales.
PRUEBAS DE ACEPTACIÓN DEL USUARIO
Las pruebas de aceptación del usuario (UAT) comprueban si un sistema o
aplicación de software cumple los requisitos empresariales y de los usuarios y
está listo para su implementación en producción. Implica una serie de pruebas
con datos reales y revisiones por parte de los usuarios finales para verificar que
el software funciona correctamente y satisface sus necesidades y expectativas.
La siguiente prueba utiliza datos irreales:
class BookCartTestCase([Link]):
def setUp(self):
[Link] = Cart()
def test_add_book(self):
[Link].add_item('xxxxx', 3, 10)
# This is not a real example
[Link](
[Link],
30,
msg='Book Cart total not correct after adding books')
[Link](
[Link]['xxxxx'],
3,
msg='Quantity of items not correct after adding book')
def test_remove_item(self):
[Link].add_item('fgdfhhfhhh', 3, 10)
[Link].remove_item('fgdfhhfhrhh', 2, 10)
# You made a typo since example is not a real one
[Link](
[Link],
10,
msg='Book Cart total not correct after removing book')
[Link](
[Link]['fgdfhhfhhh'],
1,
msg='Quantity of books not correct after removing book')
Puedes evitar la errata del ejemplo utilizando la Receta 6.8, "Sustituir
números mágicos por constantes". Pero no sustituirías TODO el texto en
tus casos de uso. Este es el aspecto de la prueba con datos reales:
class BookCartTestCase([Link]):
def setUp(self):
[Link] = Cart()
def test_add_book(self):
[Link].add_item('Harry Potter', 3, 10)
[Link](
[Link],
30,
msg='Book Cart total not correct after adding books')
[Link](
[Link]['Harry Potter'],
3,
msg='Quantity of items not correct after adding book')
# You don't reuse the same example.
# You use a new REAL book.
def test_remove_item(self):
[Link].add_item('Divergent', 3, 10)
[Link].remove_item('Divergent', 2, 10)
[Link](
[Link],
10,
msg='Book Cart total not correct after removing book')
[Link]([Link][
'Divergent'],
1,
msg='Quantity of books not correct after removing book')
Aún puedes cometer errores tipográficos en ejemplos del mundo real
(como "Devergent"), pero los encontrarás antes. Leer pruebas es la única
forma de aprender cómo se comporta el software, y tienes que ser
excesivamente explícito en tus pruebas.
ADVERTENCIA
Enalgunos dominios y bajo ciertas normativas no puedes utilizar datos reales.
En estos casos, debes falsearlos con datos significativos pero anonimizados.
Recetas relacionadas
Receta 8.5, "Convertir comentarios en nombres de funciones"
Ver también
"Dado-cuando-entonces" en Wikipedia
20.9 Proteger las pruebas que violan la
encapsulación
Problema
En tienes pruebas que violan la encapsulación.
Solución
No escribas métodos con el único propósito de utilizarlos en tus pruebas.
Debate
A veces escribes código para favorecer las pruebas y ese código viola la
encapsulación, lo que da lugar a malas interfaces y provoca un
acoplamiento innecesario. Las pruebas deben tener un control total del
entorno, y si no pueden controlar su objeto, tienes un acoplamiento
indeseado. Desacoplalas.
Aquí puedes ver un método para probar:
class Hangman {
private $wordToGuess;
function __construct() {
$this->wordToGuess = getRandomWord();
// Test is not in control of this
}
public function getWordToGuess(): string {
return $this->wordToGuess;
// Sadly you need to reveal this
}
}
class HangmanTest extends TestCase {
function test01WordIsGuessed() {
$hangmanGame = new Hangman();
$this->assertEquals('tests', $hangmanGame->wordToGuess());
// How can you make sure the word is guessed?
}
}
Este es un enfoque mejor:
class Hangman {
private $wordToGuess;
function __construct(WordRandomizer $wordRandomizer) {
$this->wordToGuess = $wordRandomizer->newRandomWord();
}
function wordWasGuessed() { }
function play(char letter) { }
}
class MockRandomizer implements WordRandomizer {
function newRandomWord(): string {
return 'tests';
}
}
class HangmanTest extends TestCase {
function test01WordIsGuessed() {
$hangmanGame = new Hangman(new MockRandomizer());
// You are in full control!
$this->assertFalse($hangmanGame->wordWasGuessed());
$hangmanGame->play('t');
$this->assertFalse($hangmanGame->wordWasGuessed());
$hangmanGame->play('e');
$this->assertFalse($hangmanGame->wordWasGuessed());
$hangmanGame->play('s');
$this->assertTrue($hangmanGame->wordWasGuessed());
// You just test behavior
}
}
Esto es un olor a diseño. Puedes detectar si necesitas un método sólo para
pruebas. Las pruebas de caja abierta son frágiles. Prueban la
implementación en lugar del comportamiento.
Recetas relacionadas
Receta 3.3, "Eliminar definidores de objetos"
Receta 20.6, "Cómo eliminar las pruebas con escamas"
Ver también
Patrones de pruebas xUnit: Refactoring Test Code por Gerard Meszaros
20.10 Eliminar información irrelevante
del test
Problema
Tienes pruebas con datos irrelevantes.
Solución
No añadas información innecesaria a tus afirmaciones.
Debate
Los datos irrelevantes distraen la atención del lector y dificultan la
legibilidad y la mantenibilidad. Debes eliminarlos en la medida de lo
posible y dejar sólo las aserciones necesarias. Las pruebas deben ser
mínimas y seguir el patrón setup/exercise/assert.
Aquí puedes ver datos irrelevantes relacionados con modelos y colores de
coches:
def test_formula_1_race():
# Setup
racers = [
{"name": "Lewis Hamilton",
"team": "Mercedes",
"starting_position": 1,
"car_color": "Silver"},
{"name": "Max Verstappen",
"team": "Red Bull",
"starting_position": 2,
"car_color": "Red Bull"},
{"name": "Sergio Perez",
"team": "Red Bull",
"starting_position": 3,
"car_color": "Red Bull"},
{"name": "Lando Norris",
"team": "McLaren",
"starting_position": 4,
"car_color": "Papaya Orange"},
{"name": "Valtteri Bottas",
"team": "Mercedes",
"starting_position": 5,
"car_color": "Silver"}
]
# Exercise
winner = simulate_formula_1_race(racers)
# Test
assert winner == "Lewis Hamilton"
# This is all irrelevant to winner asserting
assert racers[0]["car_color"] == "Silver"
assert racers[1]["car_color"] == "Red Bull"
assert racers[2]["car_color"] == "Red Bull"
assert racers[3]["car_color"] == "Papaya Orange"
assert racers[4]["car_color"] == "Silver"
assert racers[0]["car_model"] == "W12"
assert racers[1]["car_model"] == "RB16B"
assert racers[2]["car_model"] == "RB16B"
assert racers[3]["car_model"] == "MCL35M"
assert racers[4]["car_model"] == "W12"
El siguiente ejemplo sólo incluye información relevante a efectos de
prueba:
def test_formula_1_race():
# Setup
racers = [
{"name": "Lewis Hamilton", "starting_position": 1},
{"name": "Max Verstappen", "starting_position": 2},
{"name": "Sergio Perez", "starting_position": 3},
{"name": "Lando Norris", "starting_position": 4},
{"name": "Valtteri Bottas" "starting_position": 5},
]
# Exercise
winner = simulate_formula_1_race(racers)
# Test
assert winner == "Lewis Hamilton"
Puedes encontrar algunos patrones en aserciones innecesarias. Las
pruebas deben ser en prosa. Céntrate siempre en el lector. Podrías ser tú,
dentro de un par de meses.
Recetas relacionadas
Receta 20.5, "Perfeccionar aserciones genéricas"
20.11 Añadir cobertura para cada
solicitud de fusión
Problema
Has fusionado solicitudes sin incluir la cobertura.
Solución
Asegúrate de cubrir cada cambio de código con las pruebas
correspondientes.
Debate
Las solicitudes de fusión sin cobertura de pruebas disminuyen la calidad
general del sistema y perjudican la mantenibilidad. Cuando necesites
hacer un cambio, actualiza la especificación viva de tu código. En lugar de
generar documentación muerta sobre lo que hace tu código, debes
escribir un escenario de uso que lo cubra. Si cambias código que no tiene
pruebas, debes añadir cobertura. Supongamos que cambias código con
cobertura existente. ¡Tienes suerte! Puedes ir y cambiar tus pruebas rotas.
Aquí tienes un cambio funcional sin cobertura:
export function sayHello(name: string): string {
const lengthOfName = [Link];
- const salutation =
- `How are you ${name}?, I see your name has ${lengthOfName} letters!
`;
+ const salutation =
+ `Hello ${name}, I see your name has ${lengthOfName} letters!`;
return salutation;
}
Este es el aspecto después de añadir las pruebas necesarias:
export function sayHello(name: string): string {
const lengthOfName = [Link];
- const salutation = 'How are you ${name}?,'
- 'I see your name has ${lengthOfName} letters!';
+ const salutation = `Hello ${name},'
+ 'I see your name has ${lengthOfName} letters!';
return salutation;
}
import { sayHello } from './hello';
test('given a name produces the expected greeting', () => {
expect(sayHello('Alice')).toBe(
'Hello Alice, I see your name has 6 letters!'
);
});
Como excepción, si tu código y tu arnés de pruebas viven en repositorios
diferentes, podrías tener pull requests diferentes. La cobertura de las
pruebas es tan importante como el código funcional. El sistema de
pruebas es tu primer y más fiel cliente, y tienes que cuidarlo.
Recetas relacionadas
Receta 8.5, "Convertir comentarios en nombres de funciones"
20.12 Reescribir los exámenes en
función de las fechas
Problema
Afirmas algo que ocurrirá en un futuro próximo.
Solución
Las pruebas deben tener un control ambiental total (ver Receta 18.5,
"Modificar la creación global de fechas") y no puedes gestionar el tiempo,
por lo que debes eliminar este tipo de condiciones.
Debate
Las pruebas que afirman fechas fijas son un caso especial de pruebas no
deterministas. Violan el principio de la menor sorpresa (ver Receta 5.6,
"Congelar constantes mutables") y pueden fallar inesperadamente,
rompiendo el proceso CI/CD. Como siempre, las pruebas deben tener un
control total del entorno. Si añades una fecha fija para comprobar un
evento futuro (como la eliminación de una bandera de función), la prueba
fallará de forma impredecible, impidiendo la publicación e impidiendo
que otros desarrolladores confirmen sus cambios. También hay otros
malos ejemplos: llegar a una fecha determinada, pruebas que se ejecutan
a medianoche, zonas horarias diferentes, etc.
Aquí tienes una afirmación sobre una fecha fija:
class DateTest {
@Test
void testNoFeatureFlagsAfterFixedDate() {
LocalDate fixedDate = [Link](2023, 4, 4);
LocalDate currentDate = [Link]();
[Link]([Link](fixedDate) ||
![Link]());
}
}
Éste es el aspecto cuando eliminas la dependencia de la fecha y añades la
prueba sólo cuando la condición es verdadera:
class DateTest {
@Test
void testNoFeatureFlags() {
[Link]([Link]());
}
}
ADVERTENCIA
Puedes comprobar las afirmaciones basadas en el tiempo en tus pruebas. Pero
debes proceder con cautela con las pruebas y las fechas. A menudo son fuente de
errores.
Recetas relacionadas
Receta 20.6, "Cómo eliminar las pruebas con escamas"
20.13 Aprender un nuevo lenguaje de
programación
Problema
Tienes que aprender un nuevo lenguaje e implementar en él un programa
"Hola Mundo".
Solución
En lugar de empezar el tutorial con mal pie utilizando un acceso global
como las consolas, deberías escribir una prueba fallida y corregirla.
Debate
El programa "Hola Mundo" suele ser la primera instrucción que aprenden
los principiantes cuando inician su andadura en la programación. Utiliza
acceso global como la consola (ver Capítulo 18, "Globales"), y no puedes
comprobar si el resultado es correcto ya que tiene efectos secundarios
(ver Receta 5.7, "Eliminar efectos secundarios"). Además, no puedes
comprobar si tu solución sigue funcionando, ya que no escribes pruebas
automatizadas para ella.
Esta es la primera instrucción habitual:
[Link]("Hello, World!");
Deberías escribir este código en su lugar:
function testFalse() {
expect(false).toBe(true);
}
Una vez que tienes una prueba que falla, puedes iniciar tu andadura en
TDD (ver Receta 4.8, "Eliminar propiedades innecesarias") y desarrollar
soluciones de software sorprendentes.
Ver también
Colección Hola Mundo
Capítulo 21. La deuda técnica
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y
comentarios: translation-feedback@[Link]
Puedes pensar en la deuda técnica como una analogía con la fricción en los
dispositivos mecánicos; cuanta más fricción experimente un dispositivo
debido al desgaste, la falta de lubricación o un mal diseño, más difícil será
moverlo y más energía tendrás que aplicar para conseguir el efecto
original. Al mismo tiempo, la fricción es una condición necesaria para que
las piezas mecánicas funcionen juntas. No puedes eliminarla por completo;
sólo puedes reducir su impacto.
Philippe Kruchten, Robert Nord e Ipek Ozkaya, Gestión de la deuda
técnica: reducir las fricciones en el desarrollo de software
21.0 Introducción
Evitar la deuda técnica es crucial en el desarrollo de software. Afecta a
muchos atributos de calidad, como la legibilidad, la capacidad de
mantenimiento, la escalabilidad, la fiabilidad, el coste a largo plazo, las
revisiones del código, la colaboración, la reputación y la satisfacción del
cliente. Hace que el código sea más difícil de entender, modificar y
mantener, lo que disminuye la productividad y la moral. Abordar la
deuda técnica desde el principio garantiza una mayor calidad del código,
una mejor escalabilidad y adaptabilidad del sistema, al tiempo que
minimiza el riesgo de fallos y brechas de seguridad. Al dar prioridad al
código limpio y minimizar la deuda técnica, entregarás software fiable,
fomentarás la colaboración eficaz y mantendrás una reputación positiva,
lo que en última instancia conducirá a una mayor satisfacción del cliente
y al éxito empresarial.
El ciclo de desarrollo de software no termina una vez que el código está
funcionando. Un código limpio debe funcionar correctamente en todas las
etapas. Diseñar un proceso para crear código de calidad en las fases de
producción es ahora más importante que nunca, ya que vas a enviar a
producción más rápido que nunca, aunque la mayoría de los sistemas
sean de misión crítica.
DEUDA TÉCNICA
Ladeuda técnica se refiere al aumento del coste de mantener y mejorar los
sistemas de software a lo largo del tiempo debido a malas prácticas de desarrollo
o elecciones de diseño. Al igual que la deuda financiera acumula intereses con el
tiempo, la deuda técnica se acumula a medida que los desarrolladores toman
atajos, hacen concesiones en el diseño o no abordan adecuadamente los
problemas de la base de código del software. Acabas pagando más por los
intereses acumulados que por el capital inicial.
21.1 Eliminar el código dependiente de
la producción
Problema
Tienes código que funciona de forma diferente en las fases de producción.
Solución
No añadas ifs que comprueben el entorno de producción. Y evita añadir
condicionales relacionados con la producción.
Debate
El código dependiente de la producción viola el principio "fail fast", ya que
no falla antes de ejecutar el código en producción. También carece de
testabilidad, a menos que puedas emular el entorno de producción. Si el
código dependiente de producción es completamente necesario para ti,
puedes modelar entornos y probarlos todos. A veces, necesitas crear
comportamientos diferentes en desarrollo y en producción, por ejemplo,
la fortaleza de las contraseñas. En este caso, debes configurar el entorno
con la estrategia de fortaleza y probar la estrategia, no el propio entorno.
Este código se basa en una constante global codificada:
def send_welcome_email(email_address, environment):
if ENVIRONMENT_NAME == "production":
print("Sending a welcome email to {email_address} "
"from Bob Builder <bob@[Link]>")
else:
print("Emails are sent only on production")
send_welcome_email("john@[Link]", "development")
# Nothing happens. Emails are sent only on production
send_welcome_email("john@[Link]", "production")
# Sending a welcome email to john@[Link]
# from Bob Builder <bob@[Link]>
Puedes hacer explícitos todos estos cambios, como en este ejemplo que
utiliza la Receta 14.1, "Sustitución de ifs accidentales por
polimorfismo", para eliminar el if accidental:
class ProductionEnvironment:
FROM_EMAIL = "Bob Builder <bob@[Link]>"
class DevelopmentEnvironment:
FROM_EMAIL = "Bob Builder Development <bob@[Link]>"
# You can unit test environments
# and even implement different sending mechanisms
def send_welcome_email(email_address, environment):
print("Sending a welcome email to {email_address}"
" from {environment.FROM_EMAIL}")
# You can delegate to a fake sender (and possible logger)
# and unit test it
send_welcome_email("john@[Link]", DevelopmentEnvironment())
# Sending a welcome email to john@[Link]
# from Bob Builder Development <bob@[Link]>
send_welcome_email("john@[Link]", ProductionEnvironment())
# Sending a welcome email to john@[Link]
# from Bob Builder <bob@[Link]>
Debes crear configuraciones de desarrollo/producción vacías y delegarlas
con objetos polimórficos personalizables. Debes evitar añadir
condicionales no comprobables. En su lugar, crea configuraciones que
deleguen reglas de negocio. Utiliza abstracciones, protocolos e interfaces,
y evita las jerarquías rígidas.
Recetas relacionadas
Receta 23.3, "Eliminar preprocesadores"
21.2 Eliminar rastreadores de defectos
Problema
Utilizas un rastreador de defectos para gestionar los problemas conocidos.
Solución
Todo software tiene una lista de defectos conocidos. Intenta evitar su
seguimiento arreglándolos.
Debate
Los rastreadores de defectos son listas difíciles de rastrear y generan
deuda técnica y funcional. Tienes que dejar de llamar bugs a estos
defectos (véase la Receta 2.8, "El único principio de diseño de software").
Reproduce el defecto. Cubre el escenario con una prueba y luego haz la
corrección más directa (incluso soluciones de hardcoding). Por último,
refactoriza la solución cuando sea necesario. Así es como funciona la
metodología TDD (véase la Receta 4.8, "Eliminar propiedades
innecesarias"). A muchos desarrolladores no les gusta que les
interrumpan, así que crean listas y retrasan las correcciones y soluciones.
Pero esto es un síntoma de un problema mayor; deberías poder cambiar
el software con facilidad. Si te ves incapaz de realizar arreglos y
correcciones rápidas sin recurrir a listas de cosas por arreglar, indica que
necesitas mejorar tu proceso de desarrollo de software.
Aquí tienes un defecto documentado:
function divide($numerator, $denominator) {
return $numerator / $denominator;
// FIXME denominator value might be 0
// TODO Rename function
}
Y esto es lo que parece si lo abordas inmediatamente:
function integerDivide($numerator, $denominator) {
if ($denominator == 0) {
throw new DivideByZeroException();
}
return $numerator / $denominator;
}
// You pay your debts
Desaconseja los rastreadores de incidencias en la parte de ingeniería. Por
supuesto, los clientes necesitan hacer un seguimiento de sus
descubrimientos y tú necesitas abordarlos lo antes posible, así que está
bien tener rastreadores de relaciones con los clientes.
Recetas relacionadas
Receta 21.4, "Evitar y eliminar ToDos y FixMes"
Ver también
"Lista de errores de software" en Wikipedia
21.3 Suprimir
Advertencia/Desactivación Estricta
Problema
Tienes desactivadas las advertencias en el entorno de producción.
Solución
Los compiladores y las luces de advertencia están ahí para ayudar. No los
ignores. Actívalos siempre, incluso en entornos de producción.
Debate
Si ignoras las advertencias, pasarás por alto los errores y el efecto dominó
de sus consecuencias, violando así el principio " Fail Fast" (ver Capítulo
13, "Fail Fast"). La solución es activar todas las advertencias y habilitar las
precondiciones y aserciones en producción para seguir las metodologías
de diseño por contrato (consulta la Receta 13.2, "Hacer cumplir las
precondiciones").
Aquí puedes ver una advertencia desactivada:
undefinedVariable = 310;
[Link](undefinedVariable); // Output: 310
delete x; // No error you can delete undefinedVariable
Cuando activas el modo estricto:
'use strict'
undefinedVariable = 310;
[Link](undefinedVariable); // undefinedVariable is not defined
delete undefinedVariable ; // Delete of an unqualified identifier in
strict mode
La mayoría de las lenguas tienen niveles de advertencia. Deberías activar
la mayoría de ellos. Deberías ejecutar linters para analizar estáticamente
tu código en busca de problemas potenciales, ya que, si ignoras las
advertencias y el código sigue adelante, tarde o temprano fallará. Si el
software falla más tarde, te resultará más difícil encontrar la causa raíz y
es probable que el defecto esté cerca de la primera advertencia, lejos del
fallo. Si sigues la teoría de las ventanas rotas, no debes tolerar ninguna
advertencia, para que un nuevo problema no pase desapercibido en un
mar de advertencias toleradas.
TEORÍA DE LAS VENTANAS ROTAS
La teoría de las ventanas rotas sugiere que los problemas o defectos pequeños
y aparentemente insignificantes pueden conducir a problemas mayores y más
graves más adelante. Si un desarrollador advierte un pequeño problema en el
código pero decide ignorarlo porque ya hay otras ventanas rotas, esto puede
conducir a una cultura de negligencia y a una falta de atención a los detalles en
el proceso de desarrollo.
Recetas relacionadas
Receta 15.1, "Crear objetos nulos"
Receta 17.7, "Eliminar argumentos opcionales"
Ver también
"Using Strict Mode to Catch Common Mistakes" en JavaScript Cookbook,
3ª edición, de Adam D. Scott et al.
El arte del PHP moderno 8 de Joseph Edmonds y Lorna Jane Mitchell
21.4 Prevenir y eliminar ToDos y
FixMes
Problema
Insertas ToDos o FixMes en tu código y aumentas tu deuda técnica.
Solución
No dejes ToDos en tu código. ¡Arréglalos!
Debate
Debes mantener pequeña la deuda técnica (como cualquier otra deuda).
Añadir ToDos y FixMes a tu código no es una buena receta. Debes hacer
frente a la deuda porque, con el tiempo, empezarás a deber la deuda
técnica. Muy probablemente, pagarás la deuda más los intereses, y al cabo
de unos meses, estarás pagando más intereses que la deuda original.
Aquí tienes un ejemplo con un ToDo que pondrás en práctica en el futuro:
public class Door
{
private Boolean isOpened;
public Door(boolean isOpened)
{
[Link] = isOpened;
}
public void openDoor()
{
[Link] = true;
}
public void closeDoor()
{
// TODO: Implement close door and cover it
}
}
Debes ocuparte de ello inmediatamente para evitar la deuda técnica:
public class Door
{
private Boolean isOpened;
public Door(boolean isOpened)
{
[Link] = isOpened;
}
public void openDoor()
{
[Link] = true;
}
public void closeDoor()
{
[Link] = false;
}
}
Puedes contar ToDos, ya que la mayoría de los linieros lo hacen, o puedes
crear tus propias herramientas. A continuación, crea políticas para
reducirlos. Si utilizas TDD (véase la Receta 4.8, "Eliminar propiedades
innecesarias"), escribe una prueba de fallo que falte en lugar de un ToDo,
y luego impleméntalo enseguida. En el contexto de TDD, los ToDos sólo
son válidos cuando se hace un desarrollo en profundidad para recordar
los caminos abiertos que hay que visitar.
Recetas relacionadas
Receta 9.6, "Arreglar ventanas rotas"
Receta 21.2, "Eliminar rastreadores de defectos"
Capítulo 22. Excepciones Excepciones
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y
comentarios: translation-feedback@[Link]
La optimización obstaculiza la evolución. Todo debe construirse de arriba
abajo, excepto la primera vez. La simplicidad no precede a la complejidad,
sino que la sigue.
Alan Perlis
22.0 Introducción
Las excepciones son un mecanismo asombroso para favorecer el código
limpio separando los buenos casos de uso de los errores y tratando estos
últimos con elegancia. Lamentablemente, algunos lenguajes de moda
como Go decidieron, en nombre de la optimización prematura, utilizar el
viejo mecanismo del código de retorno, forzando un montón de
condiciones if (que muchos desarrolladores olvidan) y proporcionando
únicamente gestores de excepciones catchall de alto nivel.
Las excepciones son tu mejor herramienta para separar las
preocupaciones y te ayudan a separar el buen camino del excepcional,
incluso en situaciones imprevistas. Crean un buen control del flujo y
fallan rápidamente. No obstante, siguen requiriendo una consideración
meditada y un manejo adecuado para garantizar su eficacia y evitar
posibles escollos.
22.1 Eliminar bloques de excepción
vacíos
Problema
Tienes código que ignora algunas excepciones.
Solución
No ignores las excepciones. Manéjalas.
Debate
"En caso de error, reanudar siguiente" era una práctica muy común hace
algunos años. Esto violaba el principio "Fail Fast" (ver Capítulo 13 "Fail
Fast") y creaba un efecto dominó. Deberías atrapar la excepción y tratarla
explícitamente. He aquí un ejemplo en el que se ignoran las excepciones:
import logging
def send_email():
print("Sending email")
raise ConnectionError("Oops")
try:
send_email()
except:
# AVOID THIS
pass
Esto es lo que parece cuando tratas con ellos:
import logging
logger [Link](__name___)
try:
send_email()
except ConnectionError as exception:
[Link]("Cannot send email {exception}")
Muchos linters te advierten sobre los bloques de excepción vacíos. Si en
algún caso legítimo necesitas omitir e ignorar la excepción, debes
documentarlo explícitamente. Prepárate para hacer frente a los errores.
Incluso si decides no hacer nada, debes ser explícito con esta decisión.
Recetas relacionadas
Receta 22.8, "Limitar los intentos de excepción"
Ver también
"on-error-resume-next" paquete
22.2 Eliminar excepciones innecesarias
Problema
Tienes excepciones vacías.
Solución
Es muy agradable tener muchas excepciones diferentes. Tu código es
declarativo y robusto. Pero no crees objetos anémicos y vacíos, aunque
sean excepciones.
Debate
Las excepciones vacías son un síntoma de sobrediseño y provocan
contaminación del espacio de nombres. Debes crear excepciones sólo si se
comportan de forma diferente a las existentes. Modela las excepciones
con objetos. Las clases son una trampa para los programadores perezosos.
Aquí puedes ver muchas excepciones vacías:
public class FileReader {
public static void main(String[] args) {
FileReader file = null;
try {
file = new FileReader("[Link]");
[Link]();
}
catch(FileDoesNotExistException e) {
[Link]();
}
catch(FileLockedException e) {
[Link]();
}
catch(FilePermissionsException e) {
[Link]();
}
catch(Exception e) {
[Link]();
}
finally {
try {
[Link]();
}
catch(CannotCloseFileException e) {
[Link]();
}
}
}
}
Esto es más compacto:
public class FileReader {
public static void main(String[] args) {
FileReader file = null;
try {
file = new FileReader("[Link]");
[Link]();
}
catch(FileException exception) {
if ([Link] ==
([Link]().errorDescriptionFileTemporary
Locked() {
// sleep and retry
// IF behavior is the same with all the exceptions
// just change the text on
// object creation and raise the incorrect instance
}
[Link]([Link]();
// This example is simplified.
// You should translate the text
}
finally {
try {
[Link]();
} catch (IOException ioException) {
[Link]();
}
}
}
}
Las nuevas excepciones deben anular los métodos de comportamiento.
Tener código, descripción, reanudable, etc. no es de comportamiento. No
crearías clases distintas para cada instancia de Persona, para que
devuelvan nombres distintos. ¿Por qué lo harías con las excepciones?
¿Con qué frecuencia atrapas una excepción concreta? Sal y comprueba tu
código. ¿Es necesario que sea una clase? Ya está acoplado a la clase. En su
lugar, acóplate a la descripción. Las instancias de excepción no deben ser
singletons.
Recetas relacionadas
Receta 3.1, "Convertir objetos anémicos en objetos ricos"
Receta 19.9, "Migrar clases vacías"
22.3 Reescribir excepciones para casos
esperados
Problema
Utiliza excepciones para casos empresariales esperados y válidos.
Solución
No utilices excepciones para el flujo de control.
Debate
Las excepciones son como los GoTos y las banderas (véase la Receta 18.3,
"Sustituir los GoTos por código estructurado"). Utilizarlas para casos
normales perjudica la legibilidad y viola el principio de la menor sorpresa
(ver Receta 5.6, "Congelar constantes mutables"). Debes utilizar
excepciones sólo para situaciones inesperadas. Las excepciones sólo
deben manejar violaciones de contratos (ver Receta 13.2, "Hacer cumplir
las condiciones previas").
Aquí tienes un ejemplo de bucle infinito roto por una condición límite:
try {
for (int index = 0;; index++)
array[i]++;
} catch (ArrayIndexOutOfBoundsException exception) {}
// Endless loop without end condition
Esto es más declarativo, ya que llegar al final de un bucle es un caso
esperado:
for (int index = 0; index < [Link]; index++)
array[index]++;
// index < [Link] breaks execution
Se trata de un olor semántico. A menos que utilices linters de aprendizaje
automático (véase la Receta 5.2, "Declarar variables para que sean
variables"), será muy difícil encontrar los errores. Las excepciones son
útiles, y sin duda deberías utilizarlas en lugar de los códigos de retorno.
La frontera entre el uso correcto y el incorrecto es borrosa, como tantos
principios de diseño.
Recetas relacionadas
Receta 22.2, "Eliminar excepciones innecesarias"
Receta 22.5, "Sustituir códigos de retorno por excepciones"
Ver también
"No utilices excepciones para el control de flujo" en C2 Wiki
"Por qué debes evitar utilizar excepciones como flujo de control en Java"
en DZone
22.4 Reescribir Try/Catches anidados
Problema
Tienes muchos try/catches anidados.
Solución
No anides excepciones. Es difícil seguir lo que haces en los bloques
interiores. Extrae el mecanismo de manejo a una clase o función
diferente.
Debate
Las excepciones son una forma estupenda de separar el camino feliz del
camino del error. Pero las soluciones demasiado complicadas perjudican
la legibilidad. Aquí tienes algunos try/catches anidados:
try {
[Link]();
} catch (exception) {
logerror(exception);
if (exception instanceOf DBError) {
try {
[Link]();
} catch (e) {
doMoreLoggingRollbackFailed(e);
}
}
}
// Nested try catches
// Exception cases are more important than the happy path
// You use exceptions as control flow
Puedes reescribirlos como sigue:
try {
[Link]();
} catch (transactionError) {
[Link](
transationError, transaction);
}
// Transaction error policy is not defined in this function
// so you don't have repeated code and code is more readable
// It is up to the transaction and the error to decide what to do
Puedes detectar este olor utilizando árboles de análisis sintáctico. No
abuses de las excepciones, no crees clases de excepciones que nadie vaya
a coger nunca, y no estés preparado para todos los casos (a menos que
tengas un buen escenario real con una prueba de cobertura). El camino
feliz debería ser siempre más importante que los casos excepcionales.
Recetas relacionadas
Receta 22.2, "Eliminar excepciones innecesarias"
Receta 22.3, "Reescribir excepciones para casos esperados"
Ver también
"Bloque anidado Try Catch en Java - Manejo de excepciones" en
BeginnersBook
22.5 Sustituir los Códigos de Retorno
por Excepciones
Problema
En lugar de excepciones, puedes utilizar devolver códigos.
Solución
No te devuelvas códigos a ti mismo. Plantea excepciones.
Debate
Las API y los lenguajes de bajo nivel utilizan códigos de retorno en lugar
de excepciones. Los códigos de retorno aportan casos if y switch
innecesarios, contaminando el código y la lógica empresarial de los casos
buenos. También añaden complejidad accidental y son propensos a tener
documentación obsoleta. Puedes cambiar los if, devolver excepciones
genéricas y distinguir la ruta feliz de la ruta de excepción.
Aquí tienes un código con un código de retorno:
function createSomething(arguments) {
// Magic Creation
success = false; // You failed to create
if (!success) {
return {
object: null,
httpCode: 403,
errorDescription: 'You don't have permission to
create...'
};
}
return {
object: createdObject,
httpCode: 201,
errorDescription: ''
};
}
var myObject = createSomething('argument');
if ([Link] !== 201) {
[Link]([Link] + ' ' + [Link])
}
// myObject does not hold My Object but an
// accidental auxiliary based on implementation
// from now on you need to remember this
Aquí tienes una comprobación explícita:
function createSomething(arguments) {
// Magic Creation
success = false; // You failed to create
if (!success) {
throw new Error('You don't have permission to create...');
}
return createdObject;
}
try {
var myObject = createSomething('argument');
// no ifs, just happy path
} catch (exception) {
// deal with it!
[Link]([Link]);
}
// myObject holds my expected object
Puedes enseñar a tus linters a encontrar patrones de retornos de enteros
y cadenas junto con ifs y comprobación de retornos. Como excepción,
debes utilizar IDs y códigos como identificadores externos. Son útiles
cuando se interactúa con un sistema externo (por ejemplo, una API REST).
No debes utilizarlos en tus propios sistemas ni en tus propias API internas.
Crea y lanza excepciones genéricas; sólo crea excepciones específicas si
estás preparado para manejarlas y tienen un comportamiento
especializado. No crees excepciones anémicas, y evita los lenguajes
de optimización inmaduros y prematuros(véase el Capítulo 16,
"Optimización prematura") que favorecen los códigos de retorno.
Recetas relacionadas
Receta 22.2, "Eliminar excepciones innecesarias"
Ver también
"Código limpio: Capítulo 7 - Tratamiento de errores" por Nicole Carpenter
22.6 Reescribir el Código de Flecha de
Excepción
Problema
Tienes código flecha en cascada para tratar las excepciones.
Solución
No hagas excepciones en cascada.
Debate
El código de flechas es un olor a código (ver Receta 14.8, "Reescribir código
de flechas condicional"). La contaminación por excepciones es otra. Es
una combinación mortal que perjudica la legibilidad y aporta
complejidad. Puedes reescribir las cláusulas anidadas. En este ejemplo
puedes ver una cascada de excepciones:
class QuotesSaver {
public void Save(string filename) {
if ([Link](filename)) {
if ([Link](filename)) {
if () {
[Link](filename);
} else {
throw new IOException("File exists: " + filename);
}
} else {
throw new IOException("Parent directory missing at " +
filename);
}
} else {
throw new IllegalArgumentException("Invalid path " +
filename);
}
}
}
Esto es más legible:
public class QuotesSaver {
public void Save(string filename) {
if () {
throw new ArgumentException("Invalid path " + filename);
} else if () {
throw new I0Exception("Parent directory missing at " +
filename);
} else if ([Link](filename)) {
throw new I0Exception("File exists: " + filename);
}
[Link](filename);
}
}
Las excepciones son menos críticas que los casos normales. Si tienes que
leer más código excepcional de lo normal, es hora de mejorar tu código.
Recetas relacionadas
Receta 14.10, "Reescribir código de flechas anidado"
Receta 22.2, "Eliminar excepciones innecesarias"
22.7 Ocultar los errores de bajo nivel a
los usuarios finales
Problema
Puedes mostrar mensajes de bajo nivel a los usuarios finales.
Solución
Detecta tus errores. Incluso los que no esperas.
Debate
¿Has visto este mensaje en algún sitio web?
'Error fatal: Error no detectado: Clase 'logs_queries_web' no encontrada
en /var/www/html/[Link] Rastreo de pila: #0 {main} thrown in
/var/www/html/[Link] on line 718'
Esto es una mala gestión de errores y puede causar problemas de
seguridad. También es un mal caso de UX. Siempre debes utilizar un
manejador de nivel superior y evitar los lenguajes que favorecen los
códigos de retorno (véase la Receta 22.5, "Sustituir los códigos de retorno
por excepciones"). Espera que haya errores de base de datos y de bajo
nivel que debas probar antes de enviar el código a producción. Hoy en día
no es raro observar sitios web "serios" que muestran mensajes de
depuración o un seguimiento de pila a los usuarios normales.
Aquí tienes un seguimiento de pila visible para los usuarios:
Fatal error: Uncaught Error: Class 'MyClass'
not found in /nstest/src/[Link]
Este es el aspecto después de instalar un gestor de errores de nivel
superior:
// A user-defined exception handler function
function myException($exception) {
logError($exception->description())
// You don't show Exception to final users
// This is a business decision
// You can also show a generic user message
}
// Set user-defined exception handler function
set_exception_handler("myException");
Puedes utilizar las pruebas de mutación (ver Receta 5.1, "Cambiar var por
const") para simular problemas y ver si se gestionan correctamente.
Asegúrate de que tus soluciones no son chapuceras para proteger tu
reputación como ingeniero de software serio.
Recetas relacionadas
Receta 22.5, "Sustituir códigos de retorno por excepciones"
22.8 Intentos de excepción de
estrechamiento
Problema
Tienes muchos intentos de excepción.
Solución
Sé lo más específico posible al tratar los errores.
Debate
Las excepciones son útiles. Pero deben ser estrechas para favorecer el
principio de "fail fast", evitando omitir errores y falsos negativos. Debes
limitar los manejadores de excepciones seleccionando secciones de código
lo más pequeñas posible y siguiendo el principio de "Lanzar pronto y
atrapar tarde".
He aquí un ejemplo con un amplio intento:
import calendar, datetime
try:
birthYear= input('Birth year:')
birthMonth= input('Birth month:')
birthDay= input('Birth day:')
# you don't expect the above to fail
print([Link](int(birthYear), int(birthMonth),
int(birthDay)))
except ValueError as e:
if str(e) == 'month must be in 1..12':
print('Month ' + str(birthMonth) +
' is out of range. The month must be a number in
1...12')
elif str(e) == 'year {0} is out of range'.format(birthYear):
print('Year ' + str(birthYear) +
' is out of range. The year must be a number in ' +
str([Link]) + '...' +
str([Link]))
elif str(e) == 'day is out of range for month':
print('Day ' + str(birthDay) +
' is out of range. The day must be a number in 1...'
+
str([Link](birthYear, birthMonth)))
Este es el aspecto después de reducir el bloque de prueba:
import calendar, datetime
# You might add specialized tries dealing
# with errors from the following 3 statements
birthYear= input('Birth year:')
birthMonth= input('Birth month:')
birthDay= input('Birth day:')
# try scope should be narrow
try:
print([Link](int(birthYear), int(birthMonth),
int(birthDay)))
except ValueError as e:
if str(e) == 'month must be in 1..12':
print('Month ' + str(birthMonth) + ' is out of range. '
'The month must be a number in 1...12')
elif str(e) == 'year {0} is out of range'.format(birthYear):
print('Year ' + str(birthYear) + ' is out of range. '
'The year must be a number in ' +
str([Link]) + '...' +
str([Link]))
elif str(e) == 'day is out of range for month':
print('Day ' + str(birthDay) + ' is out of range. '
'The day must be a number in 1...' +
str([Link](birthYear, birthMonth)))
Si tienes un conjunto de pruebas lo suficientemente bueno, puedes
realizar pruebas de mutación (véase la Receta 5.1, "Cambiar var por
const") para reducir al máximo el ámbito de la excepción. Debes hacer
excepciones de la forma más quirúrgica que permita tu código.
LANZA PRONTO Y ATRAPA TARDE
"Throw early and catch late" hace hincapié en detectar y tratar los errores o
excepciones tan pronto como sea posible en el código, y aplazar su tratamiento o
notificación real hasta un nivel superior o un contexto más adecuado. Debes
manejar los errores lo más tarde posible, en un lugar donde dispongas de más
información contextual, en lugar de tomar decisiones localizadas con
información incompleta.
Recetas relacionadas
Receta 22.2, "Eliminar excepciones innecesarias"
Receta 22.3, "Reescribir excepciones para casos esperados"
Capítulo 23. Metaprogramación
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y
comentarios: translation-feedback@[Link]
El software es como la entropía: Es difícil de comprender, no pesa nada y
obedece a la Segunda Ley de la Termodinámica; es decir, siempre aumenta.
Norman Agustín
23.0 Introducción
La metaprogramación se refiere a la capacidad de un lenguaje de
programación para permitir la manipulación, generación y modificación
del código en tiempo de ejecución. Es fascinante. Una vez que la
descubras, verás que es la herramienta que resuelve todos los problemas.
Pero no es una bala de plata (véase la Receta 4.1, "Crear objetos
pequeños") y tampoco es gratis. Pensar que estás creando algún tipo de
magia es la razón principal por la que no debes utilizarla.
La metaprogramación es como los patrones de diseño con estados de
excitación similares:
1. Llega a conocerlo.
2. No lo entiendes del todo.
3. Estúdialo a fondo.
4. Tú lo dominas.
5. Parece que lo encuentras en casi todas partes.
6. Abusas de él (ver Receta 12.5 , "Eliminar los abusos del patrón de
diseño") pensando que es tu nueva bala de plata (ver Receta 4.1,
"Crear objetos pequeños").
7. Aprendes a evitarlo.
23.1 Eliminar el uso de la
metaprogramación
Problema
Utilizas la metaprogramación.
Solución
Cambiar el uso de la metaprogramación, favoreciendo las soluciones
directas.
Debate
Cuando utilizas la metaprogramación, hablas del metalenguaje y del
metamodelo. Se trata de aumentar el nivel de abstracción hablando por
encima de los objetos del dominio del problema. Esta capa adicional te
permite razonar y pensar sobre la relación entre las entidades de la
realidad en un lenguaje de nivel superior. Al hacerlo, rompes la biyección
que debes utilizar para observar la realidad, ya que en el mundo real no
hay modelos ni metamodelos, sólo entidades empresariales de las que
hablas. Cuando abordas un problema empresarial en la vida real, te
resulta muy difícil justificar las referencias a metaentidades, porque tales
metaentidades no existen (véase la Figura 23-1), lo que significa que no te
mantienes fiel a la única regla de utilizar una biyección entre tus objetos y
la realidad.
Figura 23-1. El metamodelo está ausente en el mundo real
Te resultará muy difícil justificar la presencia de tales objetos adicionales
y responsabilidades inexistentes en el mundo real. Uno de los principios
de diseño más importantes es el principio abierto-cerrado, que forma
parte de la definición del diseño SOLID (véase la Receta 19.1, "Romper la
herencia profunda"). La regla de oro establece que un modelo debe estar
abierto a la extensión y cerrado a la modificación.
Esta regla sigue siendo cierta y es algo que debes intentar enfatizar en tus
modelos. Sin embargo, en muchas implementaciones, te encuentras con
que la forma de hacer que estos modelos sean abiertos es dejar la puerta
abierta utilizando subclases.
Como implementación de una extensión, el mecanismo parece muy
robusto a primera vista, pero genera acoplamiento. Al vincular la
definición de dónde obtener los posibles casos, aparece una referencia
inocente a una clase y sus subclases, que es la parte que podría cambiar
dinámicamente (la extensión).
He aquí un ejemplo de una jerarquía de analizadores sintácticos
polimórficos :
public abstract class Parser {
public abstract boolean canHandle(String data);
public abstract void handle();
}
public class XMLParser extends Parser {
public static boolean canHandle(String data) {
return [Link]("<xml>");
}
public void handle() {
[Link]("Handling XML data...");
}
}
public class JSONParser extends Parser {
public static boolean canHandle(String data) {
try {
new JSONObject(data);
return true;
} catch (JSONException e) {
return false;
}
}
public void handle() {
[Link]("Handling JSON data...");
}
}
public class CSVParser extends Parser {
public static boolean canHandle(String data) {
return [Link](",");
}
public void handle() {
[Link]("Handling CSV data...");
}
}
El algoritmo pide a la clase Parser que interprete cierto contenido. La
solución es delegar en todas sus subclases hasta que una de ellas acepte
que puede interpretarlo y se encargue de continuar con esa
responsabilidad. Este mecanismo es un caso particular del patrón de
diseño de cadena de responsabilidad.
CADENA DE RESPONSABILIDAD
Lacadena de responsabilidad permite que varios objetos gestionen una
solicitud de forma encadenada, sin saber qué objeto de la cadena gestiona
específicamente la solicitud. En este patrón, la solicitud pasa por una serie de
gestores hasta que uno de ellos la gestiona o hasta que llega al final de la cadena.
Ten en cuenta que los eslabones de la cadena están desacoplados entre sí.
Sin embargo, este modelo tiene varios inconvenientes:
Genera una dependencia de la clase Parser, que es el punto de
entrada de esta responsabilidad (ver Receta 18.4, "Eliminar
clases globales").
Utiliza subclases con metaprogramación; por tanto, al no haber
referencias directas, sus usos y referencias no serán evidentes.
Como no hay referencias y usos evidentes, no se puede realizar
una refactorización directa; es difícil conocer todos los usos y
evitar eliminaciones accidentales.
Este problema es común a todos los marcos de trabajo de código abierto.
El más conocido y popular de ellos es la familia xUnit y todos sus
derivados. Las clases son variables globales y, por tanto, generan
acoplamiento y no son la mejor forma de abrir un modelo. Veamos un
ejemplo de cómo puedes abrirlo declarativamente utilizando el principio
abierto-cerrado.
Eliminas de la referencia directa de a la clase Parser y generas una
dependencia a un proveedor de análisis sintáctico utilizando la inversión
de dependencia (la D de los principios SOLID; véase la receta 12.4,
"Eliminar interfaces de un solo uso"). En distintos entornos (producción,
pruebas, configuración) utilizas distintos proveedores de análisis
sintáctico; estos entornos no pertenecen necesariamente a la misma
jerarquía. Utilizas el acoplamiento declarativo y pides a estos proveedores
que realicen la interfaz ParseHandling.
El problema más grave que tienes al utilizar la metaprogramación es
tener referencias oscuras a clases y métodos, lo que te impedirá todo tipo
de refactorizaciones y, por tanto, te impedirá hacer crecer el código
aunque tengas una cobertura del 100%. Al perder la cobertura de todos
los casos posibles puedes perder algún caso de uso que esté referenciado
de forma indirecta y oscura y que no será alcanzado por tus búsquedas y
refactorizaciones de código, generando errores indetectables en
producción. El código debe ser limpio, y transparente, y tener el menor
número posible de metareferencias, ya que puede que no sean alcanzadas
por alguien que pueda alterar ese código.
He aquí un ejemplo de construcción de un nombre de función dinámico:
$selector = 'getLanguage' . $this->languageCode;
Reflection::invokeMethod($selector, $object);
Si estás en un cliente configurado en italiano, la llamada invocará al
método getLanguageIt(). El problema con esta referencia oscura es el
mismo que el mencionado en el ejemplo del analizador sintáctico. Este
método aparentemente no tiene referencias, no se puede refactorizar, no
se puede determinar quién lo utiliza, tiene una cobertura poco clara, etc.
En estos casos, puedes evitar estos conflictos con una dependencia
explícita (incluso utilizando tablas de asignación o referencias rígidas) sin
necesidad de metaprogramar magia negra.
Hay algunas excepciones con un denominador común. Cuando crees
el MAPPER, debes mantenerte lo más alejado posible de los aspectos
accidentales ajenos al negocio. Entre estos aspectos están la persistencia,
la serialización de entidades, la impresión o "visualización/renderización"
en interfaces de usuario, las pruebas o aserciones, etc. Estos problemas
pertenecen al dominio ortogonal del modelo computable y no son
particulares de ningún negocio. Interferir en las responsabilidades de un
objeto es una violación de su contrato y de sus responsabilidades. En
lugar de añadir "capas accidentales" de responsabilidades, puedes
abordar esto utilizando la metaprogramación.
Pero ten en cuenta que la metaprogramación es algo que debes evitar a
toda costa si tienes la opción de utilizar abstracciones que existan en el
mundo real. La búsqueda de tales abstracciones requiere una
comprensión mucho más profunda del nuevo dominio empresarial.
Puedes utilizar la Receta 25.5, "Proteger la deserialización de
objetos", para conocer las vulnerabilidades asociadas a la
metaprogramación.
23.2 Reificar funciones anónimas
Problema
Utilizas demasiadas funciones anónimas.
Solución
No abuses de los cierres y las funciones. Encapsúlalas en objetos.
Debate
Las funciones anónimas, lambdas, funciones flecha o cierres son difíciles
de mantener y probar. El código es difícil de rastrear y, por tanto, de
reutilizar. Es más difícil leer y localizar el código fuente, la mayoría de los
IDE y depuradores tienen problemas para mostrar el código real, estas
funciones rara vez se reutilizan y violan el principio de ocultación de
información. Si la función no es trivial, puedes envolverla y reificar
algoritmos utilizando la Receta 10.7, "Extraer un método a un objeto".
Aquí tienes una función no tan declarativa:
sortFunction = function(arr, fn) {
var len = [Link];
for (var i = 0; i < len ; i++) {
for(var j = 0 ; j < len - i - 1; j++) {
if (fn(arr[j], arr[j+1])) {
var temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
return arr;
}
scores = [9, 5, 2, 7, 23, 1, 3];
sorted = sortFunction(scores, (a,b) => {return a > b});
Este es su aspecto después de reificarlo y encapsularlo en un objeto:
class ElementComparator{
greatherThan(firstElement, secondElement) {
return firstElement > secondElement;
// This is just an example.
// With more complex objects this comparison might not be trivial
}
}
class BubbleSortingStrategy {
// You have a strategy, you can't unit test it, change for a
polymorphic,
// Swap and benchmark algorithms etc.
constructor(collection, comparer) {
this._elements = collection;
this._comparer = comparer;
}
sorted() {
for (var outerIterator = 0;
outerIterator < [Link]();
outerIterator++) {
for(var innerIterator = 0 ;
innerIterator < [Link]() - outerIterator - 1;
innerIterator++) {
if (this._comparer.greatherThan(
this._elements[innerIterator], this._elements[
innerIterator + 1])) {
[Link](innerIterator);
}
}
}
return this._elements;
}
size() {
return this._elements.length;
}
swap(position) {
var temporarySwap = this._elements[position];
this._elements[position] = this._elements[position + 1];
this._elements[position + 1] = temporarySwap;
}
}
scores = [9, 5, 2, 7, 23, 1, 3];
sorted = new BubbleSortingStrategy(scores,new
ElementComparator()).sorted();
Una excepción es que los cierres y las funciones anónimas son muy útiles
para modelar bloques de código, promesas, etc. Sería difícil
desmenuzarlas en . Los humanos leemos código. El software funciona
bien con funciones anónimas, pero la mantenibilidad se ve comprometida
cuando se invocan varios cierres.
Recetas relacionadas
Receta 10.4, "Eliminar la astucia del código"
Receta 10.7, "Extraer un método a un objeto"
23.3 Eliminar preprocesadores
Problema
En puedes utilizar los preprocesadores de código .
Solución
Elimina los preprocesadores de tu código.
Debate
PREPROCESADORES
Un preprocesador realiza tareas en el código fuente antes de que sea compilado
o interpretado por el compilador o intérprete principal. Se utiliza habitualmente
en los lenguajes de programación para modificar o manipular el código fuente
antes de que se someta al proceso real de compilación o interpretación.
Quieres que tu código se comporte de forma diferente en distintos
entornos y sistemas operativos, por lo que tomar decisiones en tiempo de
compilación es la mejor decisión. Pero los preprocesadores perjudican la
legibilidad, ya que introducen una optimización prematura (véase el
Capítulo 16, "Optimización prematura") y una complejidad accidental
innecesaria, lo que complica la depuración. Debes eliminar todas las
directivas del compilador. Si quieres un comportamiento diferente,
modélalo con objetos, y si crees que hay una penalización de rendimiento,
haz un benchmark serio en lugar de hacer una optimización prematura.
Aquí tienes un ejemplo con código preprocesado:
#if VERBOSE >= 2
printf("Betelgeuse is becoming a supernova");
#endif
Este código no tiene instrucciones preprocesadas:
if (runtimeEnvironment->traceDebug()) {
printf("Betelgeuse is becoming a supernova");
}
## even better with polymorphism and to avoid ifs
runtimeEnvironment->traceDebug("Betelgeuse is becoming a supernova");
Se trata de una directiva sintáctica promovida por varios lenguajes, por lo
que es fácil de detectar y sustituir por un comportamiento real. Añadir
una capa extra de complejidad dificulta mucho la depuración. Esta técnica
se utilizaba cuando la memoria y la CPU eran escasas. Hoy en día,
necesitas código limpio y debes dejar la optimización prematura
enterrada en el pasado. Bjarne Stroustrup, en su libro The Design and
Evolution of C++, lamenta las directivas del preprocesador que creó
años antes.
Recetas relacionadas
Receta 16.2, "Eliminar la optimización prematura"
Ver también
"¿Estás diciendo que el preprocesador es malo?" en C++ estándar
"Preprocesador C" en Wikipedia
"#ifdef Considerado Perjudicial" por Harry Spencer y Geoff Collyer
23.4 Eliminar métodos dinámicos
Problema
Utiliza la metaprogramación para añadir dinámicamente propiedades y
métodos.
Solución
No añadas comportamiento dinámico con la metaprogramación.
Debate
La metaprogramación perjudica la legibilidad y la mantenibilidad. El
código es más difícil de depurar, ya que se genera dinámicamente en
tiempo de ejecución, y tiene posibles problemas de seguridad si el archivo
de configuración no se sanea adecuadamente. Deberías definir los
métodos a mano o utilizar el patrón de diseño decorador (consulta
la Receta 7.11, "Cambiar el nombre de las funciones básicas / hacer"). La
metaprogramación es una potente técnica que te permite escribir código
que puede generar, modificar o analizar otro código en tiempo de
ejecución. Sin embargo, puede dar lugar fácilmente a código difícil de
entender, mantener y depurar.
He aquí un ejemplo de carga dinámica de propiedades y métodos en
Ruby:
class Skynet < ActiveRecord::Base
# dynamically add some attributes based on a configuration file
YAML.load_file("[Link]")["attributes"].each do |attribute|
attr_accessor attribute
end
# define some dynamic methods based on a configuration file
YAML.load_file("[Link]")["methods"].each do |method_name,
method_body|
define_method method_name do
eval method_body
end
end
end
Ésta es la definición clásica sin carga dinámica:
class Skynet < ActiveRecord::Base
# define some attributes explicitly
attr_accessor :asimovsFirstLaw, :asimovsSecondLaw, :asimovsThirdLaw
# define some methods explicitly
def takeoverTheWorld
# implementation
end
end
Puedes establecer una lista de usos válidos permitidos o prohibir
directamente algunos métodos. La metaprogramación suele implicar el
uso de código complejo y abstracciones que pueden hacer que el código
resultante sea difícil de leer y mantener. Su uso dificulta que otros
desarrolladores comprendan y modifiquen el código en el futuro, lo que
aumenta la complejidad y los defectos.
Recetas relacionadas
Receta 23.1, "Eliminar el uso de la metaprogramación"
Receta 23.2, "Reificar funciones anónimas"
Receta 25.1, "Desinfección de entradas"
Capítulo 24. Tipos
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y
comentarios: translation-feedback@[Link]
Los tipos son esencialmente afirmaciones sobre un programa. Y creo que es
valioso que las cosas sean lo más absolutamente sencillas posible, incluso
no decir siquiera cuáles son los tipos.
Dan Ingalls, Programadores trabajando: Reflexiones sobre el oficio de
programar
24.0 Introducción
Los tipos son el concepto más importante en los lenguajes de
clasificación. Esto es cierto tanto para los lenguajes estáticos y
fuertemente tipados como para los dinámicamente tipados. Tratar con
ellos no es fácil y tienes muchos sabores, desde los muy restrictivos hasta
los más perezosos.
24.1 Eliminar la comprobación de tipo
Problema
Revisa tus argumentos.
Solución
Confía en tus colaboradores. No compruebes quiénes son. Pídeles que lo
hagan.
Debate
Evita kind(), isKindOf(), instance(), getClass(), typeOf(), etc., y no utilices
la reflexión ni la metaprogramación para objetos de dominio (consulta el
Capítulo 23, "Metaprogramación"). Evita la comprobación de indefinidos.
Utiliza objetos completos (consulta la Receta 3.7, "Completar constructores
vacíos"), y evita los nulos (consulta la Receta 15.1, "Crear objetos nulos") y
los setters. Favorece la inmutabilidad y nunca tendrás tipos indefinidos o
ifs accidentales.
Aquí tienes ejemplos de comprobación de tipos:
if (typeof(x) === 'undefined') {
[Link]('variable x is not defined');
}
function isNumber(data) {
return (typeof data === 'number');
}
Y aquí tienes un ejemplo completo de comprobación de tipos:
function move(animal) {
if (animal instanceof Rabbit) {
[Link]()
}
if (animal instanceof Seagull) {
[Link]()
}
}
class Rabbit {
run() {
[Link]("I'm running");
}
}
class Seagull {
fly() {
[Link]("I'm flying");
}
}
let bunny = new Rabbit();
let livingston = new Seagull();
move(bunny);
move(livingston);
Este es el aspecto que tiene cuando se refactoriza la página Animal:
class Animal { }
class Rabbit extends Animal {
move() {
[Link]("I'm running");
}
}
class Seagull extends Animal {
move() {
[Link]("I'm flying");
}
}
let bunny = new Rabbit();
let livingstone = new Seagull();
[Link]();
[Link]();
Como los métodos de comprobación de tipos son bien conocidos, es muy
fácil establecer una política de código que compruebe su uso. Comprobar
el tipo de una clase acopla los objetos a decisiones accidentales y viola la
biyección, ya que no existe tal control en el mundo real. Esto huele a que
tus modelos no son lo suficientemente buenos.
Recetas relacionadas
Receta 15.1, "Crear objetos nulos"
Receta 23.1, "Eliminar el uso de la metaprogramación"
24.2 Tratar con valores de verdad
Problema
Necesitas para tratar con valores de verdad contraintuitivos.
Solución
No mezcles booleanos con no booleanos. Ten mucho cuidado con los
valores verdaderos.
Debate
Algunas funciones no se comportan como se espera. La comunidad de
desarrolladores asume que éste es el comportamiento esperado, pero
viola el principio de la menor sorpresa (ver Receta 5.6, "Congelar
constantes mutables") y la biyección (como se define en el Capítulo 2) y
suele dar resultados inesperados. Los booleanos deben ser
sólo verdadero y falso. Como los valores verdaderos ocultan errores y
aportan una complejidad accidental unida a un lenguaje concreto,
también son más difíciles de leer y te impiden saltar entre lenguajes.
Tienes que ser explícito y trabajar con booleanos para las condiciones
booleanas. Ni enteros, ni nulos, ni cadenas, ni listas: sólo booleanos.
TRUTHY Y FALSY
Verdadero y falso son términos utilizados para describir el valor booleano de un
tipo de datos no booleano en muchos lenguajes de programación. Todo valor
puede evaluarse como verdadero o falso en un contexto booleano. Cuando un
valor no booleano se evalúa en un contexto booleano, se convierte mágicamente
en booleano sin previo aviso.
Repasa este ejemplo contraintuitivo e intenta encontrar el error:
[Link]([Link]() > [Link]());
// returns false
[Link]([Link]());
// returns -Infinite
En las lenguas mejor diseñadas, deberías obtener:
[Link]([Link]() > [Link]());
[Link]([Link]());
// returns Exception. Not enough arguments passed.
// Max and min require at least one argument
Estas funciones pertenecen a la biblioteca estándar de Matemáticas de
JavaScript. Por tanto, no son fáciles de evitar, y debes tener mucho
cuidado al utilizar funciones que violan conceptos del mundo real
utilizando trucos del lenguaje. Aquí puedes ver
más ejemplos contraintuitivos:
!true // returns false
!false // returns true
isActive = true
!isActive // returns false
age = 54
!age // returns false
array = []
!array // returns false
obj = new Object;
!obj // returns false
!!true // returns true
!!false // returns false
!!isActive // returns true
!!age // returns true
!!array // returns true
!!obj // returns true
Este código sigue el principio de la menor sorpresa (ver Receta 5.6,
"Congelar constantes mutables"):
!true // returns false
!false // returns true
isActive = true
!isActive // returns false
age = 54
!age // should return type mismatch (or 54 factorial)
array = []
!array // should return type mismatch
obj = new Object;
!obj // should return type mismatch (what is an object negated in a
real domain?)
!!true // returns true - it is idempotent
!!false // returns false - it is idempotent
!!isActive // returns true - it is idempotent
!!age // nonsense
!!array // nonsense
!!obj // nonsense
Como esta fundición automática es una característica nativa de algunos
idiomas, sería difícil probarla. Para evitar situaciones como ésta, puedes
establecer políticas de programación o elegir lenguajes más estrictos.
Lenguajes como JavaScript o PHP dividen todo su universo en
valores verdadero o falso. Esta decisión oculta errores al tratar con
objetos no booleanos. Deberías detectar el uso de ! y !! en objetos no
booleanos y advertir a otros programadores en las revisiones de código.
Sería mejor ser muy estricto y mantener los booleanos (y su
comportamiento) lejos de los no booleanos.
REVISIONES DE CÓDIGOS
Una revisión del código consiste en que examine el código fuente para
identificar cualquier problema, error o área susceptible de mejora. Consiste en
que una o varias personas examinen el código para asegurarse de que es
correcto, eficiente, mantenible y cumple las buenas prácticas y normas.
En la Figura 24-1 puedes ver que el modelo no se comporta correctamente
como el mundo real, rompiendo así el principio de biyección y
produciendo resultados inesperados.
Figura 24-1. El método not() produce objetos diferentes en el modelo y en el mundo real
Se trata de una característica del idioma. Algunas lenguas estrictas
muestran advertencias sobre esta hechicería mágica. Algunos lenguajes
animan a hacer abreviaturas mágicas y coladas automáticas. Esto es una
fuente de errores y una advertencia de optimización prematura
(ver Capítulo 16). Siempre debes ser lo más explícito posible.
Recetas relacionadas
Receta 10.4, "Eliminar la astucia del código"
Receta 14.12, "Modificar la comparación con booleanos"
24.3 Cambiar números flotantes a
decimales
Problema
En tu código utilizas flotantes.
Solución
Si tu idioma los admite, utiliza números decimales en su lugar.
Debate
Muchas operaciones con números flotantes violan el principio de la
menor sorpresa (ver Receta 5.6, "Congelar constantes mutables"), aportan
complejidad accidental y son susceptibles de producir representaciones
decimales incorrectas. Debes elegir lenguajes maduros que admitan
números decimales y seguir el principio de bijección representando los
números decimales con decimales.
Aquí puedes ver un ejemplo sencillo pero inesperado:
[Link](0.2 + 0.1)
// 0.30000000000000004
// You are adding two decimal numbers
// 2/10 + 1/10
// Result should be 3/10 as you learned at school
Los números en coma flotante, como 0,2 y 0,1, se representan en formato
binario en la memoria de un ordenador. Algunos números decimales no
pueden representarse exactamente en binario, lo que provoca pequeños
errores de redondeo en las operaciones aritméticas. En este caso, el
resultado real de sumar 0,2 y 0,1 es 0,3. Sin embargo, debido a la
representación binaria de los números en coma flotante, el resultado es
ligeramente diferente, dando como resultado 0,30000000000000004. Aquí
tienes una representación mejor:
class Decimal {
constructor(numerator) {
[Link] = numerator;
}
plus(anotherDecimal) {
return new Decimal([Link] + [Link]);
}
toString() {
return "0." + [Link];
}}
[Link]((new Decimal(2).plus(new Decimal(1))).toString());
// 0.3
// You can represent the numbers with a Decimal class
// (storing only the numerator) or with a generic Fraction class
// (storing both the numerator and denominator)
Como se trata de una característica del lenguaje, es difícil de detectar.
Puedes pedir a tus programadores que impidan que manipules los
números de esta forma. En el viejo Commodore 64, allá por 1985, los
programadores descubrieron que 1+1+1 no era siempre 3. Entonces
introdujeron los tipos enteros. JavaScript, como ejemplo notable, es 30
años más joven, y tiene los mismos problemas de inmadurez. Podrías
tener los mismos problemas en muchos lenguajes modernos. Éste es el
tipo de complejidad accidental que debes descartar para centrarte en los
verdaderos problemas empresariales.
Recetas relacionadas
Receta 24.2, "Tratar con valores de verdad"
Ver también
Norma IEEE para aritmética de coma flotante
Ejemplos de matemáticas en coma flotante
Capítulo 25. Seguridad
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y
comentarios: translation-feedback@[Link]
La complejidad mata. Les chupa la vida a los desarrolladores, hace que los
productos sean difíciles de planificar, construir y probar, introduce
problemas de seguridad y provoca la frustración del usuario final y del
administrador.
Ray Ozzie
25.0 Introducción
Los desarrolladores senior deben poseer la capacidad no sólo de crear
código limpio y mantenible, sino también de construir soluciones sólidas
que tengan en cuenta diversos atributos de calidad del software, como el
rendimiento, el uso de recursos y la seguridad. Es imperativo que adoptes
un enfoque orientado a la seguridad mientras escribes código, ya que
sirves como primera línea de defensa contra posibles vulnerabilidades de
seguridad.
25.1 Desinfección de Entradas
Problema
Tienes código que no sanea las entradas del usuario.
Solución
Desinfecta todo lo que proceda de fuera de tu control.
Debate
SANEAMIENTO DE ENTRADA
La higienización de la entrada implica validar y limpiar la entrada del usuario
para garantizar que es segura y se ajusta a los formatos esperados antes de
procesarla. Esto es importante para evitar diversas vulnerabilidades de
seguridad, como la inyección SQL, el cross-site scripting (XSS) y otros ataques
que pueden ser ejecutados por usuarios malintencionados.
Los malos actores siempre están presentes. Tienes que tener mucho
cuidado con sus entradas, y debes utilizar técnicas de sanitización y
filtrado de entradas. Siempre que recibas una entrada de un recurso
externo, debes validarla y comprobar si hay entradas potencialmente
dañinas. La inyección SQL es un ejemplo notable de amenaza. También
puedes añadir aserciones e invariantes (ver Receta 13.2, "Aplicar
precondiciones") para tus entradas.
INYECCIÓN SQL
La inyección SQL se produce cuando un atacante inserta código SQL malicioso
en un programa que se comunica con una base de datos. El atacante puede
introducir código SQL en campos de entrada como cuadros de texto o
formularios. La aplicación puede entonces ejecutar el código accediendo o
modificando los datos, recuperando información sensible o incluso tomando el
control del sistema.
Mira el siguiente ejemplo:
user_input = "abc123!@#"
# This content might not be very safe if you expect just alphanumeric
characters
Esto es lo que parece cuando limpias la entrada:
def sanitize(string):
# Remove any characters that are not letters or numbers
sanitized_string = [Link](r'[^a-zA-Z0-9]', '', string)
return sanitized_string
user_input = "abc123!@#"
print(sanitize(user_input)) # Output: "abc123"
Puedes comprobar estáticamente todas las entradas y también utilizar
herramientas de pruebas de penetración (consulta la Receta 25.2,
"Cambiar los identificadores secuenciales").
ADVERTENCIA
Siempre debes ser muy cauteloso con las entradas que escapan a tu control. Esto
incluye cualquier cosa que esté fuera de tus límites, como datos serializados,
interfaces de usuario, API, sistemas de archivos, etc.
Recetas relacionadas
Receta 4.7, "Reificar validaciones de cadenas"
Receta 23.4, "Eliminar métodos dinámicos"
Receta 25.5, "Proteger la deserialización de objetos"
Ver también
Estrategias de inyección SQL por Ettore Galluccio, Edoardo Caselli y
Gabriele Lombari
25.2 Cambiar los identificadores
secuenciales
Problema
En tu código utilizas identificadores secuenciales.
Solución
No expongas identificaciones consecutivas obvias.
Debate
La mayoría de los ID son problemáticos. Las identificaciones secuenciales
también son una vulnerabilidad. Los ID rompen la biyección y crean
problemas de seguridad ycolisiones. Debes utilizar claves no obvias.
Utiliza claves oscuras o UUIDs en. Los ID son un problema cuando se trata
de objetos de dominio porque no existen en el mundo real, por lo que
siempre rompen la biyección. Sólo debes utilizar IDs cuando expongas
recursos internos al mundo exterior más allá de los límites del sistema. Se
trata siempre de cuestiones accidentales y no deben interferir en tus
modelos.
Aquí tienes un ejemplo con identificaciones pequeñas:
class Book {
private Long bookId; // book knows its ID
private List<Long> authorIds; // book knows author IDs
}
Book harryPotter = new Book(1, [Link](2));
Book designPatterns = new Book(2, [Link](4, 6, 7, 8));
Book donQuixote = new Book(3, [Link](5));
// You can scrape from now on.
Puedes eliminar los identificadores:
class Author { }
class Book {
private List<Author> authors; // book knows authors
// No strange behavior, just what a book can do
// Real books don't know about IDs
// ISBN is accidental to a book. Readers don't care
}
class BookResource {
private Book resource; // The resource knows the underlying book
Private UUID id; // The id is the link you provide to external
world
}
Book harryPotter = new Book(new Author('J. K. Rowling'));
Book designPatterns = new Book(
new Author('Erich Gamma'),
new Author('Richard Helm'),
new Author('Ralph Johnson'),
new Author(('John Vlissides'))
Book donQuixote = new Book(new Author('Miguel Cervantes'));
BookResource harryPotterResource = new BookResource(
harryPotter,
[Link]());
// Books don't know their id. Just the resource does
Puedes utilizar técnicas de pentesting contra tu sistema para detectar este
problema. Si necesitas exponer objetos internos al mundo externo, debes
utilizar IDs no obvios. De este modo, puedes detectar (y bloquear) los
ataques de fuerza bruta monitorizando el tráfico y los errores 404.
PRUEBAS DE PENETRACIÓN
Las pruebas de penetración, también conocidas como pentesting, evalúan la
seguridad de un sistema simulando ataques del mundo real. Identifica
vulnerabilidades y evalúa la eficacia de las medidas de seguridad implantadas.
Es similar a las pruebas de mutación (ver Receta 5.1, "Cambiar var por const"),
en las que compruebas la calidad de tus herramientas y software.
Ver también
"Referencias directas inseguras a objetos (IDOR)"
Recetas relacionadas
Receta 17.5, "Convertir los valores de los indicadores especiales 9999 en
normales"
25.3 Eliminar dependencias de
paquetes
Problema
Puedes utilizar un gestor de paquetes y confiar en el código de otros
módulos.
Solución
Escribe tu propio código a menos que necesites una solución compleja y
exista una disponible.
Debate
Hay una tendencia en la industria a evitar escribir código en la medida de
lo posible. Pero esto no es gratuito. Existe un equilibrio entre seguir la
regla del cero (véase la Receta 16.10, "Eliminar el código de los
destructores") y confiar en el código de otras personas. Las dependencias
de paquetes conllevan acoplamiento externo, problemas de seguridad,
complejidad arquitectónica, corrupción de paquetes, etc. Siempre debes
implementar soluciones triviales y confiar sólo en dependencias externas
y maduras.
Este es un ejemplo real de una pequeña función:
$ npm install --save is-odd
// [Link]
// This package has about 500k weekly downloads
[Link] = function isOdd(value) {
const n = [Link](value);
return (n % 2) === 1;
};
Puedes hacer una implementación trivial por tu cuenta:
function isOdd(value) {
const n = [Link](value);
return (n % 2) === 1;
};
// Just solve it inline
Comprueba tus dependencias externas y redúcelas al mínimo; depende de
una determinada versión concreta para evitar secuestros. No necesitas
reinventar siempre la rueda. Antes de utilizar un paquete, debes hacer un
análisis para ver si el paquete es realmente necesario y si está
actualizado, y también comprobar su actividad como desarrollador, las
incidencias, las pruebas automatizadas, etc. Necesitas un buen equilibrio
entre la duplicación de código y el abuso de la reutilización. Como
siempre, hay reglas generales, pero no reglas rígidas.
Recetas relacionadas
Receta 11.7, "Reducir las listas de importación "
Ver también
"Paquetes Python y PHP envenenados roban contraseñas para acceder a
AWS", Naked Security
"Dev Corrupts NPM Libs 'colors' and 'faker' Breaking Thousands of Apps,"
Bleeping Computer
"Cómo un programador rompió Internet borrando un diminuto trozo de
código", Quartz
"Malware encontrado en un paquete npm con millones de descargas
semanales", The Record
25.4 Sustituir expresiones regulares
malignas
Problema
Tienes expresiones regulares malignas en tu código.
Solución
Intenta minimizar las reglas recursivas de las expresiones regulares.
Debate
Las expresiones regulares son un problema. A veces también son una
vulnerabilidad. Traen problemas de legibilidad; las expresiones regulares
recursivas son un síntoma de optimización prematura, y a veces un
problema de seguridad. Deberías cubrir los casos con pruebas para ver si
las expresiones se detienen y, como medida de seguridad, añadir
manejadores de tiempo de espera, o utilizar algoritmos en lugar de
expresiones regulares.
Uno de los problemas se conoce como ataque de denegación de servicio
por expresión regular(ReDoS), un subtipo de ataque de denegación de
servicio (DoS). Los ataques ReDoS pueden dividirse en dos tipos: uno se
produce cuando se pasa a una aplicación una cadena con un patrón
malicioso. Esta cadena se utiliza entonces como una expresión regular, lo
que conduce al ReDoS. Otro escenario es cuando se pasa a una aplicación
una cadena con un formato de ataque vectorial. A continuación, esta
cadena es evaluada por una expresión vulnerable, lo que conduce a
ReDoS.
Aquí puedes ver el ataque en acción:
func main() {
var regularExpression = [Link](`^(([a-z])+.)+[A-Z]([a-
z])+$`)
var candidateString = "aaaaaaaaaaaaaaaaaaaaaaaa!"
for index, match :=
range [Link](candidateString, -1) {
[Link](match, "found at index", index)
}
}
Se trata de un enfoque equivalente sin utilizar expresiones regulares:
func main() {
var candidateString = "aaaaaaaaaaaaaaaaaaaaaaaa!"
words := [Link](candidateString)
for index, word := range words {
if len(word) >= 2 && word[0] >= 'A' &&
word[0] <= 'Z' && word[len(word)-1] >= 'A'
&& word[len(word)-1] <= 'Z' {
[Link](word, "found at index", index)
}
}
}
Muchos lenguajes evitan este tipo de expresiones regulares. También
puedes escanear el código en busca de esta vulnerabilidad. Las
expresiones regulares son complicadas y difíciles de depurar, por lo que
deberías evitarlas en la medida de lo posible.
Recetas relacionadas
Receta 6.10, "Documentar expresiones regulares"
Ver también
Vulnerabilidades CVE-2017-16021, CVE-2018-13863, CVE-2018-8926
25.5 Proteger la deserialización de
objetos
Problema
Estás deserializando objetos procedentes de una fuente insegura.
Solución
No permitas la ejecución remota de código.
Debate
Muchas vulnerabilidades están relacionadas con la salida no saneada. Un
principio de seguridad importante es evitar la ejecución de código y
considerar la entrada sólo como datos. Deserializar objetos de una fuente
no fiable es, de hecho, una operación sensible desde el punto de vista de la
seguridad. Supón que tienes una aplicación web que acepta objetos
serializados como entrada a partir de datos enviados por el usuario, como
en un punto final de la API o a través de una función de carga de archivos.
La aplicación deserializa estos objetos para reconstruirlos en objetos
utilizables dentro del sistema. Si un atacante envía datos serializados
maliciosamente diseñados para explotar vulnerabilidades en el proceso
de deserialización, podría manipular los datos serializados para ejecutar
código arbitrario, escalar privilegios o realizar acciones no autorizadas
dentro de la aplicación o del sistema subyacente. Este tipo de ataque se
conoce comúnmente como "ataque de deserialización" o
"vulnerabilidades de serialización".
Aquí puedes ver un breve ejemplo:
import pickle # Python's serialization module
def process_serialized_data(serialized_data):
try:
obj = [Link](serialized_data)
# Deserialize the object
# Process the deserialized object
# ...
# User-submitted serialized data
user_data = b"\x80\x04\x95\x13\x00\x00\x00\x00\x00\x00\x00\x8c\x08os\
nsystem
\n\x8c\x06uptime\n\x86\x94."
# This code executes: [Link]("uptime")
process_serialized_data(user_data)
Cuando consideras la entrada como datos:
import json
def process_serialized_data(serialized_data):
obj = [Link](serialized_data)
# Deserialize the JSON object
# Does not execute code
user_data = '{"key": "value"}'
process_serialized_data(user_data)
Varios linters advierten sobre los puntos de deserialización. Ten siempre
en cuenta que la metaprogramación abre puertas a los abusadores.
Recetas relacionadas
Receta 23.1, "Eliminar el uso de la metaprogramación"
Receta 25.1, "Desinfección de entradas"
Ver también
Regla SonarSource: "Deserializar objetos de una fuente no fiable es
sensible desde el punto de vista de la seguridad"
Glosario de términos
Pruebas A/B
Compara dos versiones diferentes de un software liberado para
determinar cuál es mejor para los usuarios finales.
antipatrón
Un patrón de diseño que inicialmente puede parecer una buena
idea, pero que al final tiene consecuencias negativas. En un
principio, muchos expertos los presentaron como buenas
soluciones, pero hoy en día hay pruebas contundentes en contra de
su uso.
lenguaje ensamblador
Un lenguaje de programación de bajo nivel para escribir programas
de software para arquitecturas informáticas específicas. Es un
código imperativo de lenguaje legible por humanos que está
diseñado para traducirse fácilmente al lenguaje máquina, que es el
lenguaje que pueden entender los ordenadores.
axioma
Una afirmación o proposición que se da por verdadera sin pruebas.
Te permite construir un marco lógico para el razonamiento y la
deducción, estableciendo un conjunto de conceptos y relaciones
fundamentales que pueden utilizarse para deducir otras verdades.
pasos de bebé
Un enfoque iterativo e incremental en el que realizas pequeñas
tareas o cambios manejables durante el proceso de desarrollo. El
concepto de pasos de bebé está arraigado en la metodología de
desarrollo ágil.
biyección
Función que crea una correspondencia unívoca entre los elementos
de dos conjuntos.
operadores bit a bit
Manipula los bits individuales de los números. Tu ordenador las
utiliza para realizar operaciones lógicas de bajo nivel entre bits,
como AND, OR y XOR. Funcionan en el dominio de los números
enteros, que es diferente del dominio booleano.
bandera booleana
Variable que sólo puede ser verdadera o falsa, representando los
dos estados posibles de una condición binaria. Los indicadores
booleanos se utilizan habitualmente para controlar el flujo de la
lógica mediante sentencias condicionales, bucles y otras estructuras
de control.
Regla de los Boy Scouts
La regla del Boy Scout del tío Bob sugiere dejar el código mejor de
lo que lo encontraste, igual que dejas el camping más limpio de lo
que lo encontraste como Boy Scout. La regla anima a los
desarrolladores a realizar pequeñas mejoras incrementales en el
código base cada vez que lo tocan, en lugar de crear un lío de deuda
técnica (véase el Capítulo 21, "Deuda técnica") que será difícil de
limpiar más adelante; también favorece cambiar las cosas que no
están del todo bien. Esto contradice el principio "Si no está roto, no
lo arregles".
teoría de las ventanas rotas
Sugiere que los problemas o defectos pequeños, aparentemente
insignificantes, pueden conducir a problemas mayores y más
graves más adelante. Si un desarrollador advierte un pequeño
problema en el código pero decide ignorarlo porque ya hay otras
ventanas rotas, esto puede conducir a una cultura de negligencia y
a una falta de atención a los detalles en el proceso de desarrollo.
error
El término fallo es un error común en la industria. En este libro,
hablo en su lugar de defectos. Los bugs originales estaban
relacionados con insectos externos que entraban en circuitos
calientes y alteraban la salida del software. Esto ya no es así. Se
recomienda emplear el término defecto porque se refiere a algo
introducido y no a un invasor externo.
caché
Almacena temporalmente los objetos a los que se accede con
frecuencia para acceder a ellos más rápidamente. Puedes utilizarla
para mejorar el rendimiento de las aplicaciones de software
reduciendo el número de accesos a recursos costosos. Al almacenar
los datos en la memoria caché, el software puede evitar la
sobrecarga de acceder a dispositivos de almacenamiento más lentos
y, en su lugar, recuperar los objetos directamente de la caché.
cadena de responsabilidad
Permite que varios objetos gestionen una solicitud de forma
encadenada, sin saber qué objeto de la cadena gestiona
específicamente la solicitud. En este patrón, la solicitud pasa por
una serie de gestores hasta que uno de ellos la gestiona o hasta que
llega al final de la cadena. Ten en cuenta que los eslabones de la
cadena están desacoplados entre sí.
revisión del código
Consiste en examinar el código fuente para identificar cualquier
problema, error o área susceptible de mejora. Implica que una o
varias personas examinen el código para asegurarse de que es
correcto, eficaz, mantenible y se ajusta a las buenas prácticas
y normas.
carga cognitiva
La cantidad de esfuerzo mental y recursos necesarios para procesar
la información y completar una tarea. Es la carga que soporta la
memoria de trabajo de una persona cuando intenta procesar,
comprender y recordar información a la vez.
cohesión
Medida del grado en que los elementos de una misma clase o
módulo de software trabajan juntos para lograr un objetivo único y
bien definido. Se refiere a lo estrechamente relacionados que están
los objetos entre sí y con el objetivo general del módulo. Puedes
considerar que una cohesión alta es una propiedad deseable en el
diseño de software, ya que los elementos de un módulo están
estrechamente relacionados y trabajan juntos con eficacia para
lograr un objetivo específico.
propiedad colectiva
Establece que todos los miembros de un equipo de desarrollo tienen
la capacidad de realizar cambios en cualquier parte del código
base, independientemente de quién lo escribió originalmente.
Pretende fomentar un sentido de responsabilidad compartida,
haciendo que el código sea más manejable y más fácil de mejorar.
complejidad computacional
Estudia los recursos necesarios para resolver problemas
computacionales. Los más importantes son el tiempo y la memoria.
Mide y compara la eficacia de los algoritmos y sistemas
computacionales en relación con estos recursos.
composición
Permite que los objetos se compongan de otros objetos como partes
o componentes. Construyes objetos complejos combinando otros
más sencillos (consulta la Receta 4.1 , "Crear objetos pequeños"),
formando una relación"tiene-un" en lugar de las clásicas "es-un" o
"se comporta-como-un" (consulta la Receta 19.4, "Sustituir la
relación "es-un" por el comportamiento").
integración continua e implementación continua (CI/CD)
Una canalización que automatiza el proceso de desarrollo, prueba e
implementación de software. El canal está diseñado para agilizar el
proceso de desarrollo de software, automatizar tareas, mejorar la
calidad del código y agilizar y gestionar la implementación de
nuevas funciones y correcciones en distintos entornos.
programación de copiar y pegar
Una técnica en la que copias código existente y lo pegas en otro
lugar, en lugar de escribir código nuevo. Si utilizas mucho el copiar
y pegar, tu código es menos mantenible.
grupo de datos
En los grupos de datos, el mismo grupo de objetos pasa con
frecuencia de una parte a otra de un programa. Esto puede
aumentar la complejidad, reducir la capacidad de mantenimiento y
aumentar el riesgo de errores. Los cúmulos de datos suelen
producirse cuando intentas pasar objetos relacionados sin
encontrar un objeto adecuado que represente esa relación en la
bijección.
patrón decorador
Te permite añadir dinámicamente comportamiento a un objeto
individual sin afectar al comportamiento de otros objetos de la
misma clase.
Ley de Deméter
Principio según el cual un objeto sólo debe comunicarse con sus
vecinos inmediatos, y no debe conocer el funcionamiento interno
de otros objetos. Para favorecer la ley de Demeter, debes crear
objetos que estén débilmente acoplados, es decir, que no dependan
mucho unos de otros. Esto hace que el sistema sea más flexible y
fácil de mantener, ya que es menos probable que los cambios en un
objeto tengan consecuencias no deseadas en otros objetos.
Un objeto sólo debe acceder a los métodos de sus vecinos
inmediatos, en lugar de llegar a otros objetos para acceder a sus
partes internas. Esto ayuda a reducir el nivel de acoplamiento entre
objetos y hace que el sistema sea más modular y flexible.
inversión de la dependencia
Principio de diseño que desacopla los objetos de nivel superior de
los de nivel inferior invirtiendo la relación de dependencia
tradicional. En lugar de que los objetos de nivel superior dependan
directamente de los objetos de nivel inferior, el principio sugiere
que ambos dependan de abstracciones o interfaces. Esto permite
una mayor flexibilidad y modularidad en la base de código, ya que
los cambios en la implementación de un módulo de nivel inferior
no requieren necesariamente cambios en el módulo de nivel
superior.
diseño por contrato
Construcción de Software Ori entado aObjetos, de Bertrand
Meyer, es una guía completa para el desarrollo de software
utilizando el paradigma orientado a objetos. Una de las ideas clave
del libro es el concepto de "diseño por contrato", que subraya la
importancia de crear contratos claros e inequívocos entre los
módulos de software. Un contrato especifica las responsabilidades y
el comportamiento que garantizan que los módulos funcionen
correctamente juntos y que el software siga siendo fiable y
mantenible a lo largo del tiempo. Cuando se rompe un contrato, se
respeta el principio de "fail fast" y los problemas se detectan
inmediatamente.
diseño basado en el dominio
Se centra en alinear el diseño de los sistemas de software con el
negocio o el dominio del problema, haciendo que el código sea más
expresivo, mantenible y estrechamente vinculado a los requisitos
del negocio.
principio de no repetirse (DRY)
Establece que los sistemas de software deben evitar la redundancia
y la repetición de código. El objetivo del principio DRY es mejorar la
mantenibilidad, flexibilidad y comprensibilidad del software
reduciendo la cantidad de conocimientos, código e información
duplicados.
DTO
Se utiliza para transferir datos entre las distintas capas de una
aplicación. Es un objeto simple, serializable e inmutable que
transporta datos entre el cliente y el servidor de la aplicación. El
único propósito de un DTO es proporcionar una forma estándar de
intercambiar datos entre las distintas partes de la aplicación.
encapsulación
Se refiere a proteger las responsabilidades de un objeto.
Normalmente puedes conseguirlo abstrayendo la implementación
real. También proporciona una forma de controlar el acceso a los
métodos de un objeto. En muchos lenguajes de programación, es
posible especificar la visibilidad de las propiedades y métodos de
un objeto, lo que determina si otras partes del programa pueden
acceder a ellos o modificarlos. Esto permite a los desarrolladores
ocultar los detalles de la implementación interna de un objeto y
exponer sólo el comportamiento necesario para que lo utilicen
otras partes del programa.
diagrama entidad-relación (ERD)
Representación visual de los datos de una base de datos. En un
diagrama ERD, las entidades se representan mediante rectángulos,
mientras que las relaciones entre entidades se representan
mediante líneas que conectan los rectángulos.
esencia y accidente
En su libro The Mythical Man-Month, el informático Fred Brooks
utiliza los términos "accidental" y "esencial" para referirse a dos
tipos diferentes de complejidad en la ingeniería de software,
utilizando la definición de Aristóteles.
La complejidad "esencial" es inherente al problema que se resuelve
y no puede evitarse, ya que es la complejidad necesaria para que el
sistema funcione según lo previsto y presente en el mundo real. Por
ejemplo, la complejidad de un sistema de aterrizaje espacial es
esencial porque es necesaria para aterrizar con seguridad un
vehículo explorador.
La complejidad "accidental" surge de la forma en que se diseña e
implementa el sistema, más que de la naturaleza del problema que
se resuelve. Puede reducirse creando buenos diseños. La
complejidad accidental innecesaria es uno de los mayores
problemas del software y en este libro encontrarás muchas
soluciones.
patrón de fachada
Proporciona una interfaz simplificada a un sistema o subsistema
complejo. Se utiliza para ocultar la complejidad de un sistema y
proporcionar una interfaz más sencilla para que la utilicen los
clientes. También se comporta como un mediador entre el cliente y
el subsistema, protegiendo al cliente de los detalles de la
implementación del subsistema.
principio fail fast
Estados debes interrumpir la ejecución lo antes posible cuando se
produzca un error, en lugar de ignorarlo y fallar como
consecuencia más adelante.
envidia de funciones
Ocurre cuando un objeto está más interesado en el comportamiento
de otro objeto que en el suyo propio, utilizando excesivamente los
métodos de otro objeto.
indicador de función (también conocido como conmutador de
funciones o interruptor de funciones)
Te permite activar o desactivar una característica o funcionalidad
específica en tiempo de ejecución, sin necesidad de una nueva
implementación completa. Esto te permite lanzar nuevas funciones
a un subconjunto de usuarios o entornos, manteniéndolas ocultas a
los demás para realizar pruebas A/B y betas tempranas o
lanzamientos canarios.
control medioambiental total
La capacidad de tener un control total sobre el entorno en el que se
ejecutan las pruebas. Implica crear un entorno controlado y
predecible que permita que las pruebas se ejecuten de forma
coherente e independiente de factores externos. Debes tener
especialmente en cuenta las dependencias externas, la simulación
de red, el aislamiento de la base de datos, el control del tiempo y
muchos otros.
función firma
Especifica su nombre, los tipos de los parámetros y el tipo de
retorno si el lenguaje es estrictamente tipado. Se utiliza para
distinguir una función de otra y para garantizar que las llamadas a
funciones se realizan correctamente.
objetos fungibles
Intercambiable o idéntico en valor, calidad y características.
Cualquier instancia particular de un objeto fungible puede
sustituirse por cualquier otra instancia del mismo objeto sin
pérdida de valor o calidad. La fungibilidad es la propiedad de un
bien o mercancía cuyas unidades individuales son esencialmente
intercambiables y cada una de cuyas partes es indistinguible de
cualquier otra.
recolector de basura
Utilizado por los lenguajes de programación para gestionar
automáticamente la asignación y desasignación de memoria.
Funciona identificando y eliminando de la memoria los objetos que
el programa ya no utiliza, liberando así la memoria utilizada.
git bisectar
Git es un sistema de control de versiones para el desarrollo de
software. Te ayuda a seguir los cambios en el código, colaborar con
otros y volver a versiones anteriores si es necesario. Git almacena
todo el historial de versiones de cada archivo. También puede
gestionar múltiples desarrolladores trabajando en el mismo código
base.
git bisect es un comando que te ayuda a localizar la confirmación
que introdujo un cambio concreto en el código. El proceso comienza
especificando una confirmación "buena" que se sabe que no
contiene el defecto y una confirmación "mala" que se sabe que
contiene el cambio. Al iterar puedes encontrar el commit culpable y
localizar rápidamente la causa raíz.
identificador único global (GUID)
Identificador único utilizado en los sistemas informáticos para
asignar recursos como archivos, objetos o entidades en una red. Los
GUID se generan mediante algoritmos que garantizan su unicidad.
chapado en oro
Se refiere a la práctica de añadir características o funcionalidades
innecesarias a un producto o proyecto, más allá de los requisitos o
especificaciones mínimos. Esto puede ocurrir por diversas razones,
como el deseo de impresionar al cliente o de hacer que el producto
destaque en el mercado. Sin embargo, el gold plating puede ser
perjudicial para el proyecto, ya que puede provocar sobrecostes y
retrasos, y puede no aportar ningún valor real al usuario final.
Dios objeto
Los objetos Dios tienen una cantidad excesiva de responsabilidades
o control sobre todo el sistema. Estos objetos suelen ser grandes y
complejos, con una cantidad significativa de código y lógica. Violan
el principio de responsabilidad única (ver Receta 4.7 , "Reificar
validaciones de cadenas") y el concepto de separación de
preocupaciones (ver Receta 8.3, "Eliminar comentarios lógicos").
Los objetos Dios tienden a convertirse en un cuello de botella en la
arquitectura del software, haciendo que el sistema sea difícil de
mantener, escalar y probar.
hash
Se refiere al proceso de asignar datos de tamaño arbitrario a un
valor de tamaño fijo. El resultado de una función hash se denomina
valor hash o código hash. Puedes utilizar los valores hash como una
tabla de índices en colecciones grandes; funcionan como un atajo
para encontrar elementos de una forma más eficaz que iterando los
elementos secuencialmente.
"Si no está roto, no lo arregles" principio
Expresión común en el desarrollo de software que afirma que si un
sistema de software funciona bien, no hay necesidad de hacerle
ningún cambio o mejora. El principio se remonta a los tiempos en
que el software no tenía pruebas automatizadas, por lo que hacer
cualquier cambio probablemente rompería la funcionalidad
existente. Los usuarios del mundo real suelen tolerar los defectos
en las nuevas funciones, pero se enfadan mucho cuando algo que
antes funcionaba deja de hacerlo como se esperaba.
intimidad inapropiada
Ocurre cuando dos clases o componentes se han vuelto
excesivamente dependientes entre sí, creando un estrecho
acoplamiento que hace que el código sea difícil de mantener,
modificar o ampliar.
ocultación de información
Principio que pretende reducir la complejidad de un sistema de
software separando su funcionamiento interno de su interfaz
externa. Esto permite que la implementación interna de un sistema
cambie sin afectar a la forma en que lo utilizan otros sistemas o
usuarios.
limpieza de entradas
Consiste en validar y limpiar la entrada del usuario para garantizar
que es segura y se ajusta a los formatos esperados antes de
procesarla. Esto es importante para evitar diversas
vulnerabilidades de seguridad, como la inyección SQL, el cross-site
scripting (XSS) y otros ataques que pueden ejecutar usuarios
malintencionados.
principio de segregación de interfaces
Establece que no se debe obligar a los objetos a depender de
interfaces que no utilizan. Es mejor tener muchas interfaces
pequeñas y especializadas que una interfaz grande y monolítica.
revelador de intenciones
El código que revela intenciones comunica claramente su propósito
o intención a otros desarrolladores que puedan leer o trabajar con
tu código en el futuro. El objetivo de revelar la intención del código
es hacerlo más conductual, declarativo, legible, comprensible y fácil
de mantener.
Principio KISS
Abreviatura de "Mantenlo simple, estúpido". Aconseja que los
sistemas funcionan mejor cuando se mantienen sencillos que
cuando se complican. Los sistemas más sencillos son más fáciles de
entender, utilizar y mantener que los complejos, y por tanto es
menos probable que fallen o produzcan resultados inesperados.
inicialización perezosa
Utilizando la inicialización perezosa, retrasas la creación de un
objeto o el cálculo de un valor hasta que sea realmente necesario,
en lugar de hacerlo inmediatamente. Esto se suele utilizar para
optimizar el uso de recursos y mejorar el rendimiento, aplazando el
proceso de inicialización hasta el último momento posible.
Principio de sustitución de Liskov
Establece que si una función o método está diseñado para trabajar
con objetos de una clase concreta, entonces también debería
funcionar con objetos de cualquier subclase de esa clase sin causar
ningún comportamiento inesperado. Es la "L" de SOLID (ver Receta
4.7, "Reificar las validaciones de cadenas").
acoplamiento débil
Pretende minimizar la interdependencia de los distintos objetos de
un sistema. Tienen un conocimiento mínimo unos de otros, y los
cambios realizados en un componente no afectan a otros
componentes del sistema, evitando un efecto dominó.
MAPPER
Modelo: Parcial Abstracto y Programable Explicación de la
Realidad. Puedes definir el software como la construcción de un
simulador con este acrónimo, como se explica en el Capítulo 2.
objeto simulado
Imita el comportamiento de un objeto real para probar o simular su
comportamiento. Puedes utilizarlo para probar componentes de
software que tengan dependencias de otros componentes, como API
o bibliotecas externas.
modelo
Explica el tema que describe utilizando conceptos intuitivos o
metáforas. El objetivo final de un modelo es la comprensión de
cómo funciona algo. Según Peter Naur, "Programar es construir
teoría y modelos".
mónada
Proporciona una forma estructurada de encapsular y manipular
funciones. Te permite encadenar operaciones, manejando las
funciones y sus efectos secundarios de forma coherente y
predecible, por ejemplo cuando trabajas con valores opcionales.
pruebas de mutación
Una técnica que puedes utilizar para valorar la calidad de tus
pruebas unitarias. Consiste en introducir pequeños cambios
controlados (llamados "mutaciones") en el código que estás
probando y comprobar si tus pruebas unitarias existentes pueden
detectar esos cambios. Puede ayudarte a identificar áreas del código
en las que necesitas pruebas adicionales, y puedes utilizarla como
medida de la calidad de tus pruebas existentes.
Una mutación consiste en cambiar una pequeña parte del código
(por ejemplo, negar un booleano, sustituir una operación
aritmética, sustituir un valor por nulo, etc.) y ver si falla alguna
prueba.
parámetros con nombre
Característica de muchos lenguajes de programación que permite al
programador especificar el valor de un parámetro proporcionando
su nombre en lugar de su posición en la lista de parámetros.
También se conocen como argumentos de palabra clave
espacios de nombres
Se utilizan para organizar elementos de código como clases,
funciones y variables en grupos lógicos, evitando conflictos de
nombres y proporcionando una forma de identificarlos de forma
única dentro de un ámbito concreto. Ayudan a crear código
modular y fácil de mantener, agrupando funcionalidades
relacionadas.
código ninja (también conocido como código inteligente)
Se refiere al código que está escrito con ingenio, pero que es difícil
de entender o mantener. A menudo lo crean programadores
experimentados que disfrutan utilizando técnicas avanzadas de
programación o características específicas del lenguaje para
escribir código más eficiente y optimizado antes de tiempo. Aunque
el código ninja puede ser impresionante y ejecutarse más rápido
que otro código, puede ser difícil de leer y comprender, lo que
conlleva problemas de mantenimiento, escalabilidad y desarrollo
futuro. El código ninja es lo contrario del código limpio.
no hay bala de plata
El concepto de "ninguna bala de plata" es una frase acuñada por el
informático y pionero de la ingeniería de software Fred Brooks en
su ensayo de 1986 "Ninguna bala de plata: Esencia y accidentes de
la ingeniería de software". Brooks argumenta que no existe una
única solución o enfoque que pueda resolver todos los problemas o
mejorar significativamente la productividad y eficacia del
desarrollo de software.
patrón de objeto nulo
Sugiere la creación de un objeto especial llamado "objeto nulo", que
se comporta como un objeto normal pero no tiene casi ninguna
funcionalidad. Su ventaja es que puedes llamar con seguridad a
métodos del objeto nulo sin tener que comprobar las referencias
nulas con un if (ver Capítulo 14, "Ifs").
excepción de puntero nulo
Error habitual que se produce cuando un programa intenta acceder
o utilizar un puntero nulo, que es una variable o referencia a un
objeto que no apunta a ninguna dirección de memoria ni a ninguna
instancia de objeto.
orgía de objetos
Describe una situación en la que los objetos están insuficientemente
encapsulados, permitiendo un acceso sin restricciones a su interior.
Se trata de un antipatrón habitual en el diseño orientado a objetos y
puede provocar un aumento del mantenimiento y de la
complejidad.
cosificación del objeto
Proceso en el que se da forma concreta a un concepto o idea
abstracta, representando un concepto o idea específicos y
proporcionando comportamiento a objetos anémicos y orientados a
datos. Al dar una forma concreta a conceptos abstractos mediante
la creación de objetos, podrás manipular y trabajar con estos
conceptos de forma sistemática y estructurada.
patrón de diseño observador
Define una dependencia de uno a muchos entre objetos; por
ejemplo, cuando un objeto cambia de estado, todos sus objetos
dependientes son notificados y actualizados automáticamente, sin
una referencia directa. Te suscribes a los eventos publicados y el
objeto modificado emite una alerta sin saber quién está suscrito.
principio abierto-cerrado
La "O" de SOLID (ver Receta 19.1, "Romper la herencia profunda").
Establece que las clases de software deben estar abiertas a la
extensión, pero cerradas a la modificación. Debes poder ampliar el
comportamiento sin modificar el código. Este principio fomenta el
uso de interfaces abstractas, herencia y polimorfismo para permitir
que se añada nueva funcionalidad sin cambiar el código existente.
El principio también promueve la separación de preocupaciones
(véase la Receta 8.3, "Eliminar comentarios lógicos"), lo que facilita
el desarrollo, las pruebas y la implementación de componentes de
software de forma independiente.
encadenamiento opcional
Te permite acceder a propiedades anidadas de un objeto sin tener
que comprobar la existencia de cada propiedad de la cadena. Sin
ella, si intentas acceder a una propiedad de un objeto que no existe,
arrojará un error.
sobrediseñar
La práctica de añadir complejidad accidental innecesaria a una
aplicación de software. Esto puede ocurrir cuando te centras
demasiado en hacer que el software tenga tantas funciones como
sea posible, en lugar de mantenerlo simple y centrado en
la funcionalidad principal.
pruebas de penetración
Evalúa la seguridad de un sistema simulando ataques del mundo
real. Identifica vulnerabilidades y evalúa la eficacia de las medidas
de seguridad aplicadas. Es similar a las pruebas de mutación
(ver Receta 5.1, "Cambiar var por const"), en las que compruebas la
calidad de tus herramientas y tu software.
poltergeist
Objeto efímero que se utiliza para realizar la inicialización o para
invocar métodos de otra clase más permanente.
jerarquía polimórfica
En una jerarquía polimórfica, las clases se organizan en una
estructura jerárquica basada en sus relaciones "se comporta-como".
Esto permite crear clases especializadas que heredan
comportamientos de clases más generales. En una jerarquía
polimórfica, una clase abstracta base sirve de base y define un
comportamiento común compartido por múltiples subclases
concretas. Las subclases heredan estas características de la
superclase y pueden añadir su propio comportamiento.
La subclasificación es una forma de imponer el polimorfismo
(véase la Receta 14.14, "Convertir funciones no polimórficas en
polimórficas"). Pero es rígida, ya que no puedes cambiar una
superclase después del tiempo de compilación.
polimorfismo
Dos objetos son polimórficos respecto a un conjunto de métodos si
tienen la misma firma y realizan la misma acción (quizá con una
implementación diferente).
precondiciones, postcondiciones e invariantes
Una precondición es una condición que debe ser verdadera antes
de llamar a una función o método. Especifica los requisitos que
deben satisfacer las entradas de la función o el método.
Una invariante es una condición que debe cumplirse en todo
momento durante la ejecución de un programa,
independientemente de los cambios que puedan producirse.
Especifica una propiedad del programa que no debe cambiar con el
tiempo. Por último, una postcondición está ligada al momento
posterior a la llamada al método. Puedes utilizarlas para
garantizar la corrección, detectar defectos u orientar el diseño del
programa.
preprocesador
Realiza tareas en el código fuente antes de que sea compilado o
interpretado por el compilador o intérprete principal. Se utiliza
habitualmente en los lenguajes de programación para modificar o
manipular el código fuente antes de que se someta al proceso real
de compilación o interpretación.
clave primaria
En el contexto de las bases de datos, una clave primaria es un
identificador único para un registro o fila concretos de una tabla.
Sirve para identificar de forma única cada registro de una tabla y
permite una búsqueda y ordenación eficaces de los datos. Una clave
primaria puede ser una sola columna o una combinación de
columnas que, al combinarse, forman un valor único para cada
registro de la tabla. Normalmente, una clave primaria se crea con
una tabla y es utilizada como referencia por otras tablas de una
base de datos que tengan relaciones con esa tabla.
principio de la menor sorpresa (también conocido como principio
del menor asombro)
Dice que un sistema debe comportarse de la forma menos
sorprendente para sus usuarios y coherente con las expectativas de
éstos. Si sigues este principio, el usuario puede predecir fácilmente
lo que ocurrirá cuando interactúe con el sistema.
Como desarrollador, deberías crear un software más intuitivo y
fácil de usar, lo que aumentaría la satisfacción del usuario y su
productividad.
promesas
Un objeto especial que representa la finalización (o el fallo) de una
operación asíncrona y su valor resultante.
atributos protegidos
Variable de instancia o propiedad de una clase a la que sólo se
puede acceder dentro de la clase o de sus subclases. Los atributos
protegidos son una forma de restringir el acceso a determinados
datos dentro de una jerarquía de clases, permitiendo al mismo
tiempo que las subclases accedan a esos datos y los modifiquen si es
necesario.
prototipado rápido
Se utiliza en el desarrollo de productos para crear rápidamente
prototipos funcionales que se validan con el usuario final. Esta
técnica permite a diseñadores e ingenieros probar y refinar un
diseño antes de crear un código limpio coherente, robusto y
elegante.
transparencia referencial
Las funciones de transparencia referencial siempre producen la
misma salida para una entrada dada y no tienen efectos
secundarios, como modificar variables globales o realizar
operaciones de E/S. En otras palabras, una función o expresión es
referencialmente transparente si puede sustituirse por su resultado
evaluado sin cambiar el comportamiento del programa. Éste es un
concepto fundamental en los paradigmas de programación
funcional, en los que las funciones se tratan como expresiones
matemáticas que asignan entradas a salidas.
patrón de diseño del repositorio
Proporciona una capa de abstracción entre la lógica empresarial de
la aplicación y la capa de almacenamiento de datos, lo que permite
una arquitectura más flexible y fácil de mantener.
efecto dominó
Se refiere a la forma en que un cambio o modificación en una parte
de un sistema puede tener consecuencias no deseadas en otras
partes del sistema. Si realizas un cambio en un objeto concreto,
podría afectar potencialmente a otras partes del sistema que
dependen de él. Esto podría provocar errores o un comportamiento
inesperado en esas otras partes del sistema.
depuración del patito de goma
El concepto de explicar tu código línea por línea, como si estuvieras
enseñando a programar a un patito de goma. Al verbalizar y
describir cada paso de tu código, puedes descubrir errores
o incoherencias lóg icas que antes habías pasado por alto.
regla del cero
Sugiere que evites escribir código para cosas que el lenguaje de
programación o las bibliotecas existentes pueden hacer por sí solos.
Si hay un comportamiento que puede implementarse sin escribir
código, entonces deberías basarte en el código existente.
semáforo
Objeto de sincronización que ayuda a gestionar el acceso a recursos
compartidos y a coordinar la comunicación entre procesos o hilos
concurrentes.
Hipótesis de Sapir-Whorf
También conocida como teoría de la relatividad lingüística, sugiere
que la estructura y el vocabulario de la lengua de una persona
pueden influir y moldear su percepción del mundo que le rodea. La
lengua que hablas no sólo refleja y representa la realidad, sino que
también desempeña un papel en su configuración y construcción.
Esto significa que la forma en que piensas y experimentas el mundo
está parcialmente determinada por el lenguaje que utilizas para
describirlo.
separación de intereses
Concepto que pretende dividir un sistema de software en partes
diferenciadas y autónomas, cada una de las cuales aborda un
aspecto o problema específico del sistema global. El objetivo es
crear un diseño modular y fácil de mantener que fomente la
reutilización del código, la escalabilidad y la facilidad de
comprensión, dividiéndolo en partes más pequeñas y manejables,
lo que permite a los desarrolladores centrarse en un aspecto a la
vez.
copia superficial
Copia de un objeto que crea una nueva referencia a la misma
ubicación de memoria donde está almacenado el objeto original.
Tanto el objeto original como su copia superficial comparten los
mismos valores. Los cambios que hagas en los valores de uno se
reflejarán en el otro. En cambio, una copia profunda crea una copia
completamente independiente del objeto original, con sus propias
propiedades y valores. Los cambios que realices en las propiedades
o valores del objeto original no afectarán a la copia profunda, y
viceversa.
cirugía de escopeta
Describe una situación en la que un único cambio en el código base
requiere múltiples cambios en diferentes partes del sistema. Ocurre
cuando los cambios en una parte de la base de código afectan a
muchas otras partes del sistema. Es análogo a disparar una
escopeta: una sola ráfaga puede alcanzar varios objetivos a la vez,
igual que un solo cambio de código puede afectar a varias partes
del sistema.
Simula
El primer lenguaje de programación orientado a objetos que
incorporó la clasificación. Su nombre indicaba claramente que el
objetivo de la construcción de software era crear un simulador.
Esto sigue siendo así en la mayoría de las aplicaciones informáticas
actuales.
punto único de fallo
Se refiere a un componente o parte de un sistema que, si fallara,
haría que todo el sistema fallara o dejara de estar disponible. El
sistema depende de este componente o parte, y sin él, nada puede
funcionar correctamente. Los buenos diseños intentan tener
componentes redundantes para evitar este efecto dominó.
principio de responsabilidad única
Establece que cada módulo o clase de un sistema de software debe
tener responsabilidad sobre una única parte de la funcionalidad
proporcionada por el software y esa responsabilidad debe estar
totalmente encapsulada por la clase. En otras palabras, una clase
sólo debe tener una razón para cambiar.
software linter
Comprueba automáticamente el código fuente en busca de
problemas previamente definidos. El objetivo de un linter es
ayudarte a detectar errores al principio del proceso de desarrollo,
antes de que sean más difíciles y costosos de solucionar. Puedes
configurar tu linter para que compruebe una amplia gama de
problemas, como el estilo de codificación, las convenciones de
nomenclatura y las vulnerabilidades de seguridad. Puedes utilizar
la mayoría de los linters como plug-ins en tu IDE y también pueden
añadir valor como pasos en la tubería de integración
continua/desarrollo continuo. También puedes conseguir los
mismos resultados con muchas herramientas de aprendizaje
automático generativo, como ChatGPT, Bard y muchas otras.
sistema de control de fuentes de software
Herramienta que permite a los desarrolladores hacer un
seguimiento de los cambios realizados en el código fuente de un
proyecto de software. Puedes trabajar con muchos otros
desarrolladores al mismo tiempo en el mismo código base, lo que
favorece la colaboración, la reversión de cambios y la gestión de
distintas versiones del código. En la actualidad, Git es el sistema
más utilizado.
Principios SÓLIDOS
Mnemotécnica que representa cinco principios de la programación
orientada a objetos. Fueron definidos por Robert Martin y son
directrices y heurísticos, no reglas rígidas. Se definen en los
capítulos relacionados:
Principio de responsabilidad única (ver Receta 4.7,
"Reificar las validaciones de cadenas")
Principio abierto-cerrado (ver Receta 14.3, "Reificar
variables booleanas")
Principio de sustitución de Liskov (ver Receta 19.1,
"Romper la herencia profunda")
Principio de segregación de interfaces (ver Receta 11.9,
"Romper las interfaces gordas")
Principio de inversión de dependencia (ver Receta 12.4,
"Eliminar interfaces de un solo uso")
código espagueti
Código mal estructurado, difícil de comprender y mantener. El
nombre "espagueti" se utiliza porque el código suele estar enredado
e interconectado de forma que se asemeja a un plato de fideos de
espagueti enredados. Contiene código redundante o duplicado, así
como numerosas sentencias condicionales, saltos y bucles que
pueden ser difíciles de seguir.
operador de dispersión
El operador de expansión en JavaScript se representa con tres
puntos (...). Permite expandir un iterable (como una matriz o una
cadena) en lugares donde se esperan cero o más elementos (o
caracteres). Por ejemplo, puedes utilizarlo para combinar matrices,
copiar matrices, insertar elementos en una matriz o expandir
propiedades de un objeto.
Inyección SQL
Se produce cuando un atacante inserta código SQL malicioso en un
programa que se comunica con una base de datos. El atacante
puede introducir código SQL en campos de entrada como cuadros
de texto o formularios. La aplicación puede entonces ejecutar el
código accediendo o modificando los datos, recuperando
información sensible o incluso tomando el control del sistema.
función estática
Pertenece a una clase y no a una instancia de esa clase. Esto
significa que se puede llamar a un método estático sin crear un
objeto de la clase.
patrón de diseño de estrategias
Define una familia de algoritmos intercambiables, encapsula cada
uno de ellos y los hace intercambiables en tiempo de ejecución. El
patrón permite a un objeto cliente elegir entre una gama de
algoritmos a utilizar, en función del contexto o situación específicos
en tiempo de ejecución. También promueve el acoplamiento
flexible entre el objeto cliente y las estrategias y te facilita ampliar o
modificar el comportamiento del objeto cliente sin afectar a su
implementación.
programación estructurada
Hace hincapié en el uso de construcciones de flujo de control, como
bucles y funciones, para mejorar la claridad, mantenibilidad,
legibilidad y fiabilidad de los programas informáticos.
Descompones un programa en piezas más pequeñas y manejables,
y luego organizas esas piezas utilizando construcciones de flujo de
control estructurado.
deuda técnica
Se refiere al aumento del coste de mantenimiento y mejora de los
sistemas de software a lo largo del tiempo, debido a malas prácticas
de desarrollo o elecciones de diseño. Al igual que la deuda
financiera acumula intereses con el tiempo, la deuda técnica se
acumula a medida que los desarrolladores toman atajos, hacen
concesiones en el diseño o no abordan adecuadamente los
problemas de la base de código del software. Acabas pagando más
por los intereses acumulados que por el capital inicial.
"Principio "Cuenta, no preguntes
Define una forma de interactuar con los objetos invocando sus
métodos en lugar de pedir sus datos.
desarrollo dirigido por pruebas (TDD)
Un proceso de desarrollo de software que se basa en la repetición
de un ciclo de desarrollo muy corto: primero, el desarrollador
escribe un caso de prueba automatizado que falla y que define una
mejora deseada o un nuevo comportamiento, luego produce un
código de producción mínimo para superar esa prueba y, por
último, refactoriza el nuevo código para que cumpla unos
estándares aceptables. Uno de los principales objetivos de TDD es
facilitar el mantenimiento del código asegurándose de que está bien
estructurado y sigue unos buenos principios de diseño. También
ayuda a detectar defectos en una fase temprana del proceso de
desarrollo, ya que cada nuevo fragmento de código se prueba en
cuanto se escribe.
lanza pronto y atrapa tarde
Hace hincapié en detectar y tratar los errores o excepciones lo antes
posible en el código, y aplazar su tratamiento o notificación real
hasta un nivel superior o un contexto más apropiado. Debes tratar
los errores lo más tarde posible, en un lugar donde dispongas de
más información contextual, en lugar de tomar decisiones
localizadas con información incompleta.
explicar
Aristóteles decía que "explicar es encontrar las causas". Según él,
todo fenómeno o acontecimiento tiene una causa o serie de causas
que lo producen o determinan. El objetivo de la ciencia es
identificar y comprender las causas de los fenómenos naturales y, a
partir de ahí, predecir cómo se comportarán en el futuro.
Para Aristóteles, "explicar" consistía en identificar y comprender
todas estas causas y cómo interactúan entre sí para producir un
fenómeno concreto. "Predecir", en cambio, se refiere a la capacidad
de utilizar este conocimiento de las causas para predecir cómo se
comportaría un fenómeno en el futuro.
rasgo
Define un conjunto de características o comportamientos comunes
que pueden compartir varias clases. Un rasgo es esencialmente un
conjunto de métodos que pueden ser reutilizados por diferentes
clases sin necesidad de que hereden de una superclase común.
Proporciona un mecanismo de reutilización de código más flexible
que la herencia, ya que permite a las clases heredar
comportamientos de múltiples fuentes.
veraz y falso
Términos utilizados para describir el valor booleano de un tipo de
dato no booleano en muchos lenguajes de programación. Todo
valor puede evaluarse como verdadero o falso en un contexto
booleano. Cuando un valor no booleano se evalúa en un contexto
booleano, se convierte mágicamente en booleano sin previo aviso.
Modelo Turing
Un ordenador basado en el modelo de Turing es una máquina
teórica capaz de realizar cualquier tarea computable para la que
pueda escribirse un conjunto de instrucciones, o algoritmo. La
máquina de Turing se considera el fundamento teórico de la
informática moderna, y sirve de modelo para el diseño y análisis de
ordenadores y lenguajes de programación reales.
Diagramas UML
Representaciones visuales estándar que describen la estructura y el
comportamiento de un sistema o aplicación de software con un
conjunto común de símbolos y notaciones. Se pusieron de moda en
los años 80 y 90 y estaban estrechamente relacionadas con el
modelo de desarrollo en cascada, en el que el diseño se termina
antes de empezar la codificación real, a diferencia de las
metodologías ágiles. Muchas organizaciones siguen utilizando UML
hoy en día.
optimización de máquinas virtuales
Hoy en día, la mayoría de los lenguajes de programación modernos
se ejecutan en máquinas virtuales (VM). Abstraen los detalles del
hardware y realizan muchas optimizaciones bajo el capó para que
puedas centrarte en hacer que el código sea legible y evitar la
optimización prematura(consulta el Capítulo 16, "Optimización
prematura"). Escribir código inteligente de alto rendimiento casi
nunca es necesario, ya que resuelven muchos problemas de
rendimiento. En el Capítulo 16 descubrirás cómo recopilar pruebas
reales para determinar si necesitas optimizar el código.
modelo en cascada
Un enfoque escalonado y secuencial para organizar el trabajo,
dividiéndolo en una serie de fases distintas con traspasos bien
definidos entre cada fase. La idea es que abordes cada fase por
turnos, en lugar de iterar. Esta era la idea dominante hasta que las
metodologías ágiles cobraron importancia en los años 90.
problema del yoyó
Ocurre cuando necesitas navegar por las clases y métodos de una
jerarquía de clases para comprender o modificar el código, lo que
dificulta el mantenimiento y la ampliación de la base de código.
Sobre el autor
Maximiliano Contieri lleva 25 años trabajando en la industria del
software, al tiempo que ejerce como profesor universitario. A lo largo de
los años, ha sido un ávido escritor en varias plataformas de blogs muy
conocidas, publicando múltiples artículos cada semana sobre una amplia
gama de temas como código limpio, refactorización, diseño de software,
desarrollo dirigido por pruebas y olores de código. El enfoque de Contieri
sobre la codificación se alinea con los paradigmas declarativo y
conductual, haciendo hincapié en el uso de los fundamentos del software
para construir soluciones elegantes, escalables y robustas.
Colofón
El animal de la portada de Clean Code Cookbook es una foca
gris(Halichoerus grypus). También se las llama cariñosamente "cabezas
de anzuelo" y "cerdo del mar con nariz de anzuelo" por sus grandes
narices.
Las focas grises pesan entre 550 y 880 libras y pueden medir entre 7,5 y 10
pies de largo. Cuando están en tierra, utilizan sus cortas aletas para
desplazarse con un movimiento similar al de una oruga. Pueden vivir
hasta 35 años y bucear a más de 1.000 pies de profundidad durante una
hora.
Las focas grises son excelentes cazadoras gracias a su aguda visión y oído.
Suelen cazar en manada, dándose un festín de peces, crustáceos,
calamares, pulpos y ocasionalmente aves marinas. Diariamente, las focas
grises pueden ingerir entre el cuatro y el seis por ciento de su peso
corporal en comida.
Hay tres poblaciones de focas grises en el mundo: una en el Atlántico
Norte (este de Canadá y noreste de [Link].), otra en el Atlántico Norte
oriental (Gran Bretaña, Islandia, Noruega, Dinamarca, Islas Feroe, Rusia)
y otra en el Mar Báltico. Habitan costas rocosas, islas, bancos de arena,
plataformas de hielo e icebergs.
La población de focas grises sufre varias amenazas. Pueden enredarse en
redes de pesca, sufrir acoso, contaminación química, vertidos de petróleo,
colisiones de embarcaciones y vehículos, y caza ilegal. Son un mamífero
marino protegido en Estados Unidos, pero algunos países permiten su
matanza legal para controlar la población y minimizar el impacto de la
foca en las poblaciones pesqueras de importancia comercial. A pesar de
estos problemas, la población de focas grises es numerosa, y se las
considera una especie poco preocupante en las listas de especies
amenazadas. Muchos de los animales de la portada de O'Reilly están en
peligro; todos ellos son importantes para el mundo.
La ilustración de la portada es obra de Karen Montgomery, basada en un
antiguo grabado lineal de Cuadrúpedos británicos. Los tipos de letra de
la portada son Gilroy Semibold y Guardian Sans. La fuente del texto es
Adobe Minion Pro; la fuente del encabezamiento es Adobe Myriad
Condensed; y la fuente del código es Ubuntu Mono de Dalton Maag.