El Manifiesto PHP Way of Life
- Introducción: PHP, el lenguaje de la Web
- Arreglos: La forma preferida de transferir datos, simple y flexible
- Objetos: Perfectos para organizar el código, mejor usados con lógica procedural
- Tipado: Estricto en espíritu, flexible en la práctica, pero nunca riguroso
- Interfaces web: El HTML generado en el servidor es clave para una Web rápida y accesible
- Bases de datos: Los ORM y NoSQL parecen tus aliados, pero SQL es el verdadero amigo
- Frameworks: Buenos sirvientes, malos amos
- Pruebas automatizadas: Unitarias, de integración, funcionales — encuentra tu equilibrio
- Microservicios: Es muy poco probable que los necesites
- APIs: Cuestiona los hábitos establecidos
- Seguridad: Los fundamentos no son negociables
- A. Recursos
- B. Referencias
Introducción: PHP, el lenguaje de la Web #
Un poco de historia #
La historia de PHP es conocida y documentada [Documentación PHP 1, Lerdorf 2]. Es un lenguaje que fue creado específicamente para la Web, en el momento en que la Web se expandía en los años 90.
Tuvo éxito entre los desarrolladores, porque era más simple que las principales soluciones utilizadas en aquella época para crear sitios dinámicos,
C y Perl. Y al mismo tiempo, presentaba una sintaxis similar, así que los desarrolladores se adaptaron rápidamente.
Tuvo éxito entre los proveedores de alojamiento, porque su archivo de configuración —con directivas como max_execution_time
y memory_limit
—
permitía alojar una gran cantidad de sitios en el mismo servidor, sin que un bug en un sitio impidiera el funcionamiento de los demás.
[Lerdorf 2]
La filosofía de PHP #
Uno de los principios básicos de PHP es que es utilizado tanto por desarrolladores aficionados como por grandes empresas.
Como dice su creador:
Siempre quise asegurarme de que la escala permitiera a los guerreros del fin de semana lanzar ideas interesantes sin tener que leer 30 libros diferentes. [Lerdorf 2]
Uno de los puntos fuertes de PHP es que escala. Puede hacer funcionar los sitios más grandes del mundo, y aun así sigue siendo accesible para los desarrolladores del fin de semana. Lograr ambas cosas con una sola base de código es todo un desafío.
Esta idea es fundamental, y no debe perderse de vista.
PHP es un lenguaje humilde. Está en su ADN desde el principio:
PHP es tan emocionante como tu cepillo de dientes. Lo usas todos los días, hace su trabajo, es una herramienta sencilla, ¿y qué? ¿Quién querría leer cosas sobre cepillos de dientes? [Lerdorf 4]
Nunca consideré PHP como algo más que una herramienta sencilla para resolver problemas. [Lerdorf 5]
Ha pasado tiempo y se ha invertido dinero desde esas declaraciones, pero esa filosofía ha guiado el desarrollo del lenguaje. PHP es un lenguaje para todos, no elitista.
PHP y el desarrollo moderno #
Desde la creación de PHP, la práctica del desarrollo web se ha enriquecido considerablemente. La ingeniería de software ha permitido construir aplicaciones de gran envergadura.
PHP mismo ha evolucionado enormemente, ofreciendo un modelo de objetos particularmente completo y un rendimiento que no ha dejado de mejorar.
Hoy en día, no todo el mundo adopta la nueva complejidad del desarrollo web. El artículo The New Internet de Avery Pennarun (CEO y cofundador de Tailscale) resume por sí solo el espíritu del Manifiesto:
Lo que hemos visto es que muchas cosas han mejorado desde los años 90. Las computadoras son literalmente millones de veces más rápidas. Y hoy en día, cien veces más personas pueden convertirse en desarrolladores, porque ya no están atrapados solo con C++ y ensamblador.
Pero, al mismo tiempo, algunas cosas han empeorado. Muchas tareas cotidianas que eran simples para los desarrolladores se han vuelto complicadas. Y eso no lo vimos venir.En lugar de eso, la industria tecnológica se ha convertido en un verdadero desastre. ¡Y no mejora, sino que empeora! Nuestra torre de complejidad es ahora tan alta que seriamente consideramos poner LLMs encima para que escriban el código incomprensible en marcos incomprensibles para que no tengamos que hacerlo nosotros mismos.
Leí un post recientemente donde alguien presumía de usar Kubernetes para ejecutar un sitio con 500,000 visitas al mes. Pero eso son 0.2 solicitudes por segundo. Podría servir eso desde mi teléfono, con batería, y pasaría la mayor parte del tiempo durmiendo.
En la informática moderna, toleramos compilaciones interminables, seguidas de compilaciones de Docker, cargas a registros de contenedores, implementaciones que tardan varios minutos antes de que el programa se ejecute, y aún más tiempo antes de que los registros aparezcan en algún lugar donde podamos leerlos. Todo ello porque nos han hecho creer que todo debe "escalar" necesariamente. La gente se emociona con los servicios de implementación a la moda porque tardan solo decenas de segundos en lugar de varios minutos. Pero en mi vieja computadora lenta de los años 90, podía ejecutar un script Perl o Python en unos pocos milisegundos, servir mucho más de 0.2 solicitudes por segundo, y ver los registros imprimirse inmediatamente en stderr, lo que me permitía hacer varios ciclos de edición-ejecución-depuración por minuto.
Como industria, hemos pasado todo nuestro tiempo haciendo posibles las cosas difíciles, pero nada de nuestro tiempo haciendo fáciles las cosas simples.
El desarrollo de software moderno es una sobrecarga desordenada [Pennarun 6]
Hay tres razones para esta complejidad creciente:
-
Las personas que son intrínsecamente incapaces de hacer las cosas de manera simple, o que valoran la complejidad.
Los complicadores son alérgicos a la simplificación. Su reflejo natural es transformar las tareas simples en pantanos, y rechazar las ideas claras hasta que estén enterradas (o asfixiadas) bajo capas de abstracción.
Los simplificadores se deleitan en la concisión. Buscan los 6x=6y en el mundo y felizmente los transforman en x=y. Nunca dejan que su ego se interponga en el camino más corto. [Berkun 7]
La gente confunde cada vez más complejidad y sofisticación. [Wikipedia 8]
-
Las personas que piensan que para mostrar sus competencias, deben constantemente surfear la ola de las últimas tecnologías de moda.
Un equipo elige adoptar la última tecnología de moda para su proyecto. Poco después, comienza a utilizarla, entusiasmado por esta novedad brillante, pero en lugar de ir más rápido (como prometía) y construir un mejor producto, se enfrenta a dificultades. Se ralentiza, se desmotiva y tiene problemas para entregar la próxima versión funcional en producción.
El problema del desarrollo guiado por la moda es que lleva muy fácilmente a malas decisiones. Las malas decisiones arquitectónicas y tecnológicas persiguen a menudo al equipo durante meses, incluso años. [Kirejczyk 9]
No hay orgullo en manejar o comprender la complejidad. [Kiehl 73]
-
La aplicación ciega de las buenas prácticas de las grandes empresas tecnológicas, sin darse cuenta de que lo que funciona para un equipo
de 400 personas no funcionará para un equipo de 12 o 80 personas.
[Las buenas prácticas] deberían inspirarnos, pero no aplicarse al pie de la letra. Cada contexto es único y complejo, con sus propias necesidades y objetivos, que no pueden ser reducidos ni satisfechos por una solución genérica. Nunca se debe aplicar una buena práctica sin tener en cuenta el contexto en el que se inscribe. [Goosens 10]
Las buenas prácticas, aunque útiles en algunos aspectos, son a menudo soluciones "talla única" que ignoran la realidad específica de cada audiencia, producto y empresa. Esto mata la innovación y empuja a las empresas hacia la mediocridad. [Knight 11]
La gente mira a Amazon o Google y se dice: "¡Hey, si funciona para los exitosos, también funcionará para mí!". ¡Bzzzzzzzzt!! ¡Falso!
Los modelos que tienen sentido para organizaciones cien veces más grandes que la tuya son a menudo exactamente lo opuesto a lo que te convendría. [DHH 74]
No hay que olvidar que el código más simple es el que mejor funciona en empresa, tanto a corto plazo (más rápido de desarrollar, más fácil de probar, más rápido de ejecutar) como a largo plazo (más fácil de modificar, más resistente a las regresiones).
Las personas "más avanzadas" usan a menudo soluciones tan simples que parecen de personas que no saben lo que hacen. Las personas promedio a menudo están en la categoría de "saben lo suficiente como para ser peligrosas" porque reflexionan, trabajan y procesan todo de manera excesiva debido a la falta de una experiencia más completa para descubrir soluciones más simples y limpias. [Stancliff 12]
> El mejor código, escrito por los desarrolladores más experimentados, a menudo parece código de principiantes — excepto que funciona.
¿Podría ser que las mejores prácticas estén diseñadas para asegurarse de que los programadores mediocres trabajando juntos produzcan un código decente?
Después de todo, los verdaderos principiantes escriben un código muy similar al de los mejores, excepto que no funciona. [Hacker News 13]
Bajo el pretexto de la profesionalización, el ecosistema PHP se ha inspirado mucho en el mundo Java y ha perdido de vista lo que hacía fuerte al lenguaje,
y que en ningún caso lo hacía amateur.
Pero no tiene por qué ser así.
Al esquematizar al extremo, la naturaleza humana sigue ciclos de complejidad y simplificación. En informática, cada tecnología tiende a volverse más compleja (por ejemplo, C ➡ C++, Lisp ➡ Haskell, Java ➡ JEE, CGI ➡ WebSphere) hasta que aparece una tecnología más simple (por ejemplo, Fortran ➡ BASIC, C ➡ Perl, C++ ➡ Java, Perl ➡ PHP/Python, Objective-C ➡ Swift).
Hoy en día, después de haber aceptado volverse más complejo, el ecosistema PHP está listo para volver a sus raíces y encontrar un mejor equilibrio.
Arrays: La forma preferida de transferir datos, simple y flexible #
Los arrays de PHP han sido un tipo de datos relativamente único en los lenguajes de programación. Son extremadamente versátiles y pueden usarse como listas indexadas numéricamente, arrays asociativos, o una combinación de ambos, mientras preservan el orden de inserción.
Las primeras versiones de PHP v1 tenían implementaciones distintas para listas, mapas y conjuntos. Pero las reemplacé rápidamente por una implementación híbrida unificada de mapa ordenado, que simplemente llamé "Array". La idea era que en casi todas las situaciones de una aplicación web, un mapa ordenado puede resolver el problema. Se parece lo suficiente a un array para usarse donde se espera un array, al mismo tiempo que evita el dolor de cabeza de presentar al usuario 3 o 4 tipos diferentes, con las palabras clave y la sintaxis asociadas, forzando al usuario a adivinar cuál usar. Esta decisión se tomó en 1994, y aparte de algunas críticas puntillosas sobre el nombre a lo largo de los años, creo que ha resistido bastante bien la prueba del tiempo. [Lerdorf 3]
Se puede observar que otros lenguajes de programación han modificado desde entonces sus equivalentes (llamados diccionario o hash) para que tengan un comportamiento similar por defecto.
Los arrays son fáciles de entender y usar para los principiantes. Añadir elementos, iterar sobre ellos, serializarlos/deserializarlos en JSON es sencillo. Un gran número de funciones nativas están disponibles para manipularlos.
Los arrays de PHP pueden usarse para describir tipos de datos tan variados como las pilas, colas o registros.
Un código que usa arrays es fácil de leer y entender; es autónomo, sin objetos adicionales que inspeccionar.
Cuando necesites transferir datos, comienza haciéndolo usando arrays. Su flexibilidad te permitirá adaptar tu desarrollo fácilmente a
medida que la necesidad evolucione.
La mayor parte del tiempo, verás que esto es suficiente, incluso a largo plazo. Tu código será explícito al mismo tiempo que evolutivo.
PHP ofrece estructuras de datos más especializadas (SplDoublyLinkedList, SplStack, SplQueue, SplHeap, SplFixedArray…). Pero úsalas solo si está plenamente justificado. No busques optimizar prematuramente tu código; las fuentes de rendimiento suelen estar en otro lugar.
No comiences a crear DTO (Data Transfer Object, objetos sin lógica de negocio que solo transportan datos) desde el principio, solo harás que tu código sea innecesariamente complejo.
Llegamos a la misma conclusión en varios proyectos en el trabajo. Comenzamos con implementaciones ingenuas de objetos, luego retrocedimos — solo por simplicidad — a pasar conjuntos de datos brutos. [Atwood 14]
Por supuesto, hay casos en los que un objeto será más adecuado, especialmente si estás manteniendo una biblioteca (para evitar escribir código defensivo que verifique todo lo que recibe como entrada). Pero en tu propio código de negocio, esto no debería ser tu primer impulso.
Objetos: Perfectos para organizar el código, a utilizar de preferencia con una lógica procedural #
Un poco de contexto #
La programación orientada a objetos es un concepto que existe desde hace mucho tiempo, con las primeras implementaciones en el lenguaje Simula en 1967 [Wikipedia 15]. La mayoría de los lenguajes orientados a objetos comparten los conceptos de herencia, encapsulación y polimorfismo. PHP ofrece un modelo orientado a objetos sólido desde la salida de PHP 5 en 2005.
Hoy en día, parece evidente que la utilización de objetos es algo natural. La práctica del ingenio lógico se ha generalizado, y los patrones de diseño ahorran tiempo al proporcionar un lenguaje común a todos los desarrolladores.
Sin embargo, muchas voces se alzan para afirmar que la programación orientada a objetos no es la panacea universal para todos los problemas de desarrollo.
En este sentido, algunos consideran que la POO (programación orientada a objetos) conviene a ciertos tipos de desarrollo, pero no a todos.
Creo que los objetos, las clases, el polimorfismo e incluso la herencia pueden ser herramientas valiosas en ciertos casos. Pero contrariamente a lo que promueve la POO, estos son casos de nicho, no la norma por defecto. [Will 16]
El concepto de diseño orientado a objetos se reveló útil en los sistemas gráficos, las interfaces de usuario gráficas y ciertos tipos de simulación. Pero para sorpresa (y decepción progresiva) de muchas personas, ha resultado difícil demostrar beneficios significativos de la aproximación de objetos fuera de estos dominios. [Raymond 17]
La orientación a objetos es verborrágica #
El primer argumento que lleva a este razonamiento es el hecho de que la aproximación orientada a objetos es mucho más compleja y verborrágica que una aproximación procedural clásica, llevando a escribir (y por lo tanto a mantener) mucho más código. Se vuelve cada vez más difícil representarse mentalmente el conjunto de objetos y sus interacciones. El flujo de ejecución se vuelve mucho más difícil de seguir en comparación con un código procedural.
Añadir objetos a su código es como añadir sal a un plato: en pequeña cantidad, realza el sabor; añade demasiado y lo arruina todo. A veces, es mejor pecar por exceso de simplicidad y tiendo a favorecer la aproximación que reduce la cantidad de código en lugar de la que la aumenta. [Atwood 18]
He trabajado con desarrolladores que insistían en que todo debía pasar por un modelo de objetos, incluso si eso multiplicaba la cantidad de código. [Atwood 19]
Creo que los grandes programas orientados a objetos se vuelven cada vez más complejos a medida que se construye un vasto gráfico de objetos mutables. Usted sabe, intentar comprender y mantener en su mente qué sucederá cuando se llama a un método y cuáles serán los efectos secundarios. [Hickey 20]
El problema con la programación orientada a objetos es que estos lenguajes fueron diseñados para ayudar a los desarrolladores a gestionar su código… Pero hoy en día, es casi imposible seguir el flujo de ejecución. Ya no es posible detectar errores relacionados con este flujo simplemente leyendo el código. [Shelly 21]
Algunos comentaristas afirman que sin POO, el código terminará inevitablemente en espagueti. El miedo al monstruo de espagueti es una fobia sana entre los desarrolladores, pero la POO no nos protege de los espaguetis, solo los oculta detrás de capas de indirección. [Will 16]
Separación entre datos y tratamientos #
También está el hecho de que los datos y las operaciones son dos cosas diferentes que no tienen por qué estar mezcladas. La programación se hace con verbos y no con nombres; hacemos cosas, no nos contentamos con manipular conceptos abstractos.
Dejemos que los datos sean datos. Dejemos que las acciones sean acciones.
No necesitamos conceptualizar todo lo que queremos hacer en el código en términos de datos. No necesitamos transformar todos nuestros verbos en nombres. [Will 22]
No existe ninguna prueba objetiva y accesible que demuestre que la POO es superior a la simple programación procedural. La POO no es natural para el cerebro humano, nuestra forma de pensar está centrada en la acción: ir a caminar, hablar con un amigo, comer una pizza. Nuestro cerebro ha evolucionado para hacer cosas, no para organizar el mundo en jerarquías complejas de objetos abstractos. [Suzdalnitski 23]
Los objetos vinculan funciones y estructuras de datos en unidades indivisibles. Creo que esto es un error fundamental, porque funciones y estructuras de datos pertenecen a dos mundos totalmente diferentes. [Armstrong 24]
Los fanáticos de la orientación a objetos temen a los datos. Prefieren declaraciones o constructores a tablas inicializadas simples. No quieren escribir pruebas guiadas por tablas. ¿Por qué? ¿Qué mentalidad lleva a pensar que una jerarquía de tipos de varios niveles, con capas de abstracción, es preferible a una tabla de tres líneas? Un día, escuché a alguien decir que estimaba que su trabajo era eliminar todas las bucles while existentes en el código, reemplazándolos por objetos. ¿En serio? [Pike 25]
Orientación a objetos versus enfoque procedural #
Pero, ¿realmente hay que oponer POO y código procedural? Sí y no.
Sí, si se sumerge en el "todo objeto"; no, si se utilizan los objetos de manera inteligente junto con una lógica procedural.
Porque, aunque es indudable que los objetos son muy prácticos en muchas situaciones, se pueden usar de una manera que permita
seguir la ejecución del código paso a paso.
Tomemos los ejemplos dados por Yegor Bugayenko (director de laboratorio en Huawei) en su artículo “OOP Alternative to Utility Classes”. Estos ejemplos están en Java, pero los principios siguen siendo los mismos.
Nos explica que, para determinar el mayor de dos números, el enfoque procedural es tener una clase utilitaria que contenga un método max(), mientras que el enfoque POO sería tener un objeto Max.
Esto daría lugar a este código:
// versión procedural
int max = NumberUtils.max(10, 5);
// versión objeto
int max = new Max(10, 5).intValue();
¿Es la segunda más legible? Claramente no. Además, el código fuente del objeto Max es mucho más verboso que el de NumberUtils. Y, ¿realmente queremos crear un objeto para cada operación posible e imaginable? Claramente no.
Su segundo ejemplo presenta una función que lee un archivo, elimina los espacios al inicio y al final de cada línea, y escribe el resultado en otro archivo.
Aquí está la versión procedural:
void transform(File in, File out) {
Collection src = FileUtils.readLines(in, "UTF-8");
Collection dest = new ArrayList<>(src.size());
for (String line : src) {
dest.add(line.trim());
}
FileUtils.writeLines(out, dest, "UTF-8");
}
Y aquí está la versión estrictamente orientada a objetos:
void transform(File in, File out) {
Collection src = new Trimmed(
new FileLines(new UnicodeFile(in))
);
Collection dest = new FileLines(
new UnicodeFile(out)
);
dest.addAll(src);
}
Tómese el tiempo de leer cada función.
La primera se puede leer línea por línea, permitiéndonos comprender las operaciones que se realizan.
Incluso sin conocer las funciones llamadas, podemos intuir qué hacen.
La segunda función, en cambio, instancia varios objetos, pasándolos como parámetros unos a otros.
Es difícil seguir el curso probable de la ejecución solo leyendo este código.
Queda claro que con una programación orientada a objetos estricta, el esfuerzo cognitivo necesario para comprender
el código es mucho mayor.
Aunque esto podría estar alineado con el "Java Way of Life", no es en absoluto compatible con el PHP Way of Life.
Para concluir #
Si desea adoptar un enfoque de "todo objeto", haga Java, no PHP.
Tipado: Fuerte en principio, adaptable en la práctica, pero nunca estricto #
PHP ofrece una gestión de tipos que es flexible. Las primeras versiones de PHP tenían un tipado débil, pero ha sido posible tipar los parámetros y los retornos de funciones y métodos durante mucho tiempo. Más recientemente, se añadió la opción strict_types, que fuerza un tipado fuerte.
El tipado PHP clásico #
El tipado de los parámetros y los retornos de funciones permite asegurarse, dentro de una función, de que los tipos recibidos son los que se desean.
Tomemos el ejemplo simple (y en la práctica inútil) de una función de concatenación de cadenas. Sin tipado de parámetros, a menudo escribimos código defensivo que verifica los tipos de los datos de entrada:
function concat($a, $b)
{
if (!is_scalar($a) || !is_scalar($b))
throw new \TypeError(“Parámetro incorrecto.”);
return $a . $b;
}
Mientras que con un tipado de parámetros, estamos seguros de recibir datos manipulables sin problemas:
function concat(string $a, string $b) : string
{
return $a . $b;
}
Mientras que la función concat() se llama con parámetros escalares (booleano, entero, flotante, cadena), PHP realizará la conversión, y la función recibirá cadenas. Si sabemos que el código llamante no puede contener más que escalares, no se necesita gestión de errores ni conversión explícita:
$result = concat($str1, $str2);
Si, por el contrario, el código llamante no controla completamente los datos que se enviarán a la función, simplemente se gestiona la excepción TypeError que se lanzará en caso de conversión imposible:
try {
$result = concat($str1, $str2);
} catch (\TypeError $te) {
// gestion d'erreur
}
La mayor parte del tiempo, las excepciones se gestionan a un nivel superior para mantener el código lo más simple posible.
El tipado PHP estricto #
Si la directiva strict_types está activada, la llamada requiere parámetros del tipo correcto. Si sabemos que el código llamante solo contiene escalares, se deben realizar conversiones explícitas:
$result = concat((string)$str1, (string)$str2);
Podemos ver que, incluso para un ejemplo tan simple, el código es más verboso. Su lectura requiere un esfuerzo cognitivo adicional.
Si los datos de origen en las variables fuente pueden contener cualquier dato, se debe verificar sus tipos y gestionar los errores, además de realizar conversiones explícitas:
if (!is_scalar($str1) || !is_scalar($str2)) {
// gestión de error
}
$result = concat((string)$str1, (string)$str2);
Aunque la activación del modo strict_types está de moda, su uso lleva a un código más difícil de mantener. Las herramientas de análisis estático de código permiten prevenir la gran mayoría de los casos en los que un tipado estricto es útil.
Es interesante señalar que Gina Banyard (miembro del equipo central de PHP) ha propuesto una RFC que sugiere eliminar la directiva strict_types:
La utilización ciega del modo de tipado estricto ha tenido consecuencias imprevistas:
- La utilización de casts (conversiones de tipo) explícitos para cumplir con los requisitos de tipo, incluso cuando son menos "type safe"
- La percepción de la necesidad de realizar casts de tipo dits “strictos”
- Análisis manual o manipulación de tipos (“type juggling”) que el motor PHP ya puede manejar automáticamente
Debido al uso sistemático del modo de tipado estricto impuesto por los estilos de codificación modernos, muchos usuarios no entienden el alcance de la declaración declare, ni lo que realmente hace.
Muchos asumen que hace que PHP sea más estricto en cuanto a la conversión de tipos, aunque en realidad solo afecta al paso de escalares a las funciones llamadas en el código de usuario, los retornos de escalares de funciones personalizadas del usuario, y la asignación de una propiedad escalar tipada.
No previene que los tipos se manipulen con operadores, ni en las llamadas a funciones internas, incluso si esas funciones se llaman en el código de usuario con tipado estricto activado. Los manejadores de errores, excepciones o cierres de PHP son buenos ejemplos.Demasiado estricto puede llevar a demasiado laxo.
La necesidad percibida de realizar casts de tipo dits “strictos” es un síntoma claro de que se están utilizando casts de tipo en lugares donde no deberían, simplemente para cumplir con la declaración de tipo de un parámetro de función. [Banyard 26]
Conclusión #
El uso del tipado para los parámetros y los retornos de funciones ha demostrado su valía. Hace el código más robusto.
Los lenguajes tipados son esenciales en equipos con niveles de experiencia heterogéneos. [Kiehl 73]
Sin embargo, no active strict_types por defecto. No aporta realmente ningún valor.
Interfaces web: El HTML generado en el servidor es la clave de un Internet rápido y accesible #
Contexto general #
Originalmente, PHP fue diseñado para ser un motor de plantillas, permitiendo insertar llamadas a código en C dentro de un archivo HTML. Y de alguna manera, todavía es posible usarlo de esta forma.
Hoy en día, existen dos maneras de crear un sitio web: generando el HTML en el servidor o proporcionando una API a la que se conecta una aplicación JavaScript.
Los frameworks de JavaScript se han vuelto muy comunes, impulsados por la moda de las aplicaciones de una sola página (Single Page Application). Para aplicaciones con una complejidad funcional, su utilidad no está en duda.
El problema de los frameworks JavaScript #
Desafortunadamente, con la lógica habitual de generalizar sin discernimiento las "buenas prácticas" de grandes empresas, se puede ver que
una mayoría de sitios web utilizan frameworks JavaScript, incluso cuando su funcionamiento es puramente transaccional: páginas con enlaces
y formularios; al hacer clic en un enlace, se carga otra página; al enviar un formulario, se recibe una redirección.
En este caso, la generación de páginas en el navegador solo alarga los tiempos de carga, hace que los desarrollos sean más complejos y dificulta los depurados.
Para los usuarios, los impactos de estos frameworks son inmediatos: las páginas web se vuelven más pesadas, se cargan más lentamente y son menos accesibles.
En los últimos años, el rendimiento web parece haber pasado a un segundo plano. De hecho, con muchos sitios utilizando ahora frameworks como React y Vue, las aplicaciones de una sola página (SPA) se han vuelto comunes y las solicitudes se cuentan por cientos, la página web promedio es hoy más voluminosa que nunca, con páginas de 2 a 3 MB siendo más comunes que nunca.
El byte más costoso en términos de rendimiento en la web es el JavaScript. JavaScript afecta el rendimiento de la red, el tiempo de procesamiento del procesador, la memoria utilizada y la experiencia del usuario en su conjunto. Los scripts ineficientes pueden ralentizar tu sitio, haciéndolo menos reactivo y más frustrante para tus usuarios. [Zeman 28]
La búsqueda incesante de los frameworks de JavaScript "de última moda" ha contribuido involuntariamente a hacer que la web sea menos accesible, con un impacto desproporcionado en los usuarios.
Un planteamiento más sensato sería centrarse en el acceso a la información y la accesibilidad, más que en interfaces llamativas.
Sin contar las dependencias de numerosas bibliotecas JavaScript, que tienen sus propias vulnerabilidades o ciclos de actualización frenéticos.
Tengo la impresión de que pocas personas hablan de la fatiga relacionada con la gestión de dependencias.
Pasaba mucho tiempo gestionando actualizaciones de paquetes, especialmente paquetes de React. Actualizaba mis paquetes a su última versión, solo para descubrir que sus APIs habían cambiado de manera no retrocompatible, obligándome a invertir tiempo en refactorizar mi código.Si intentas construir un producto que requiera el menor mantenimiento posible una vez entregado, te aconsejo que te mantengas lo más lejos posible del ecosistema JavaScript. [Rodriguez 30]
[A propósito del paquete left-pad] Lo que me preocupa aquí es que tantos paquetes y proyectos han añadido una dependencia para una simple función de relleno a la izquierda en una cadena, en lugar de que sus desarrolladores pasaran 2 minutos escribiendo ellos mismos una función tan básica.
Una instalación nueva del paquete Babel contiene 41,000 archivos.
Un simple template de aplicación basado en jspm/npm ahora arranca con más de 28,000 archivos.¿Hemos olvidado cómo programar? [Haney 31]
Pero entonces, ¿por qué seguir utilizando estos frameworks incluso cuando no se justifica? Algunas personas han llegado a pensar que muchos desarrolladores anteponen su deseo de usar tecnologías "de última moda" al bienestar de sus usuarios, o que la influencia de las grandes empresas tecnológicas es abrumadora.
El problema es el estado de ánimo de los desarrolladores y diseñadores (…) que consideran que el desarrollo web debería ser ante todo "divertido". Estoy convencido de que muchos desarrolladores y ingenieros de software anteponen su propia satisfacción a la de sus usuarios o clientes.
Y esto ha llevado a todas esas prácticas dudosas, así como a una falta de interés por lo que realmente importa. Sistemas de compilación pesados como Webpack y decenas de componentes prefabricados de NPM se integran para "ahorrar tiempo a los desarrolladores", sin preocuparse demasiado por los kilobytes (o incluso megabytes) adicionales de JavaScript que esto añade al producto final.
¿Por qué es tan complicado? En esencia, codificar el frontend de una aplicación web es complejo; hay muchos elementos que interactúan, muchas cosas que pueden salir mal, entonces, ¿por qué no dejar que los "expertos" de Facebook o Google nos digan qué hacer, ¿verdad?
También hay razones más cínicas. Me gusta la idea de que los desarrolladores frontend han sido criticados como "desarrolladores frontend, solo son un montón de inútiles" durante tanto tiempo que ahora están compensando en exceso haciendo cosas super complicadas, solo para poder decir "Oye, yo también soy un ingeniero".
Otra razón cínica es que empresas como Facebook y Google, que promueven estos frameworks, tienen todo el interés en ganar influencia, porque es mejor para ellos que la gente use las tecnologías que han creado, lo que aumenta su reputación. [Holovaty 32]
Generación en el servidor y mejora progresiva #
La solución es basarse en lo que mejor dominamos: generar páginas HTML en el servidor. Asociarlo con CSS y añadir JavaScript solo para mejoras progresivas si es necesario.
Mejor práctica para optimizar JavaScript: En la medida de lo posible, no uses JavaScript
Generar HTML en el servidor es una aproximación mucho más eficiente que depender del JavaScript en el cliente para generar todo.Reducir la dependencia de JavaScript durante la carga de las páginas no solo disminuye la cantidad de código que el navegador debe descargar, analizar, compilar y ejecutar, sino que también le permite aprovechar sus propias optimizaciones internas para un rendimiento máximo. [Zeman 28]
El código ejecutado en el servidor puede ser completamente contabilizado.
El código ejecutado en el cliente, en cambio, se ejecuta en la máquina del diablo.Por lo tanto, una estrategia absurdamente eficaz consiste en enviar menos código. En la práctica, esto significa que debes priorizar HTML y CSS sobre JavaScript, ya que se degradan propremente y presentan tasas de compresión más altas.
La única cosa que realmente mejora una experiencia web es preocuparse sinceramente por la experiencia del usuario. [Russell 33]
La mejora progresiva es un método de creación de sitios y aplicaciones web basado en la idea de que primero se debe hacer que la página funcione en HTML. Solo después se añaden CSS y JavaScript.
Si crees que tu servicio no puede construirse sin JavaScript, considera usar soluciones más simples basadas en HTML y CSS que satisfagan las necesidades de los usuarios.
Si usas JavaScript, debería servir solo para mejorar el HTML y el CSS, de modo que los usuarios puedan seguir utilizando el servicio incluso si falla la ejecución de JavaScript.
No construyas tu servicio como una aplicación de una sola página (SPA). [UK Government 34]
La mejora progresiva es un principio de diseño y desarrollo que consiste en construir por capas, que se activan automáticamente según las capacidades del navegador. Por defecto, estas capas están desactivadas, lo que garantiza una base sólida y accesible para todos.
Se aplica este principio con un enfoque declarativo, que ya está integrado en la forma en que el navegador maneja HTML y CSS. En cuanto a JavaScript, que es imperativo, solo se usa para enriquecer la experiencia, no como una dependencia obligatoria. Solo se carga cuando los elementos fundamentales (HTML y CSS) ya ofrecen una experiencia de usuario de calidad. [Bell 35]
Si realmente necesitas añadir JavaScript a tus páginas, evita usar un framework completo. Tu sitio seguirá siendo más ligero, reactivo y accesible usando JavaScript básico, o una biblioteca especializada como Vik, Turbo, htmx, o incluso el buen viejo jQuery. Pero nunca deberías necesitar crear un "controlador" en JavaScript.
React nos sirvió bien, pero las cosas que eran fáciles se volvieron más difíciles. Nos dimos cuenta cuando añadir un simple campo a un formulario se convirtió en una solicitud pull de 700 líneas. Al pasar de React a StimulusJS, eliminamos aproximadamente el 60% de nuestro JavaScript. Curiosamente, la cantidad de JavaScript en nuestra aplicación se ha mantenido globalmente estable desde entonces.
StimulusJS permite consolidar mejor la lógica y el estado de la aplicación en el backend. Sí, perdemos un poco de reactividad en el cliente, pero la gestión del estado en el cliente es una ilusión. StimulusJS te obliga a escribir la menor cantidad de JavaScript posible.
Las líneas de código son un lastre, no una herramienta.Ahora estamos en una pila técnica con la que no tenemos que luchar. [Sutton 36]
Creo que la era de los "clientes pesados" y los frontales sobrecargados de JavaScript está llegando a su fin. El entusiasmo por las "aplicaciones de borde" está mal dirigido e innecesario para la creación de muchos tipos de negocios prósperos. Muchas interacciones son imposibles sin JavaScript, pero eso no es una razón para escribir más de lo necesario.
Lo mejor es no tener que soportar la carga mental de la gestión de estado en el frontend. Todo es solo una página HTML con una pincelada de JavaScript, no hay estado que mantener entre los cambios de página. No hay gestión de estado complicada en el cliente. [Sutton 37]
Un front-end simple para un build simple #
Para ir aún más lejos, simplificando el desarrollo front-end, simplificas tu proceso de build. En lugar de pasar por etapas complejas, idealmente no deberías necesitar ningún preprocesamiento antes de poder desplegar tu sitio. Cuanto más rápido sea tu ciclo de desarrollo-despliegue-prueba-depuración, más eficiencia ganarás.
El transpiling con Babel marcó el inicio de una era de pipelines y herramientas horriblemente complejas. Escribir el JavaScript del futuro no era gratuito. El precio a pagar era un enmarañamiento de complejidad cada vez más vasto.
Ya no creo que este compromiso valga la pena para la mayoría de las nuevas aplicaciones. [DHH 38]
Nada es más rápido que no hacer build
Trabajo sin ninguna etapa de build front-end significativa. Todo es tan… simple. Y es rápido. Realmente rápido. Increíblemente rápido.
Por primera vez en tal vez 15 años, el estado del arte no es inventar maneras más sofisticadas de compilar JavaScript o CSS. Es no compilar nada en absoluto. [DHH 39]
Bases de datos: Los ORM y NoSQL parecen ser tus amigos, pero SQL lo es realmente #
En una aplicación web, el acceso a los datos puede realizarse de varias maneras diferentes: a través de APIs, gracias a bases de datos relacionales (SGBD), bases de datos no relacionales (NoSQL), archivos, etc.
Las bases NoSQL #
Existe una multitud de bases NoSQL diferentes, basadas en mecanismos variados. Algunas de ellas son particularmente eficaces en casos de uso muy específicos.
Las bases orientadas a documentos han ganado notoriedad, pero cuidado con no tomarlas por lo que no son. Si prometen indexar la integridad de los documentos almacenados, realizar consultas complejas que abarquen un gran número de parámetros — como se haría con consultas relacionales utilizando joins — resulta en un rendimiento decepcionante.
Los joins en MongoDB son muy frágiles (con el más mínimo cambio, hay que reescribir las aplicaciones en profundidad), y en muchos casos, el rendimiento de MongoDB es muy mediocre.
Los joins son generalmente lentos en MongoDB. No dispone de optimizador de consultas y la estrategia de ejecución está codificada en la aplicación. Tan pronto como el merge sort o el hash join serían la mejor opción, el rendimiento de Mongo se desploma. [Stonebraker 40]
MongoDB tiene excelentes rendimientos en escritura, lo que lo convierte en una excelente opción para aplicaciones que requieren una inserción rápida de datos. En contraste, MySQL es más performante en lectura, especialmente para consultas que pueden aprovechar una indexación eficaz.
Si el rendimiento en escritura es esencial, MongoDB es la mejor opción. Sin embargo, si tu aplicación requiere lecturas rápidas sobre campos indexados, MySQL será probablemente una mejor opción. [Verma 41]
DynamoDB es la peor opción posible para el desarrollo de aplicaciones generalistas. [Kiehl 73]
En general, las bases NoSQL se manipulan a través de APIs específicas, que no son ni más eficientes ni más legibles que las consultas SQL.
Nota que el código [usando MongoDB] es mucho más complejo que el código usando Postgres, ya que MongoDB no tiene nociones de joins relacionales y utiliza un lenguaje de nivel inferior a SQL. Además, el desarrollador debe construir algorítmicamente un plan de consulta para el join. [Stonebraker 40]
Por muy molesto que sea, SQL sigue siendo mucho más conciso y legible que varias líneas de llamadas a una API. Además, para explicar cómo usar su API, la documentación de MongoDB lista las consultas SQL equivalentes. Es un indicio bastante claro a favor de la legibilidad y ergonomía de SQL. [Voss 42]
Cuando inicies un proyecto, es muy probable que una base de datos relacional sea lo que necesitas.
Si tienes necesidades muy variadas, usa cada sistema donde más destaque:
MySQL/PostgreSQL
para datos relacionales, Redis para pares clave-valor,
ElasticSearch para indexación de texto completo,
MongoDB/CouchDB/CouchBase
para documentos sin esquema, un sistema de archivos en red o
un almacenamiento en la nube para archivos binarios, etc.
El modelo relacional tiene algo mágico. Crea un modelo de entidades, vierte datos en él, y obtienes respuestas.
Si no conoces todas las preguntas que podrías tener que hacer sobre tus datos, lo más seguro es almacenarlos en un SGBD. Y cuando inicias un proyecto, casi nunca conoces todas las preguntas que tendrás que hacer. Mi consejo: usa siempre un SGBD. Pero no te limites solo a un SGBD. [Voss 42]
Los ORMs #
En cuanto al acceso a bases relacionales, habitualmente se contraponen las consultas SQL y el uso de ORM (Object-Relational Mapping).
Los ORMs ofrecen:
- La posibilidad de manipular los datos con código orientado a objetos, sin necesidad de escribir consultas SQL;
- un medio para mapear la información almacenada en la base de datos en objetos (llamados entidades);
- una capa de abstracción que permite pasar de un sistema de gestión de base de datos a otro de manera transparente.
A menudo se ven como un patrón de diseño moderno y eficiente, pero algunas personas lo consideran un anti-patrón.
Quiero ser muy claro al respecto: los ORMs son una idea estúpida.
El ORM nació porque SQL parecía complicado e intimidante. Así que se lanzó una capa de abstracción sobre él, y se hizo como si el SGBD ni siquiera existiera debajo. Esto es claramente absurdo. [Voss 42]
Un defensor del ORM dirá que no todo el mundo necesita hacer joins complejos, que el ORM es una solución "80/20", donde el 80% de los usuarios solo necesitan el 20% de las funcionalidades de SQL, y que el ORM puede manejarlo. Todo lo que puedo decir es que en mis quince años de desarrollo de aplicaciones web utilizando bases de datos, nunca ha sido cierto para mí. Solo al principio de un proyecto puedes salirte sin joins o con joins ingenuos. [Voss 43]
El ORM es un anti-patrón terrible que viola todos los principios de la programación orientada a objetos. No hay excusa para la existencia de un ORM en una aplicación, ya sea una pequeña aplicación web o un sistema empresarial con miles de tablas y operaciones CRUD.
Afirmo que la idea misma detrás de los ORMs es fundamentalmente defectuosa. [Bugayenko 44]
Cada vez que desarrollamos una funcionalidad compleja en una nueva capa, corremos el riesgo de aumentar la complejidad global cuando esa capa se vuelve compleja. Esto a menudo resulta en fugas en las abstracciones: la capa no logra encapsular completamente la funcionalidad subyacente, y los desarrolladores entonces tienen que lidiar con ambas capas al mismo tiempo. [Bendersky 45]
Personalmente, creo que la única solución viable al problema de los ORMs es elegir: o abandonar las bases relacionales o abandonar los objetos. Si eliminas el O o el R de la ecuación, el problema de mapeo desaparece.
Ambos enfoques se defienden. Tiendo a inclinarme por el lado de la base de datos, porque creo que los objetos están sobrevalorados. [Atwood 46]
[Los ORMs] deberían evitarse, ya que a menudo introducen lenguajes, paradigmas y sistemas completamente nuevos, pero no aportan ninguna ventaja real, ya sea para mapeos simples o complejos. En el caso de un mapeo objeto-relacional simple, la tarea es sencilla y no se necesita ninguna herramienta. En los casos complejos, el ORM añade complejidad y a menudo se requiere intervención manual, lo que anula el beneficio de la automatización.
El código SQL debe permanecer visible en tus objetos de negocio. Los desarrolladores necesitan entender lo que sucede cuando se recuperan objetos o se manipulan los datos subyacentes. [Maffey 47]
Los ORMs son el diablo en todos los lenguajes e implementaciones. Simplemente escribe ese maldito SQL. [Kiehl 73]
Incluso el co-creador de Propel (uno de los principales ORMs PHP en los años 2010) dice:
Personalmente, ya no uso ORMs. [Zaninotto 48]
Escribir consultas solo en PHP daría a entender que SQL es un lenguaje inútil, que debería ocultarse al máximo. Sin embargo, se dice constantemente que todo desarrollador debe conocer varios lenguajes, que es absolutamente necesario.
Aprender varios lenguajes es una excelente idea — no solo te da mayor flexibilidad en la búsqueda de empleo, sino que también amplía tu mente y tu visión de lo que es la programación, simplemente. [Martelli 49]
¿Por qué SQL debería ser el único lenguaje que no merece consideración? Es porque es un lenguaje poderoso que ha demostrado su estabilidad a lo largo de los años y su eficiencia para manejar datos relacionales.
Ningún otro lenguaje informático ha permanecido tan popular y extendido su alcance durante 50 años. Como desarrolladores, pasamos nuestro tiempo aprendiendo nuevos lenguajes, variantes y conceptos para mantenernos actualizados. Nada permanece estático. SQL es una excepción magnífica y refrescante a este caos. Aprenderlo es probablemente adquirir una de las pocas competencias técnicas en las que podrás confiar para que sean útiles, relevantes y portables a largo plazo.
SQL es poderoso. Gracias a su sintaxis esencialmente declarativa, lo que se expresa en tres líneas de SQL puede requerir de 20 a 30 líneas en un lenguaje procedural. [Maffey 47]
SQL es casi siempre la mejor manera de evitar tener que aprender algo nuevo que, tarde o temprano, inevitablemente te ralentizará o simplemente no funcionará a largo plazo. [Righetti 50]
Es difícil competir con décadas de investigación y optimizaciones en bases de datos relacionales. [Kiehl 73]
El hecho de que los ORMs mapeen los datos en objetos es seductor cuando se quiere desarrollar con un enfoque "todo objeto". Pero hemos visto más arriba que este tipo de enfoque no es sostenible por sí mismo.
Sobre todo, al manipular solo objetos, los desarrolladores pierden de vista la materialidad de los datos subyacentes, con tres efectos negativos:
- El ORM no sabe cómo se utilizan los datos, recupera todos los campos de las tablas, incluso aquellos que no se necesitan, lo que lleva a un mayor consumo de memoria y más latencia en los intercambios de datos. Y si el ORM no conoce las relaciones entre las tablas, es fácil terminar haciendo múltiples consultas donde una sola consulta con varios joins sería suficiente.
-
En un código aplicativo, las entidades pueden manipularse sin conocimiento del esquema de la base. Un desarrollador puede
acceder a las propiedades de las entidades recuperadas sin darse cuenta de que cada acceso desencadena — de manera invisible —
consultas adicionales. Un código mal gestionado puede así generar decenas de consultas, simplemente porque los desarrolladores
confían en los objetos sin saber lo que sucede detrás.
Tu ORM es una pistola cargada apuntando a tu pie, esperando tranquilamente que alguien apriete el gatillo. - Al no conocer el esquema de la base y no saber escribir consultas SQL, se vuelve imposible probar adecuadamente un desarrollo verificando los datos registrados. De la misma manera, depurar datos se convierte en un verdadero calvario.
El rendimiento de los ORMs presenta serias limitaciones. Si funcionan bien en casos simples, tienen dificultades para optimizar sus
tratamientos cuando se necesita generar consultas complejas. Entonces, se termina haciendo múltiples consultas donde una sola sería suficiente.
La respuesta aportada a este problema es escribir consultas en un lenguaje cercano al SQL, haciendo perder el pretendido beneficio
de no necesitar conocer la sintaxis SQL ni el esquema de la base de datos subyacente.
Esto nos lleva naturalmente a otro problema de los ORMs: la ineficiencia. Cuando recuperas un objeto, ¿de qué propiedades (columnas de la tabla) necesitas? El ORM no puede saberlo, así que las recupera todas (o te pide que las especifiques, lo que rompe la abstracción). Al principio, esto no es un problema, pero cuando recuperas miles de registros a la vez, recuperar 30 columnas cuando solo necesitas 3 se convierte en una fuente perniciosa de ineficiencia. Muchos ORMs también son particularmente malos para deducir joins y a menudo se basan en decenas de consultas individuales para recuperar los objetos relacionados. Como se mencionó anteriormente, muchos ORMs reconocen su propia ineficiencia y algunos proponen mecanismos para ajustar las consultas problemáticas. Su falta de sensibilidad al contexto los obliga a depender de la caché u otros artificios para intentar compensar. [Voss 43]
Los ORMs fomentan malas prácticas, ya que es muy fácil apoyarse en la lógica del lenguaje anfitrión para combinar datos.
Los ORMs no son tan eficientes como las consultas SQL directas. A menudo son un poco menos eficientes, y en algunos casos, significativamente ineficientes.El primer problema es la sobrecarga masiva de cálculo que algunos ORMs generan al convertir consultas en objetos.
El segundo problema es que a menudo realizan múltiples idas y vueltas a la base de datos al basarse en relaciones uno-a-muchos o muchos-a-muchos. Esto se conoce como el problema N+1 (1 consulta principal + N subconsultas).Pero el peor problema es la falta de visibilidad. Como un ORM actúa como un generador de consultas, no juega el papel de separador de errores más allá de escenarios muy simples (como un tipo primitivo inválido). En todos los demás casos, el ORM debe interpretar el error SQL devuelto y traducirlo para el usuario. [Chuong 51]
Además, los ORMs no aprovechan funcionalidades básicas de las bases SQL, como vistas, procedimientos almacenados o triggers, que pueden mejorar el rendimiento de una base y la coherencia de sus datos.
Además, el uso de ORMs induce heterogeneidad en los diferentes accesos a los datos. Las entidades se manipulan de manera diferente según el ORM (con diferencias importantes según si utiliza el patrón de concepción Active Record o el Data Mapper). Si el código también debe acceder a otras fuentes de datos (a través de una base NoSQL o una API, por ejemplo), lo hará con una lógica y una sintaxis completamente diferentes.
En cuanto al argumento de la abstracción del SGBD, que permite cambiarlo fácilmente, es falaz. Los ORMs se basan en el mínimo denominador común. Cambiar de base de datos solo tiene sentido para utilizar funcionalidades específicas proporcionadas por un SGBD, y que los otros no soportarían; pero en este caso, es muy probable que el ORM no soporte estas especificidades de manera nativa, o al menos no completamente.
En términos prácticos, es extremadamente raro que una empresa cambie su base de datos, manteniéndose dentro de un ámbito funcional para el que un ORM es suficiente.
En toda mi carrera como desarrollador, nunca he visto a una empresa migrar de una base SQL a otra. [Maffey 49]
Y en el caso en que una plataforma necesitara pasar de una base relacional a una base NoSQL o a una fuente de datos por API, el conjunto del código basado en la presencia del ORM tendría que ser reescrito.
Un interfaz más simple con los datos puede ofrecer un acceso más unificado a los diferentes tipos de fuentes de datos.
Los query builders #
Ante las dificultades planteadas por los ORMs, algunos avanzan que los query builders son una solución elegante. Sirven para construir consultas únicamente con código.
Aquí tienes un ejemplo de uso de un query builder:
DB::table('Users')
->select('Groups.id')
->join('Groups', 'Groups.userId', 'Users.id')
->where('Users.created_at', '>', $date1)
->where('Groups.created_at', '<', $date2)
->get();
Y aquí está el equivalente sin query builder:
DB::select(
'SELECT Groups.id
FROM Users
INNER JOIN Groups ON (Groups.userId = Users.id)
WHERE Users.created_at > :date1
AND Groups.created_at < :date2',
['date1' => $date1, 'date2' => $date2]
);
Podemos ver que el query builder no es más legible ni comprensible que la consulta SQL, y que no hace más que intentar reproducir la semántica SQL con código.
La abstracción es extremadamente limitada, es necesario conocer SQL y tener una idea de la consulta final para usar correctamente un query builder. Y para consultas más complejas, hay pocas posibilidades de que el query builder permita tanta flexibilidad como el SQL nativo.
Me recuerdo a mí mismo escribiendo buenas consultas SQL y me sorprende cuánto tiempo me llevó. «¿Por qué?», te preguntarás tal vez. Bueno, principalmente por las siguientes razones:
[Righetti
- No se obtiene el 100% de la expresividad del SQL
- No funciona bien para consultas complejas, o pierde todo su propósito
52 ]
Repensar el modelo con la concepción orientada a datos #
El paradigma de la concepción orientada a datos (o Data Oriented Design − DOD) surgió en el contexto del desarrollo de juegos en C++. El objetivo era entonces casar el modelo de objeto tradicional utilizando estructuras de datos más eficientes, garantizando en particular un mejor uso de la caché del procesador.
Sus principios son recalcar el desarrollo en torno a los datos, que se convierten en el elemento más importante del código, y que son tratados por un flujo de ejecución claro y explícito.
El objetivo de todo programa, y de cada una de sus partes, es transformar datos de una forma a otra.
Si no comprendes los datos, no comprendes el problema.
Si no comprendes la solución del problema, no comprendes el problema.
Todo es un problema de datos. Incluyendo la ergonomía, el mantenimiento, la depuración, etc.Los tres grandes errores:
[Acton 53]
- La lógica es una pizarra en blanco.
- El código debe ser escrito en torno a modelos del mundo real.
- El código es más importante que los datos.
La concepción orientada a datos desplaza la perspectiva. Ya no programamos en torno a objetos, sino en torno a los datos mismos: su tipo, su disposición en memoria, y la manera en que serán leídos y tratados.
La programación, por definición, consiste en transformar datos: es el acto de crear una secuencia de instrucciones de máquina para tratar datos de entrada y producir un resultado de salida. [Llopis 54]
La programación DOD da prioridad a la organización eficiente de los datos, a la optimización del rendimiento y a la gestión de la carga. A diferencia del paradigma OOP tradicional, centrado en los objetos y sus comportamientos, la programación DOD se centra firmemente en los datos y su manipulación. Al centrarse en los datos, la programación DOD ofrece una perspectiva única que se alinea perfectamente con la naturaleza intensiva en datos de los proyectos modernos.
Los principios clave son los siguientes:
[Dang 55]
- Concepción centrada en los datos: Las aplicaciones están principalmente estructuradas en torno al modelo de dominio de datos, relaciones y esquemas de acceso, en lugar de centrarse en abstracciones o jerarquías orientadas a objetos.
- Flujo de datos explícito: El recorrido de los datos en el sistema es claramente visible en el código, a través de estructuras de datos particionadas y funciones sin estado. Los datos circulan entre componentes, en lugar de a través de llamadas de métodos enmarañadas.
- Acoplamiento flexible: Al priorizar desacoplamientos orientados a datos en lugar de entidades acopladas, el código DOD favorece naturalmente funciones y componentes fácilmente probados, comprendidos y paralelizados.
Esto difiere profundamente del enfoque estrictamente orientado a objetos. El código es sin estado, sirve para tratar y transformar los datos cuando sea necesario.
Dado que la concepción orientada a datos coloca los datos en primer plano, se puede estructurar el conjunto del programa en torno al formato de datos ideal. No siempre se alcanza este ideal, pero sigue siendo el objetivo principal. Y una vez alcanzado, la mayoría de los problemas tienden a desaparecer.
La concepción orientada a datos mejora tanto el rendimiento como la facilidad de desarrollo. Cuando se escribe código específicamente para transformar datos, se obtienen pequeñas funciones, con muy pocas dependencias de otras partes del código. El código se vuelve más plano, compuesto de numerosas funciones "feuilles", independientes unas de otras. Este nivel de modularidad y esta ausencia de dependencias hacen que el código sea mucho más fácil de comprender, reemplazar y hacer evolucionar. [Llopis 54]
Los enfoques OOP tradicionales tienden a modelar los conceptos o abstracciones del mundo real bajo la forma de "objetos" que encapsulan tanto los datos como el comportamiento. En contraste, el DOD separa las estructuras de datos de la lógica del métier y se centra en la organización y el tratamiento de los datos de manera optimizada para la caché. Este cambio de perspectiva conduce a programas DOD que privilegian:
[Dang 55]
- Estructuras de datos independientes en lugar de tipos de objetos agrupados.
- Funciones aisladas y sin estado en lugar de métodos.
- El paso de datos por valor (inmutables) en lugar de referencias de objetos.
- La eliminación de estados mutables entre componentes.
[Nikolov 56]
- Separar los datos de la lógica
- Los datos son vistos como información que debe ser transformada.
- La lógica envuelve los datos
- No intenta ocultarlos.
- Conduce a funciones que operan sobre tablas.
- Reorganizar los datos según su uso
- Si un dato no sirve para nada, ¿por qué conservarlo?
- Evitar los "estados ocultos"
Finalmente, este modelo puede extenderse a otros dominios además de los videojuegos, y en particular al desarrollo web.
Recuerda que el papel del modelo no es representar objetos, sino responder a preguntas. Proporciona una API que responde a las necesidades de tu aplicación de la manera más simple y eficiente posible. Sin embargo, estas respuestas serán extremadamente específicas, hasta el punto de parecer "mal concebidas" incluso a los ojos de un desarrollador orientado a objetos con experiencia; pero con experiencia, aprenderás a reconocer puntos comunes que te permitirán agrupar varios métodos de consulta en uno solo. [Voss 43]
Frameworks: De buenos servidores pero malos maestros #
¿Qué es un framework? #
Wikipedia proporciona una definición de los frameworks:
Los frameworks son ampliamente utilizados por su capacidad para aumentar la productividad de los desarrolladores, proporcionar modelos estructurados adaptados a aplicaciones a gran escala, simplificar la gestión de casos particulares y ofrecer herramientas de optimización del rendimiento. [Wikipedia 57]
Mozilla ofrece una definición más completa:
Los frameworks de servidor web son entornos de software diseñados para facilitar la escritura, mantenimiento y gestión de la carga de aplicaciones web. Proporcionan herramientas y bibliotecas que simplifican las tareas comunes del desarrollo web, como el enrutamiento de URL, el acceso a bases de datos, la gestión de sesiones y autorizaciones de usuarios, el formateo de respuestas (HTML, JSON, XML, etc.), y el fortalecimiento de la seguridad frente a ataques web. [Mozilla 58]
Para explicar la diferencia entre una biblioteca y un framework, a menudo se dice que el código de negocio llama a las bibliotecas, pero es el framework el que llama al código de negocio.
Un poco de historia #
La historia de los frameworks es larga y puede resumirse en tres grandes fases.
Los primeros frameworks aparecieron a mediados de los años 90 (ColdFusion en 1995, WebObjects en 1996, WebLogic en 1997,
WebSphere en 1998, Java EE en 1999). Se dirigían principalmente a grandes organizaciones como bancos y primeros sitios de
comercio electrónico (la licencia WebObjects costaba $50,000 antes del año 2000). Se caracterizaban por infraestructuras
complejas y la utilización generalizada del lenguaje Java.
Luego, el ecosistema Java EE se consolidó y se complejizó con componentes como Tomcat, Struts, Hibernate, JBoss, Spring…
En respuesta, surgieron frameworks más simples en los años 2000, basados en lenguajes de script menos estrictos, permitiendo
un desarrollo más rápido (Ruby on Rails en 2004; CakePHP, Django y luego Symfony en 2005; CodeIgniter y Zend Framework en 2006;
Temma en 2007).
En esa época, el entorno Java era presentado como costoso para las empresas debido a su complejidad, que alargaba los tiempos
de desarrollo, impulsado por empresas que tenían todo que ganar al crear tal ecosistema, ya sea vendiendo soluciones, formación
o tiempo de desarrollo.
En los años 2010, el mundo PHP buscó profesionalizarse a través de una "profesionalización" de los frameworks, inspirándose
cada vez más en el mundo Java.
Esta tendencia tuvo varios efectos:
- Una estandarización de las prácticas, llevando a proyectos más homogéneos y a una disminución drástica de los proyectos desarrollados "sin importar cómo".
- Una complejización de lo que se presentaba como el desarrollo "normal" en PHP, con una curva de aprendizaje empinada.
Al competir con las tecnologías Java, el campo PHP dejó espacio libre para Python y Node.js en desarrollos ágiles y rápidos, que se convirtieron en las nuevas plataformas "cool".
En los años 2000, se consideraba que el mundo Java debía ser respaldado por empresas, y que la complejidad y las evoluciones
permanentes le permitían vender prestaciones de servicio y formación.
Se puede observar que los principales actores del mundo PHP también son respaldados por empresas, que tienen sus propios objetivos.
¿Deberíamos replantearnos los frameworks? #
Como todas las buenas prácticas informáticas, es bueno cuestionarse si una recomendación hecha por un grupo de personas es adecuada para ti; una práctica que resuelve problemas que no tienes puede causarte más problemas de los que resuelve. Si un framework es utilizado por cientos de personas, ¿es realmente el adecuado para tu equipo de 12 o 80 personas?
Diferentes problemas requieren soluciones diferentes.
Resolver problemas que no tienes puede causarte más problemas de los que resuelve. [Acton 53]
Es esencial recordar que los creadores de frameworks tienen sus propias prioridades. Resuelven SUS problemas, no los tuyos. Los gigantes de la tecnología han marcado el camino creando, y luego liberando bajo licencia libre, herramientas que usamos hoy en día. Pero estas herramientas no fueron concebidas para ser universales. Responden a necesidades muy específicas que la mayoría de las empresas nunca encontrarán. [Bobrov 60]
Porque si los frameworks se han vuelto imprescindibles en el mundo PHP, algunas voces comienzan a cuestionar el daño causado por los grandes frameworks complejos. Ya sea el ciclo de desarrollo, la dificultad de mantenerse al día, o el ritmo rápido de actualizaciones que imponen cambios constantes en el código, parece sano revisar los hábitos.
Los frameworks proporcionan herramientas y patrones eficientes que simplifican muchas tareas de desarrollo. Utilizados tanto por startups como por gigantes tecnológicos, son ampliamente elogiados por su capacidad para racionalizar los flujos de trabajo y estandarizar las soluciones.
Sin embargo, a medida que su uso se generaliza, las inquietudes sobre su uso excesivo y los inconvenientes potenciales para los desarrolladores y el software se vuelven evidentes. ¿Los frameworks son una solución milagrosa o un freno silencioso a la creatividad y el progreso técnico?Un desarrollador que trabaja exclusivamente con frameworks corre el riesgo de carecer de una comprensión más profunda de los lenguajes de programación y de los principios básicos para resolver problemas más complejos.
Los frameworks imponen convenciones y patrones estrictos. Esto es muy eficiente para reducir la complejidad, pero también puede limitar la creatividad. Las reglas del framework a menudo terminan restringiendo a los desarrolladores y limitando su capacidad para innovar.
La gran variedad de herramientas disponibles a menudo constituye un desafío para los desarrolladores que intentan mantenerse al día con las últimas herramientas, agotándolos con la necesidad constante de adaptarse. [.NET Expert blog 59]
¿Los frameworks son realmente una solución milagrosa o una trampa silenciosa para la creatividad y el progreso técnico? Todo un ecosistema empuja a los desarrolladores a adoptarlos, y se dan cuenta, después de haber dado el salto, de que están atrapados en plataformas costosas. Esto se parece un poco a una trampa, ¿no?
Recientemente, una forma de rebelión ha comenzado a surgir, ya que la gente empieza a estar harta de los frameworks. Los desarrolladores están cansados de los cambios constantes. Lo has visto: con cada actualización mayor, te ves obligado a reescribir una gran parte de tu código para mantenerte en la carrera. Y ni siquiera te hablo del ciclo interminable de problemas de compatibilidad.
Esta frustración ha llevado a un resurgimiento del interés por pilas más simples y estables entre los desarrolladores que privilegian la productividad sobre estar a la vanguardia de la tecnología. Sí, puede parecer un poco "viejo juego", pero está lejos de ser obsoleto. Con una pila más simple, puedes iterar rápidamente y entregar aún más rápido. A veces, no necesitas lo que está de moda. A veces, ceñirte a lo que funciona puede ahorrarte muchos dolores de cabeza. [Bobrov 60]
Evita a toda costa usar un framework, porque terminará causando más problemas de los que resuelve.
Si dependes demasiado de un framework, corres el riesgo de perder la oportunidad de aprender el lenguaje subyacente. Con un framework, solo interactúas con los niveles superiores del sistema y tienes menos posibilidades de resolver problemas complejos.
Cada framework incluye una multitud de herramientas, bibliotecas y funcionalidades para cubrir un amplio abanico de casos de uso, pero que son inútiles para tu proyecto. Cuando desarrollas aplicaciones web simples, este código inútil perjudica el rendimiento global. [TechAffinity blog 61]
Una dependencia excesiva de bibliotecas y frameworks puede llevar a una falta de comprensión del código y de los principios subyacentes. Esto limita la flexibilidad y complica la adaptación del código a las necesidades específicas de un proyecto.
Utilizar demasiadas bibliotecas o frameworks complejos como base de código puede hacer que el desarrollo sea más difícil de mantener y depurar con el tiempo. Aunque estas herramientas pueden ofrecer soluciones rápidas, en algunos casos, las soluciones personalizadas pueden ser más eficientes y adaptadas a las necesidades específicas del proyecto.
Las bibliotecas y frameworks pueden volverse obsoletos con el tiempo, lo que genera problemas de compatibilidad y la necesidad de reescribir una gran parte del código para mantenerse al día con la evolución de las tecnologías. [Mishra 62]
Cada framework impone su propia manera de hacer las cosas. Para poder mantener un proyecto, un desarrollador PHP no es suficiente, se necesita obligatoriamente un desarrollador Laravel/Symfony/CodeIgniter… Y hasta un desarrollador experimentado puede no conocer todas las funcionalidades asociadas de cerca o de lejos con un framework, y tener dificultades para comprender una base de código.
Porque los frameworks añaden abstracciones que enmascaran las funcionalidades nativas del lenguaje de programación. Una primera funcionalidad
ahorra tiempo en desarrollos simples; luego, una segunda funcionalidad se apoya en la primera, y así sucesivamente hasta que el conjunto se
convierte en un enredo complejo de dependencias.
Un día, te das cuenta de que la primera funcionalidad es inútil porque has superado su caso de uso óptimo; y sin embargo, estás obligado a
mantener la integridad de la pila tecnológica, porque se ha infiltrado en todos los aspectos del desarrollo y tiene ramificaciones en todo el código.
En realidad, mis antiguos proyectos CodeIgniter son hoy en día tan dependientes del framework que se ha vuelto casi imposible añadir cualquier cosa nueva... ¡incluso cosas tan simples como tests! Por lo tanto, si algún día quiero (o debo) deshacerme de él, será complicado. Y el problema no termina ahí; si, por ejemplo, el motor de plantillas por defecto del framework ya no conviene al proyecto, deshacerse de él no tiene nada de evidente.
Cuando eliges un framework full-stack, también eliges atar tu proyecto a ese framework. Sí, teóricamente puedes construir un proyecto desacoplado del framework, pero claramente no es la vía más directa ni la más fácil. [Junior 63]
¿Y los micro-frameworks? #
Los micro-frameworks pueden parecer una alternativa seductora a los grandes frameworks. Pero si su extrema simplicidad es fácil de entender, tiene dificultades para superar la prueba del fuego de los proyectos reales. La mezcla de rutas con el código tiene dificultades para escalar, y la adición de funcionalidades necesarias puede volverse abrumadoramente complicada.
Bueno, para aplicaciones muy, muy pequeñas, sin conexión a una base de datos, sin consumo de APIs y fundamentalmente sin más que unas pocas líneas de código para cada ruta (o prototipado con datos ficticios), los micro-frameworks serán realmente más fáciles de usar y no necesitarán toneladas de archivos de configuración y otros servicios en curso de ejecución como exigen ciertos frameworks full-stack. [Junior 63]
Debido a la naturaleza más declarativa de los microframeworks y a la correspondencia generalmente 1:1 de una ruta a un controlador, los microframeworks no incitan a la reutilización de código. Esto se debe a la manera en que las aplicaciones basadas en un microframework están organizadas: en general, no hay convenciones claras sobre cómo organizar las rutas y los controladores, y mucho menos separarlos en varios archivos. Esto puede llevar a problemas de mantenimiento a medida que la aplicación crece, así como problemas lógicos cada vez que necesitas añadir nuevas rutas y controladores. [O’Phinney 64]
Me encanta la sintaxis y el estilo del framework Echo de Labstack para Golang. Pero mi experiencia cambió cuando añadí una base de datos a mi aplicación. La simplicidad se vio comprometida porque la adición de una variable global dificultaba las pruebas. Sin estado, no tengo este problema. Hay muchos microframeworks donde esto ocurre. Puedes casi preverlo si miras el índice de la documentación y ves que no hay nada sobre la gestión de bases de datos. [Dillon 65]
Existen numerosos microframeworks en todos los lenguajes, pero todo comenzó con Sinatra. Los compromisos de Sinatra no saltan a la vista en un simple Hello World. A mis ojos, la promesa de simplicidad seduce sobre todo a los desarrolladores jóvenes justamente porque aún no perciben los compromisos que implica. La superficie de la API es reducida, lo que facilita el aprendizaje. Pero las cosas se complican rápidamente después. Tan pronto como te sales del caso más básico, surgen muchas preguntas urgentes e incertidumbres.
A medida que el proyecto crece, o simplemente al continuar trabajando con él, te encuentras teniendo que corregir, buscar o arreglar Sinatra por ti mismo. Más de una vez, he copiado archivos enteros desde un proyecto Rails por defecto. Cosas como dónde colocar los archivos de configuración, las fixtures de prueba, o el concepto de dev/test/prod. [Dillon 66]
La pereza de los micro-frameworks es particularmente discutible. En el mundo PHP, se puede observar que Silex y Lumen han desaparecido (en favor de los frameworks full-featured correspondientes).
¿Los micro-frameworks se multiplican porque hay menos cosas que desechar o porque son fáciles de inventar? Es mucho más simple lanzar un micro-framework amateur, con un perímetro reducido. Podríamos casi permitirnos ignorar los micro-frameworks porque serán los primeros en desaparecer. [Dillon 66]
La buena aproximación #
Hoy en día parece imposible retroceder y hacer sitios sin frameworks. Cuando se usan bien, reducen los tiempos de desarrollo.
Pero es necesario seleccionar un framework en función de las necesidades reales, no de los efectos de moda. Es necesario poner el cursor en el lugar correcto, eligiendo una herramienta que ofrezca un marco de desarrollo, al mismo tiempo que permite escribir código personalizado.
Un framework debe ser una ayuda y no una restricción.
Aunque los frameworks son excelentes para acelerar el desarrollo, a veces el código personalizado es la mejor solución. Utilice los frameworks para sus puntos fuertes (como las plantillas, el enrutamiento y las funcionalidades básicas), pero tenga la audacia y el coraje de salir de sus límites. Al combinar el dominio de un framework con un código a medida, podrá responder eficazmente a necesidades específicas. [.NET Expert blog 59]
No olvide que los frameworks deben servir a SU arquitectura, y no dictarla. Gracias a una planificación minuciosa y a una abstracción estratégica, puede recoger las ventajas de los frameworks sin dejarse atrapar por dependencias a largo plazo. Todo se trata de mantener el control. Así que la próxima vez que esté a punto de sumergirse en un framework, tómese un momento y recuerde: usted decide. [Bobrov 60]
Si considera que una dependencia excesiva de bibliotecas y frameworks es un problema, considere otras enfoques como el desarrollo de soluciones a medida, el uso de herramientas simples, el dominio de tecnologías básicas, o combinar componentes existentes con código personalizado. [Mishra 62]
El problema es que muchos principiantes se acostumbran a un framework y tienden a usarlo para todo. Si es un principiante, le recomiendo construir cosas, muchas cosas, sin ningún framework. Haga varios proyectos "de juguete", luego pruebe varios frameworks diferentes, y entonces estará en una mejor posición para hacer mejores elecciones. [Junior 63]
Pruebas automatizadas: unitarias, de integración, funcionales — encuentre el equilibrio adecuado #
Definiciones rápidas #
En el desarrollo informático, cuando hablamos de pruebas automatizadas, nos referimos a código que prueba otro código. Se distinguen tres grandes tipos de pruebas automatizadas:
- Las pruebas unitarias sirven para probar individualmente los objetos de negocio. Para cada método público de cada objeto, diferentes escenarios envían datos (correctos o incorrectos) como entrada y verifican que el retorno corresponde a la salida esperada. Los objetos se prueban de manera aislada, por lo que si necesitan otros objetos, estos suelen ser simulados (reemplazados por objetos falsos cuyo comportamiento se controla).
- Las pruebas de integración permiten probar las interacciones entre varios objetos. Esto significa que los objetos van a comunicarse entre sí. Solo las dependencias externas permanecen simuladas.
- Las pruebas funcionales (también llamadas pruebas end-to-end) prueban la plataforma en su conjunto, simulando los accesos de un usuario (en un sitio web) o las conexiones a una API.
Las pruebas automatizadas son una gran ayuda en el desarrollo ágil. Se puede modificar el código fuente sin temor a romper una dependencia. Basta con ejecutar las pruebas para darse cuenta de una posible regresión del código y corregirla lo antes posible.
El objetivo de las pruebas es producir información sobre su programa. (Las pruebas no mejoran la calidad por sí solas; son el diseño y el código los que lo hacen. Las pruebas simplemente proporcionan los comentarios que el equipo necesita para diseñar e implementar correctamente.) [Coplien 67]
Cómo es en realidad #
Habitualmente, se considera que las pruebas unitarias son más rápidas de escribir y ejecutar que las pruebas funcionales [Modus Create blog 68]. En consecuencia, se recomienda siempre probar unitariamente cada parte del código [Twilio blog 69]. La consecuencia es que se supone que es preferible escribir muchas más pruebas unitarias que pruebas funcionales [Modus Create blog 68].
Esto es cierto en teoría, pero la práctica puede ser muy diferente.
Para probar un objeto unitariamente, a menudo es posible escribir muchos tests, que verificarán su comportamiento en todos los casos posibles e imaginables. Puede volverse difícil saber cuándo parar, definir el límite más allá del cual los tests se vuelven excesivos.
Se habla del índice de cobertura de los tests, pero es una medida difícil de determinar. Algunas personas piensan que por debajo del 90% de código probado, no sirve de nada hacer tests; pero sabemos bien que es mejor tener un mínimo de tests que no tener ninguno. Y aún un índice del 100% no significa mucho; puede significar que todos los métodos han sido probados en su caso nominal, pero ¿sus casos de error también han sido verificados?
Pocos desarrolladores admiten que solo hacen tests parciales o aleatorios. Muchos te dirán que hacen tests completos, según una idea más o menos vaga de lo que eso significa. Por ejemplo: «Cada línea de código ha sido verificada», lo cual, desde el punto de vista de la teoría de la informática, es un sinsentido total si se quiere saber si el código hace lo que debe hacer.
Los programadores creen tácitamente que pueden pensar más claramente (o adivinar mejor) escribiendo tests que escribiendo código, o que hay más información en un test que en el código. Esto es un sinsentido formal. [Coplien 67]
La cobertura del código no tiene absolutamente nada que ver con la calidad del código (en muchos casos, es inversamente proporcional). [Kiehl 73]
El uso de mocks también es muy extendido, ya que a menudo es necesario para poder realizar pruebas unitarias. Pero estos objetos falsos a veces hacen que los tests sean más largos de escribir, más difíciles de mantener, con resultados que no son tan fiables.
Al escribir tests, puede parecer más simple ignorar las dependencias mockeándolas. Pero a veces, no usar mocks permite escribir tests más simples y útiles.
El uso excesivo de mocks puede llevar a varios problemas: los tests pueden ser más difíciles de entender. Los tests pueden ser más difíciles de mantener. Los tests pueden ofrecer menos seguridad de que su código funcione correctamente.
Si, al leer un test con mocks, debe reproducir mentalmente el código probado para entender lo que hace, es probable que esté usando demasiados mocks. [Trenk 70]
Por otro lado, una prueba de integración o una prueba funcional validará por sí sola varias capas de código.
Si tal prueba falla, se puede entonces centrar en los objetos por los que pasó la ejecución; con buenos
informes de registro, el error es rápido de identificar y, por lo tanto, de corregir.
Esto puede parecer menos riguroso que tener todos los objetos probados unitariamente. Pero es más eficiente, ya que se puede considerar
que el tiempo ganado (al no escribir todos los tests unitarios posibles) será superior al tiempo perdido (al buscar el origen preciso de un error).
Acerca del Desarrollo Guiado por Pruebas (TDD) #
La práctica del Desarrollo Guiado por Pruebas (TDD) permite integrar completamente la escritura de pruebas unitarias en el desarrollo, comenzando por escribir las pruebas antes de producir el código. Esto las hace menos dolorosas de escribir que cuando se hacen al final del proceso, y participan de alguna manera en la especificación técnica del desarrollo: al definir los criterios de aceptación de un objeto o un método, se determina claramente su comportamiento esperado; este es el papel de una especificación técnica.
Una de las ventajas ocultas del TDD es que, al tener una especificación técnica extremadamente cartesiana, la fase de desarrollo es más corta, ya que se sabe a dónde se debe llegar. Hay menos errores y tanteos para llevar a cabo el desarrollo.
Sin embargo, el TDD no es una panacea absoluta. No se aplica a código existente, y se limita a pruebas unitarias
que — como se vio anteriormente — no son las únicas pruebas que se deben implementar.
Y hay situaciones en las que el TDD no se aplica, especialmente cuando no hay una especificación lo suficientemente
precisa, y el desarrollo juega un papel exploratorio.
El fundamentalismo del “probar primero” es como la educación sexual basada en la abstinencia: Una campaña de moralización irrealista e ineficaz, que alimenta la culpabilidad y el desagrado por uno mismo. El fanatismo actual en torno al TDD lleva a concentrarse únicamente en las pruebas unitarias. No creo que sea saludable. Este enfoque de “probar primero” lleva a un enredo de objetos intermedios e indirecciones inútiles. [DHH 71]
¿Qué estrategia aplicar? #
Al final, como siempre, es necesario implementar una estrategia inteligente, sin fundamentalismos, que aproveche todas las herramientas disponibles:
- Realizar pruebas unitarias en las capas más fundamentales e importantes del código, para asegurar su estabilidad.
- Realizar pruebas de integración y pruebas funcionales para validar el conjunto del código aplicativo, sin riesgo de tener zonas de sombra que pasen desapercibidas.
Las pruebas automatizadas deben mantenerse actualizadas a lo largo de las modificaciones del código. Este mantenimiento tiene un costo.
Interiorizar las ventajas de las pruebas es solo el primer paso hacia la iluminación. Saber qué no se debe probar es lo más difícil.
Las pruebas no son gratuitas. Cada línea de código que escribes tiene un costo. Se necesita tiempo para escribirla, tiempo para actualizarla y tiempo para leerla y comprenderla. Por lo tanto, el beneficio obtenido debe ser superior al costo de realización. En el caso de pruebas excesivas, por definición, no es así. [DHH 72]
Las pruebas de bajo riesgo tienen consecuencias menores (o potencialmente negativas). [Coplien 67]
En una base de código, no todos los objetos evolucionan al mismo ritmo. Algunos cambian muy poco, y sus pruebas unitarias serán
fácilmente rentabilizadas. Estos son a menudo las capas más estables del código, aquellas de las que justamente hay que asegurarse
de que las regresiones sean detectadas lo más rápido posible.
Para los objetos que evolucionan rápidamente, mantener una cobertura de pruebas del 100% puede volverse muy costoso. Esto puede
aplicarse a los objetos más cercanos al renderizado del usuario, por ejemplo. En este caso, se hará un mínimo de pruebas unitarias,
y se complementará con pruebas de integración que permitirán cubrir los errores siendo menos costosas de mantener.
Sin olvidar que, en un mundo ideal, las pruebas automatizadas deben ser complementadas con pruebas humanas, realizadas por personas diferentes a las que hacen los desarrollos, para identificar bugs en los que los desarrolladores no pensarían.
Microservicios: Es muy poco probable que los necesite #
La razón de ser de las arquitecturas de microservicios #
Las arquitecturas de microservicios surgieron en respuesta a dos problemáticas muy específicas: la escalabilidad y el desarrollo colaborativo a gran escala.
- Para la escalabilidad, cuando un servidor alcanzaba rápidamente sus límites, se hacía necesario escalar horizontalmente, replanteando las aplicaciones de manera que fueran distribuibles y redundantes en varios servidores.
- Para el desarrollo colaborativo, era necesario que equipos de cientos de desarrolladores pudieran hacer evolucionar una aplicación sin interferir entre sí, y sin que una modificación en un lugar pudiera causar errores de regresión en otro.
La solución fue, por lo tanto, dividir finamente las aplicaciones, haciendo que sus componentes fueran lo más autónomos posible entre sí. Así, los equipos pueden encargarse del desarrollo de cada parte por separado; y los recursos del servidor pueden asignarse a cada componente sin afectar a los demás.
Funciona muy bien, y de hecho, en algunos casos, es absolutamente obligatorio.
La arquitectura orientada a microservicios consiste en dividir una aplicación en una multitud de pequeñas partes, ejecutar cada una de ellas como una aplicación autónoma, y luego dejar que esta constelación resuelva el gran problema que se busca abordar.
Es un modelo excelente. Si eres Amazon, Google o cualquier otra organización de software con miles de desarrolladores, es una excelente manera de paralelizar las posibilidades de mejora. A partir de cierta escala, simplemente no hay otra manera razonable de coordinar los esfuerzos. [DHH 74]
Los inconvenientes de los microservicios #
El problema es que — como siempre — algunas personas consideran que si esta práctica es buena en ciertas condiciones, lo es en todas las condiciones. Y el desarrollo monolítico es visto entonces como anticuado.
Esto es, por supuesto, un error. Desacoplar una arquitectura en varios componentes distintos no tiene sentido para una pequeña aplicación:
- Toma más tiempo desarrollar. En una aplicación monolítica, los objetos se conocen y se comunican directamente, sin necesidad de implementar capas de abstracción e interfaz.
- Es mucho más complicado probar y depurar. Cada componente es fácil de probar de manera unitaria, por supuesto. Pero cuando quieres probar todos los componentes juntos, verás aparecer errores aleatorios, ya sea porque hay interacciones que no se anticiparon, o porque es la comunicación entre los componentes lo que causa los errores.
- Es más exigente en recursos. Donde una aplicación monolítica funcionará eficientemente en un pequeño servidor, una arquitectura de microservicios requerirá más potencia y costará mucho más.
El problema, cuando se transforma demasiado pronto una aplicación en una constelación de servicios, es que se viola la regla número 1 de la informática distribuida: ¡No distribuyas tu informática! Al menos, si puedes evitarlo.
Cada vez que transformas una colaboración entre objetos en una colaboración entre sistemas, te expones a un mundo de dolor con una miríada de responsabilidades y estados de fallo. ¿Qué hacer cuando los servicios fallan, cómo migrar en conjunto, y todas las dificultades relacionadas con la operación de muchos servicios en primer lugar. [DHH 74]
Porque las ilusiones de la informática distribuida son bien conocidas desde los años 90 [Wikipedia 75]:
- La red es confiable.
- La latencia es nula.
- El ancho de banda es infinito.
- La red es segura.
- La topología de la red no cambia.
- Hay un solo administrador de red.
- El costo de transporte es nulo.
- La red es homogénea.
Por lo tanto, a menos que esté en uno de los casos muy particulares para los cuales una arquitectura de microservicios es útil, manténgase en una arquitectura monolítica. Es probado, es confiable, es mantenible y es escalable.
El monolítico sigue siendo bastante bueno. [Kiehl 73]
La gran mayoría de las aplicaciones web deberían comenzar como un monolito majestuoso: una única base de código que se encarga de todo lo que la aplicación debe hacer. Esto es lo opuesto a una constelación de servicios, ya sean micro o macro, que fragmentan la aplicación en una serie de pequeños islotes, cada uno responsable de una parte del todo. [DHH 76]
Debe ser grande para usar microservicios.
A los desarrolladores les gusta trabajar con pequeñas unidades, esperando una mejor modularidad que con un monolito. Pero como con cualquier decisión arquitectónica, hay compromisos que hacer. Los microservicios tienen graves consecuencias para las operaciones, que ahora deben gestionar un ecosistema de pequeños servicios en lugar de un monolito único y bien definido. En otras palabras, si no dominas ciertas habilidades básicas, ni siquiera consideres adoptar una arquitectura basada en microservicios. [Fowler 77]
El equipo de Prime Video de Amazon publicó un caso de estudio bastante notable sobre su decisión de abandonar su arquitectura de microservicios sin servidor, y reemplazarla por un monolito. Esta decisión resultó en un ahorro espectacular del 90% (!!) en los costos operativos, y simplificó su sistema. ¡Qué victoria! |DHH 78]
Más allá del monolito #
Si una parte de su aplicación se vuelve tan compleja que su peso afecta a toda la aplicación, no es una razón para cambiar a una arquitectura de microservicios. Simplemente extraiga esa parte para gestionarla por separado, pero sin alterar el resto del monolito.
Esto es lo que David Heinemeier Hansson llama “la ciudadela”:
El siguiente paso es la Ciudadela, que mantiene el monolito majestuoso en el centro, pero lo apoya con un conjunto de avanzadas, cada una encargada de un pequeño subconjunto de responsabilidades de la aplicación. Estas avanzadas permiten al monolito majestuoso delegar ciertos comportamientos divergentes, ya sea por razones organizativas, de rendimiento o de implementación.
No hemos intentado dividir toda la aplicación en microservicios escritos cada uno en un lenguaje diferente. No, solo hemos extraído una única avanzada. Eso es una arquitectura Ciudadela.
A medida que más personas se dan cuenta de que la carrera hacia los microservicios ha terminado en un callejón sin salida, el péndulo oscilará en la otra dirección. El monolito majestuoso sigue ahí, esperando a los refugiados de los microservicios. Y la Ciudadela está ahí para ofrecerles una verdadera tranquilidad: el modelo sabrá evolucionar si, algún día, su aplicación necesita mega-escalabilidad. [DHH 76]
APIs: Cuestione las costumbres #
Existen varias maneras de crear APIs. Pero, una vez más, los impulsos sucesivos han llevado a una estandarización de una complejidad innecesaria. Es necesario hacerse algunas preguntas para crear APIs más fáciles de desarrollar y mantener.
La simplicidad de un webhook cuando es posible #
Ya sea que hablemos de un webhook o de una API, se trata de hacer una solicitud HTTPS a una URL, enviando datos y recibiendo otros. La diferencia entre ambos es más filosófica que técnica.
Los webhooks suelen presentarse como parte de un subconjunto de APIs (o incluso como "sub-APIs") que solo se utilizan de manera eventual.
Originalmente, el término designaba una URL que se proporciona a un sistema remoto, y que este llamará para notificar que ha ocurrido un evento. Esto es más eficiente que conectarse regularmente a una API para preguntar si el evento en cuestión ha ocurrido.
Desde entonces, el término se ha ampliado para designar simples URLs a las que se pueden enviar datos en POST
(más raramente en GET), y recibir datos en respuesta, generalmente en formato JSON.
Un webhook es entonces una URL única que contiene todo lo necesario para ser utilizada directamente;
sin mecanismos complicados de autenticación, pocas opciones.
El ejemplo típico es la mensajería empresarial que ofrece una API completa que da acceso a todas sus funcionalidades, pero también webhooks que permiten enviar fácilmente mensajes en salas de chat.
Para simplificar, no cree una API cuando un webhook es suficiente. Esto complicaría innecesariamente las cosas, tanto el desarrollo de su parte como el de los clientes que se conectan a su sistema.
Muchos servicios ofrecen webhooks, solos o en complemento a una API REST: Slack, Discord, Twilio, Stripe, PayPal, GitHub, GitLab, IFTTT, GoCardless…
Actuar como un procesamiento de formulario para datos simples #
Cuando se deben tratar datos entrantes (ya sea a través de un webhook o una API), se tiene la costumbre de serializarlos en un formato estándar, generalmente JSON. Y esto funciona bastante bien.
Cuando los datos entrantes no presentan estructuras complejas, es aún más sencillo recibirlos como si hubieran sido enviados por un formulario HTML:
- Todos los lenguajes de programación permiten enviar datos como parámetros POST, incluso más fácilmente que enviar una carga útil JSON.
- La recepción de los datos se realiza sin ningún proceso de deserialización.
- En el caso de un webhook, es posible probarlo con solo una página que contenga un formulario, permitiendo ingresar los valores manualmente y probar el retorno.
Varios servicios reciben datos a través de parámetros GET o POST, para una parte o la totalidad de su API: Twilio, Vonage, OpenWeatherMap, Google Maps Static, Pingdom…
Realizar llamadas a procedimientos remotos en lugar de REST #
En la historia de la informática distribuida, la arquitectura REST es más reciente que el enfoque RPC.
El enfoque RPC consiste en llamar a métodos pertenecientes a objetos remotos, proporcionándoles parámetros,
y que envían un resultado de vuelta.
La filosofía REST se basa en la noción de recursos, que son manipulados a través de un número limitado de operaciones,
las cuales se materializan a través de los métodos HTTP GET (lectura), POST (creación), PUT (reemplazo), PATCH (modificación)
y DELETE (borrado). Es decir, operaciones CRUD básicas.
Hay un gran número de casos en los que el enfoque REST funciona maravillosamente bien. El problema, una vez más, es que se ha convertido en una buena práctica tan generalizada que algunas personas piensan que si te desvías del credo REST, significa que no sabes hacer una "API real".
Tomemos un ejemplo concreto. Imaginemos un sistema de discusión.
Si deseas recuperar la lista de salas de discusión, REST funcionará muy bien. Te conectarás a la URL:
GET /api/channels
Para recuperar los mensajes de un salón (cuyo identificador es 123):
GET /api/channels/123/messages
Ahora, para suscribir a un usuario (identificador 789) al salón, podríamos imaginar hacer la siguiente llamada:
POST /api/channels/123/users/789
Cuando vemos esta solicitud POST, podríamos pensar que solo agregará una conexión entre el usuario y el salón, y nada más. No necesariamente imaginamos que se cambiarán estados o que se enviarán notificaciones por correo electrónico.
Sin embargo, en una aplicación cliente, es muy probable que escribamos algo como esto:
$channelManager->subscribeUser($channelId, $userId);
Cuando vemos esta línea de código, pensamos que pueden suceder varias cosas. No estamos manipulando un recurso, estamos solicitando que se realice una acción. Y eso lo cambia todo.
Y por lo tanto, no nos haríamos más preguntas si la URL fuera esta:
/api/channels/subscribeUser/123/789
En general, no hay razón para que las llamadas a las APIs funcionen fundamentalmente diferente de las que hacemos dentro de nuestro código. Que le digamos a un objeto local que haga algo, o que se lo digamos a un objeto remoto, no debería cambiar gran cosa.
El REST limita la expresividad del código. ¿Podríamos imaginar hacer ingeniería de software con CRUD como único vocabulario?
Hoy en día, cada vez más APIs se alejan del modelo REST para ofrecer un funcionamiento mediante llamadas a objetos remotos. Algunos ejemplos: Telegram Bot API, Slack API, MetaWeblog API, Google Cloud gRPC API, Bitcoin API…
Autenticación: diga sí a HTTP Basic, no a JWT #
Para dar acceso a una API, es necesario autenticar al usuario que se conecta. Para ello, existen varios medios, pero la tendencia actual es el uso de tokens JWT.
Los JWT son tokens criptográficos, es decir, pueden contener información que no puede ser modificada (de lo contrario, el token ya no será válido). La idea parece seductora: el cliente envía su identificador y contraseña (o sus claves pública y privada), y recibe a cambio un token que contiene sus derechos de acceso. Luego, el token se envía en todas las solicitudes a la API; el servidor no necesita verificar los derechos del usuario, puede confiar en los datos incluidos en el token.
Sin embargo, en la vida real, no es tan simple. Si se retiran los derechos de acceso de un usuario, no se desea que pueda
seguir utilizando la API; y sin embargo, mientras el token sea válido, el usuario será aceptado.
La solución suele ser reducir la vida útil del token, para forzar su regeneración frecuente. Pero en este caso, la más mínima
solicitud a la API se encuentra realizando tres conexiones:
-
El cliente intenta usar la API, proporcionando el token que tiene en caché.
→ La API responde que el token ha caducado. -
El cliente solicita un nuevo token, enviando su identificador y contraseña.
→ La API responde proporcionando un nuevo token. -
El cliente intenta nuevamente usar la API, proporcionando el token que acaba de obtener.
→ La API realiza su procesamiento y devuelve el resultado.
Para evitar esta complejidad innecesaria, es posible basar la autenticación de una API en el mecanismo HTTP Basic. Es una técnica muy simple, soportada por absolutamente todos los clientes HTTP, que implica enviar las credenciales de autenticación en todas las solicitudes.
Esta manera de hacerlo tiene mala reputación. Se considera menos segura, ya que las credenciales circulan en cada solicitud, lo cual era efectivamente un problema en la época en que el cifrado SSL/TLS no estaba extendido. Sin embargo, hoy en día, no hay excusa para no tener un certificado SSL (gracias a Let’s Encrypt). Por lo tanto, no hay problema en hacer circular las claves de conexión, ya que ninguna persona externa podrá leerlas.
Por otro lado, la cinemática se vuelve mucho más simple:
-
El cliente se conecta a la API, proporcionando sus credenciales.
→ La API realiza su procesamiento y devuelve el resultado.
Un gran número de servicios utilizan la autenticación HTTP Basic, ya sea sola o en complemento a tokens: Azure API, Twilio, Stripe, GitHub, IBM MQ, IBM App Connect…
Dual-Purpose Endpoints #
También llamada "content negociation" o "API mode switch", esta técnica busca evitar desarrollar APIs desde cero cuando no es necesario. Esto puede ser útil para proporcionar datos a una aplicación móvil cuando ya se tiene un sitio web funcional, por ejemplo.
Una parte o la totalidad de las páginas del sitio pueden devolver un flujo JSON en lugar del flujo HTML habitual. Varios criterios pueden ser utilizados para hacer la distinción:
- Un encabezado HTTP Accept que vale application/json en lugar de text/html.
- Un encabezado HTTP X-Requested-With que vale XMLHttpRequest.
- Un prefijo /api/ añadido al inicio de la URL.
- Un parámetro GET ?api=1 o ?format=json.
Dependiendo de cómo esté desarrollado el sitio, esto puede ser muy fácil de manejar, con un plugin/middleware/hook que hace que se utilice una vista JSON en lugar del motor de plantillas que genera el HTML normalmente. Todos los datos proporcionados normalmente a la plantilla se serializan entonces en JSON.
Las llamadas a la API que están reservadas para usuarios autenticados se benefician naturalmente de la gestión de derechos de usuario del sitio web. La autenticación puede hacerse de dos maneras posibles:
- Cada URL puede recibir las claves de autenticación (en HTTP Basic, ver más arriba), y así proceder a la autenticación, la verificación de derechos y el procesamiento aplicativo en la misma solicitud.
- O pasar por el proceso de autenticación web (pero llamado en modo API), proporcionando el par identificador/contraseña, y recuperando la cookie de sesión que se utilizará como token de autenticación para las solicitudes siguientes.
Esta técnica es más utilizada de lo que parece: Jekyll/GitHub Pages, Discourse, MediaWiki/Wikipedia, IBM UrbanCode Release…
Seguridad: Los fundamentos no negociables #
La seguridad en el desarrollo web no debe tomarse a la ligera. Existen dos categorías de vulnerabilidades de seguridad:
- aquellas que hacen vulnerables a los servidores que alojan la aplicación, pudiendo dar acceso a los datos que almacenan;
- aquellas que hacen vulnerables a los usuarios del servicio, permitiendo acceder a él bajo su identidad o hacerles ejecutar acciones en su contra.
Muchos sitios están dedicados a la seguridad en el desarrollo, como la Fundación OWASP (Open Worldwide Application Security Project), que enumera, entre otras cosas, los diez riesgos de seguridad críticos que afectan a las aplicaciones web [OWASP 79], y cuya lectura es altamente recomendada.
Principios básicos #
El principio fundamental al desarrollar un sistema que se comunica con otros sistemas y con usuarios externos es nunca confiar en lo que viene del exterior. Todo lo que entra debe ser verificado y filtrado antes de integrarse en el sistema interno. Todo lo que sale debe ser tratado de manera que no pueda ser utilizado de manera maliciosa.
Casi 9 de cada 10 ataques se deben a una mala validación de las entradas. [Vijayan 80]
Configuración del servidor HTTP #
Es importante asegurar los intercambios entre el servidor y los navegadores utilizando certificados de cifrado, para evitar que un observador externo pueda interceptar los flujos de datos. Si estos certificados eran costosos en el pasado, el proyecto Let’s Encrypt permite hoy obtenerlos gratuitamente.
Ha habido varias versiones de los protocolos SSL y luego TLS a lo largo de los años. Se recomienda desactivar las versiones más antiguas (SSLv3, TLSv1.0, TLSv1.1) ya que no son suficientemente seguras. A menos que se necesite específicamente soportar navegadores muy antiguos, incluso se recomienda desactivar el protocolo TLSv1.2, para mantener solo el TLSv1.3. Varios sitios explican cómo proceder [Better Stack 81, Nek 82].
Existen varios encabezados HTTP que deben definirse para reforzar la seguridad de un sitio:
Content-Security-Policy, X-Frame-Options, X-XSS-Protection, X-Content-Type-Options,
Referrer-Policy, Permissions-Policy, Strict-Transport-Security…
Encontrará varios sitios que explican cómo configurarlos
[Starr 83,
OWASP 84].
La inyección SQL #
La inyección SQL sigue siendo el ataque más frecuente encontrado en los servicios web.
Los ataques de inyección SQL representan dos tercios de los ataques dirigidos a las aplicaciones web [Vijayan 80]
El principio de la inyección SQL es — para un usuario malintencionado — corromper los datos enviados a un sistema, por ejemplo, a través de un formulario enviado al servidor, para modificar el comportamiento de una consulta en la base de datos.
Por ejemplo, en un formulario de autenticación, sería posible ingresar como identificador el valor "admin'; --", sin proporcionar una contraseña. Un sistema vulnerable ejecutará la siguiente consulta SQL:
SELECT * FROM users WHERE login = 'admin'; -- AND password = '';
En SQL, los dos guiones (--) marcan el inicio de un comentario, por lo que la consulta devolvería los datos del usuario "admin".
La solución es escapar todas las entradas de datos, ya sea utilizando una función como mysqli_real_escape_string() o PDO::quote(), o utilizando consultas preparadas. La consulta final sería entonces:
SELECT * FROM users WHERE login = 'admin\'; --' AND password = '';
La consulta no devolverá entonces ningún resultado.
Es importante señalar que las consultas preparadas a menudo se presentan como la única buena práctica a utilizar en este ámbito,
ya que permiten escapar fácilmente los parámetros. Sin embargo, fueron creadas con la idea de que una misma consulta
se utilice varias veces con diferentes parámetros.
Algunas consultas complejas generadas dinámicamente no pueden satisfacerse con consultas preparadas, por lo que es
necesario conocer las diferentes técnicas para escapar los datos.
Cross-site scripting (XSS) #
El XSS es una vulnerabilidad de seguridad que ocurre bastante comúnmente cuando los datos ingresados por un usuario pueden aparecer tal cual en una página web vista por otra persona.
Por ejemplo, esto puede ser un sitio que permite a sus visitantes dejar comentarios; el texto ingresado por una persona
se almacena en la base de datos y luego se muestra tal cual cada vez que alguien visita la página. Si un comentario
contiene una etiqueta "<script>" con código JavaScript, este código se ejecuta en los navegadores de los visitantes.
Esto abre la puerta a la recopilación de información personal, el robo de cookies de sesión (y por lo tanto la suplantación
de identidad), la ejecución de programas maliciosos (minería de criptomonedas, DDoS).
La solución depende del tipo de datos tratados.
Si se debe aceptar texto sin formato, es en la visualización donde hay que asegurarse de que el texto esté correctamente escapado,
para que una etiqueta "<script>" aparezca como texto ("<script>") y no sea interpretada por el navegador.
Para ello, la función htmlspecialchars()
es útil, a menos que se utilice un motor de plantillas que escape las variables por defecto o a demanda.
Si se debe aceptar código HTML enviado por un editor WYSIWYG, es necesario limpiar este flujo entrante para asegurarse de que solo contenga etiquetas HTML permitidas. Bibliotecas como HTMLPurifier permiten hacer esto.
OWASP propone el Cross-Site Scripting Prevention Cheat Sheet, que lista otros tipos de vulnerabilidades XSS menos comunes y cómo protegerse de ellas.
Serverside request forgery (SSRF) #
Los ataques SSRF tienen como objetivo hacer que el servidor ejecute solicitudes no deseadas. En todos los casos, la vulnerabilidad se encuentra en una entrada de datos que se utiliza con total confianza sin haber sido verificada.
Un sitio puede, por ejemplo, recibir como parámetro una URL a la que debe conectarse para recuperar información. Un hacker podría entonces proporcionar una URL que apunte a un archivo local o a una URL interna de la red, exponiendo así datos secretos.
Otro ejemplo, un sitio puede recibir como parámetro una ruta hacia un archivo local que debe incluirse. Si el parámetro apunta a una URL externa, el sistema irá a buscar ese contenido y lo interpretará, permitiendo la ejecución de código malicioso en el servidor.
La solución es, una vez más, no confiar nunca en los datos provenientes del exterior y siempre verificarlos antes de utilizarlos. Y nunca incluir un archivo cuya ruta sea proporcionada como parámetro (GET o POST).
OWASP propone el Server-Side Request Forgery Prevention Cheat Sheet, que lista las vulnerabilidades SSRF y cómo protegerse de ellas.
Cross-site request forgery (CSRF) #
A diferencia de otros ataques, que se basan en la inyección de datos corruptos en un sistema, el CSRF consiste en inducir a los usuarios a realizar acciones sin darse cuenta.
Por ejemplo, una interfaz de administración requiere autenticación para acceder. Una vez autenticado, se coloca una cookie de sesión
en el navegador. Esta interfaz permite eliminar artículos haciendo clic en enlaces del tipo /article/delete/[articleId],
que luego redirige a la lista de artículos.
Imaginemos ahora que un correo electrónico de phishing o un anuncio contiene un enlace que redirige a https://admin.mysite.com/article/delete/1234
Al hacer clic en él, el usuario elimina un artículo sin darse cuenta y no entiende por qué llega a la lista de artículos.
La solución más conocida es generar un token para incluirlo en la URL y verificar este token antes de ejecutar la acción solicitada.
Esta solución es tan extendida que muchos parecen pensar que es la única solución viable.
Cuidado, ya que una mala implementación puede generar una falsa sensación de seguridad. Y una implementación sólida, basada en sesiones,
impedirá usar el mismo sitio en varias pestañas al mismo tiempo.
Sin embargo, existen buenas prácticas, mucho más simples de implementar y igualmente eficaces.
La primera es aceptar solo solicitudes POST para acciones delicadas. De hecho, durante una redirección (o tras hacer clic en un enlace en un correo electrónico), el navegador solo puede realizar una solicitud GET.
La segunda buena práctica es utilizar el parámetro SameSite al crear la cookie de autenticación. Si este parámetro se establece en “Lax”, la cookie no se enviará en caso de una solicitud POST proveniente de otro sitio. Con el valor “Strict” es aún más restrictivo, ya que la cookie tampoco se enviará para las solicitudes GET.
La tercera, si es necesario, es verificar el contenido del encabezado REFERER enviado por el navegador. Durante una redirección, el REFERER contendrá un dominio diferente del dominio actual.
Cuando se utilizan conjuntamente, estas buenas prácticas son muy eficaces.
Una vez más, OWASP propone el Cross-Site Request Forgery Prevention Cheat Sheet, para profundizar en las vulnerabilidades CSRF.
Recursos #
- PHP Zen: Una selección de artículos y recursos, actualizada regularmente, en consonancia con el Manifiesto.
- Temma: El framework simple y eficaz — más fácil que los grandes frameworks, más potente que los microframeworks.
- PHP The Right Way: Una guía de referencia sobre los usos modernos de PHP, que propone buenas prácticas, estándares de código y recursos para escribir código PHP limpio y eficaz.
- PHP The Wrong Way: Una mirada satírica sobre el desarrollo en PHP, que pone de manifiesto con humor las malas prácticas y las trampas a evitar.
Referencias #
- The PHP Documentation Group. “Historia de PHP”
- Rasmus Lerdorf (Creador de PHP). “25 years of PHP” (2019)
- Rasmus Lerdorf (Creador de PHP). "[PHP-DEV] Re: Generators in PHP” (2012)
- Kevin Yank (Arquitecto principal en Culture Amp). “Interview – PHP’s Creator, Rasmus Lerdorf” (2002)
- Rasmus Lerdorf (Creador de PHP). @rasmus tweet (2010)
- Avery Pennarun (CEO de Tailscale). “The New Internet” (2024)
- Scott Berkun (Autor de The Myths of Innovation). “Two kinds of people: complexifiers and simplifiers” (2006)
- Articulo Wikipedia. “Wirth's law” (2024)
- Marek Kirejczyk (Fundador de vlayer Labs). “Hype Driven Development” (2016)
- Anouk Goossens (Consultora en The Learning Hub). “Why best practices aren’t the holy grail” (2023)
- Austin Knight (Responsable de diseño en Square). “The Road to Mediocrity Is Paved with Best Practices” (2015)
- Matt Stancliff (Contribuidor de Redis). “Panic! at the Job Market” (2024)
- Discusión Hacker News. “Why bad scientific code beats code following ‘best practices’” (2016)
- Jeff Atwood (Cofundador de StackOverflow y Discourse). “Why Objects Suck” (2004)
- Articulo Wikipedia. “Object-oriented programming”
- Brian Will (Ingeniero de software senior en Unity). “How to program without OOP” (2016)
- Eric S. Raymond (Cofundador de la Open Source Initiative). “The Art of Unix Programming” (2003)
- Jeff Atwood (Cofundador de StackOverflow y Discourse). “Your Code: OOP or POO?” (2007)
- Jeff Atwood (Cofundador de StackOverflow y Discourse). “When Object-Oriented Rendering is Too Much Code” (2006)
- Rich Hickey (Creador del lenguaje Clojure). Software Engineering Radio podcast #158 (2010)
- Asaf Shelly (Experto en IA y ciberseguridad). “Flaws of Object Oriented Modeling” (2008)
- Brian Will (Ingeniero de software senior en Unity). “Object-Oriented Programming is Embarrassing: 4 Short Examples” (video, 2016)
- Elliot Suzdalnitski (CEO de Empire School of Business). “Object-Oriented Programming — The Trillion Dollar Disaster” (2019)
- Joe Armstrong (Cocreador del lenguaje Erlang). “Why OO Sucks” (2011)
- Rob Pike (Cocreador del sistema operativo Plan 9, de la codificación UTF-8 y del lenguaje Go). Post en Google+ (2012)
- Gina Peter Banyard (Miembro del Core Team PHP − desarrollo y documentación). “PHP RFC: Unify PHP's typing modes (aka remove strict_types declare” (2021)
- Cheatmaster30 (Periodista en Gaming Reinvented). “Putting devs before users: how frameworks destroyed web performance” (2020)
- Mark Zeman (Fundador de Speedcurve). “Best Practices for Optimizing JavaScript” (2024)
- Artículo en el blog Easy Laptop Finder. “The relentless pursuit of cutting-edge JavaScript frameworks inadvertently contributed to a less accessible web” (2023)
- Eduardo Rodriguez (Ingeniero de software full-stack en they consulting). “Dependency management fatigue, or why I forever ditched React for Go+HTMX+Templ” (2024)
- David Haney (Fundador de CodeSession). “NPM & left-pad: Have We Forgotten How To Program?” (2016)
- Adrian Holovaty (Cocreador del framework Django). “dotJS - A framework author's case against frameworks” (video, 2017)
- Alex Russell (Responsable de producto asociado en Microsoft). “If Not React, Then What?” (2024)
- Gobierno del Reino Unido. “Building a robust frontend using progressive enhancement” (2024)
- Andy Bell (Fundador de Set Studio y Piccalilli). “It’s about time I tried to explain what progressive enhancement actually is” (2024)
- Kelly Sutton (Cofundador de Scholarly Software). “Moving on from React” (2024)
- Kelly Sutton (Cofondateur de Scholarly Software). “Moving on from React, a Year Later” (2025)
- David Heinemeier Hansson (Director técnico de 37signals, creador de Ruby On Rails). “Modern web apps without JavaScript bundling or transpiling” (2021)
- David Heinemeier Hansson (Director técnico de 37signals, creador de Ruby On Rails). “You can't get faster than No Build” (2023)
- Michael Stonebraker (Cocreador de la base de datos PostgreSQL). “Comparison of JOINS: MongoDB vs. PostgreSQL” (2020)
- Mridul Verma (Ingeniero de software senior en Sumo Logic). “Database Performance: MySQL vs MongoDB” (2024)
- Laurie Voss (Cofundador de NPM). “In defence of SQL” (2011)
- Laurie Voss (Cofundador de NPM). “ORM is an anti-pattern” (2011)
- Yegor Bugayenko (Director de laboratorio en Huawei). “ORM Is an Offensive Anti-Pattern” (2014)
- Eli Bendersky (Investigador en Google). “To ORM or not to ORM” (2019)
- Jeff Atwood (Cofundador de StackOverflow y Discourse). “Object-Relational Mapping is the Vietnam of Computer Science” (2006)
- Chris Maffey (Fundador de PHP Lab). “Why SQL is still really important” (2020)
- François Zaninotto (Cocreador del ORM Propel). Tweet de @francoisz (2019)
- Alex Martelli (Ingeniero principal en Google y "Fellow" de la Python Software Foundation). Respuesta en Stack Overflow (2010)
- Mattia Righetti (Ingeniero de sistemas en Cloudflare). “You Probably Don't Need Query Builders” (2025)
- Anh-Tho Chuong (CEO de Lago). “Is ORM still an 'anti pattern'?” (2023)
- Mattia Righetti (Ingeniero de sistemas en Cloudflare). “Can't Escape Good Old SQL” (2025)
- Mike Acton (Director de ingeniería en Hypnos Entertainment). “CppCon 2014: Data-Oriented Design and C++” (video, 2014)
- Noel Llopis (Desarrollador de juegos independiente). “Data-Oriented Design (Or Why You Might Be Shooting Yourself in The Foot With OOP)” (2009)
- Tan Dang (Redactor en Orient Software). “Revolutionize Your Code: The Magic of Data-oriented Design (DOD) Programming” (2023)
- Stoyan Nikolov (Ingeniero de IA principal en Google). “CppCon 2018: OOP Is Dead, Long Live Data-oriented Design” (video, 2018)
- Wikipedia. “Web framework”
- Contributors Mozilla. “Server-side web frameworks” (2024)
- Blog Expert .NET. “The Problem with Frameworks in Software Development” (2024)
- Kirill Bobrov (Ingeniero de datos senior en Spotify). “The Frameworks Dilemma” (2024)
- Blog TechAffinity. “The Benefits and Limitations of Software Development Frameworks” (2023)
- Sushrut Mishra (Editor técnico en FuelEd). “Why you shouldn’t use Libraries/Frameworks for everything” (2023)
- Evaldo Junior (Desarrollador senior). “Are micro-frameworks suitable only for small projects?” (2015)
- Matthew Weier O’Phinney (Responsable de producto senior en Zend). “On Microframeworks” (2012)
- Chris Dillon (Ingeniero de software senior en Mitre). “The Database Ruins All Good Ideas” (2021)
- Chris Dillon (Ingeniero de software senior en Mitre). “Microframeworks Are Too Small” (2023)
- Jim Coplien (Redactor, conferencista y researcher). “Why Most Unit Testing is Waste” (PDF)
- Blog Modus Create. “An Overview of Unit, Integration, and E2E Testing” (2023)
- Blog twilio. “Unit, Integration, and End-to-End Testing: What’s the Difference?” (2022)
- Andrew Trenk (Ingeniero de software en Google). “Testing on the Toilet: Don’t Overuse Mocks” (2013)
- David Heinemeier Hansson (Director técnico de 37signals, creador de Ruby On Rails). “TDD is dead. Long live testing” (2014)
- David Heinemeier Hansson (Director técnico de 37signals, creador de Ruby On Rails). “Testing like the TSA” (2012)
- Chris Kiehl (Ingeniero de software senior en Amazon). “Software development topics I've changed my mind on after 10 years in the industry” (2025)
- David Heinemeier Hansson (Director técnico de 37signals, creador deRuby On Rails). “The Majestic Monolith” (2016)
- Articulo Wikipedia. “Fallacies of distributed computing”
- David Heinemeier Hansson (Director técnico de 37signals, creador deRuby On Rails). “The Majestic Monolith can become The Citadel” (2020)
- Martin Fowler (Autor y conferencista internacional sobre desarrollo de software y metodologías ágiles). “Microservice Prerequisites” (2014)
- David Heinemeier Hansson (Director técnico de 37signals, creador deRuby On Rails). “Even Amazon can't make sense of serverless or microservices” (2023)
- OWASP. “OWASP Top Ten”
- Jai Vijayan (Periodista principal en Computerworld). “SQL Injection Attacks Represent Two-Third of All Web App Attacks” (2019)
- Better Stack. “How can I disable TLS 1.0 and 1.1 in apache?” (2023)
- Dimitri Nek. “How to Enable TLS 1.3 in Apache and Nginx on Ubuntu and CentOS”
- Jeff Starr (Desarrollador, diseñador, autor y editor). “Seven Important Security Headers for Your Website” (2024)
- OWASP. “OWASP Secure Headers Project”