1 puntos por GN⁺ 2024-08-17 | 1 comentarios | Compartir por WhatsApp
  • Basado en una cita de Linus Torvalds, un buen diseño empieza por definir de forma sólida las estructuras de datos y sus relaciones antes de escribir código
  • Un modelo de datos bien diseñado simplifica de manera natural la lógica de la aplicación y hace que el software sea más confiable y fácil de entender
  • Si se pospone el modelo de datos, la carga de trabajo aumenta más adelante, pero si la estructura se define bien desde el inicio, las migraciones y la expansión de sistemas complejos se vuelven más fáciles
  • En un proyecto, en lugar de optimizar algoritmos complejos, una reestructuración de datos eliminó por completo la categoría del problema y reemplazó una función de 500 líneas por una de 50 líneas y una estructura de datos
  • En la práctica, hay que aplicar tipos más estrictos en las interfaces y bases de datos, y diseñar primero el flujo de datos y la interacción entre componentes antes que los detalles del código

Las estructuras de datos determinan el diseño del código

  • Según Linus Torvalds, Git tiene un diseño simple con estructuras de datos estables y documentadas, y enfatiza la idea de organizar el código alrededor de los datos
    • La frase central es: “Los malos programadores se preocupan por el código; los buenos programadores se preocupan por las estructuras de datos y sus relaciones”
    • Una de las razones del éxito de Git está en haber diseñado el código con un enfoque centrado en los datos
  • Una buena estructura de datos facilita el diseño y mantenimiento del código, y mejora la confiabilidad del software, la comprensión del sistema y la legibilidad del código
    • La lógica de la aplicación suele seguir el modelo de datos
    • Si el modelo de datos se deja para después, el trabajo posterior aumenta
    • Un modelo de datos bien diseñado facilita más adelante las migraciones y la expansión de sistemas complejos
  • En un caso real de proyecto, reestructurar los datos tuvo más impacto que seguir refinando un algoritmo complejo
    • Al cambiar la estructura de datos, se eliminó toda una categoría del problema
    • Una función de 500 líneas fue reemplazada por una de 50 líneas y una estructura de datos bien diseñada
    • El nuevo código fue más rápido y también más fácil de entender y mantener
    • Aun así, como hubo que reestructurar los datos existentes, el esfuerzo se trasladó a las capas inferiores

Es mejor mover la complejidad hacia los datos

  • La “Rule of Representation” de The Art of Unix Programming explica que conviene poner el conocimiento en los datos para que la lógica del programa sea simple y robusta
    • La lógica procedural es difícil de verificar para las personas, pero las estructuras de datos complejas son más fáciles de modelar y razonar
    • Un diagrama de árbol de punteros con 50 nodos puede ser más expresivo y explicativo que un diagrama de flujo de un programa de 50 líneas
    • Si una tabla de transformación se expresa como inicialización de arreglos, puede ser más transparente y clara que escribir lo mismo con una sentencia switch
    • Si hay que elegir entre poner la complejidad en el código o en la estructura de datos, conviene moverla hacia la estructura de datos

En la práctica, primero se diseña el flujo de datos

  • La forma más directa de llevar esto a la práctica es empezar por los datos
    • Aplicar tipos más estrictos en interfaces o bases de datos puede reducir la complejidad del código
    • Hace falta dedicar más tiempo por adelantado a pensar en las estructuras de datos
    • Esto no significa que el código no importe; todos los elementos importan en conjunto
    • Antes de entrar en los detalles del código, resulta útil adoptar un enfoque de alto nivel para ver cómo fluyen los datos y cómo interactúan los componentes
  • Como ejemplo de los requisitos para un Senior Engineer (L5), en FAANG suele incluirse la redacción de documentos de diseño de alto nivel para sistemas más complejos
    • Esto también incluye liderar la planificación del equipo y construir buenos roadmaps para funciones de tamaño mediano a grande
    • La capacidad de diseñar primero el flujo de datos y la interacción entre componentes se relaciona con un mayor nivel de impacto en ingeniería

1 comentarios

 
GN⁺ 2024-08-17
Comentarios en Hacker News
  • Ese post de Substack parece haber copiado sin más varias citas de esta publicación de Stack Exchange: https://softwareengineering.stackexchange.com/questions/1631...

  • “Muéstrame tus diagramas de flujo[código] y escóndeme tus tablas[esquema], y seguiré desconcertado. Muéstrame tus tablas[esquema] y por lo general no necesitaré tus diagramas de flujo[código]. Serán evidentes.” — Fred Brooks, "The Mythical Man Month", capítulo 9

    • Cuando era niño y estaba obsesionado con Quake, le escribí a John Carmack para preguntarle qué consejo le daría a un aspirante a programador y si tenía algún libro favorito. Sorprendentemente, me respondió de forma bastante considerada, y en esa respuesta decía esto:
      “Lee The Mythical Man Month. Recuerdo haber pensado que un libro tan viejo no podía tener nada relevante que decir sobre el desarrollo de software actual, pero estaba equivocado.”
    • Entré para compartir esta cita porque da demasiado en el blanco. Aunque hay una excepción: cuando el costo de cambiar el esquema de la base de datos se vuelve mucho mayor que el costo de cambiar el código.
      Entonces los desarrolladores de aplicaciones empiezan a abusar de la base de datos porque pueden avanzar más rápido y tienen más trabajo por hacer
  • Las estructuras de datos y los tipos no son lo mismo. Las estructuras de datos son patrones de bits y referencias a otros patrones de bits, es decir, punteros o relaciones.
    Los tipos imponen restricciones sobre esos patrones de bits en la forma en que se usan dentro de un lenguaje de programación, pero además pueden expresar muchas otras funciones del lenguaje. Crear jerarquías de tipos complejas con abstracciones innecesarias no significa “preocuparse por las estructuras de datos”, y es un patrón de fracaso en el que incluso ingenieros brillantes caen a menudo

    • Es un punto sutil pero importante. Los tipos pueden ser una herramienta útil para restringir y especificar el esquema de una estructura de datos, pero preocuparse por los tipos y preocuparse por las estructuras de datos son cosas bastante distintas
    • Los tipos son estructuras de datos reconocidas por el lenguaje. Por eso permiten que las herramientas hagan verificaciones que no podrían hacer sobre estructuras de datos comunes
    • Las estructuras de datos son algoritmos detenidos. En cada operación se mezcla y se mueve algo, pero en general se quedan quietas, como una máquina de Turing a la que la gente solo le gira la manivela de vez en cuando.
      Los tipos son bits en disco
    • Buen punto. Igualar las estructuras de datos con los tipos es una simplificación excesiva que pierde lo esencial.
      La idea original aquí se parece más a pensar el problema con mayor profundidad y no elegir una estructura que después te va a pasar factura. Por ejemplo, mira hasta dónde llegaron los pipes de Unix y cuánto se expandieron a tantos ámbitos y casos de uso. Es una gran manera de visualizar cómo construir sistemas respetando las limitaciones de las personas y de las máquinas.
      A Ken Thompson y a otros les tomó bastante tiempo darse cuenta de que algo como los pipes tenía sentido en Unix. No fue una intuición fácil de obtener; hizo falta persistencia para encontrar los bloques de construcción correctos del sistema y el trabajo posterior
    • A una misma estructura de datos se le pueden asignar tipos distintos. Eso es lo que hace el operador typedef de Pascal
  • Linus siempre resume bien lo que otros piensan de forma difusa. Lo que dice el texto también se parece a DDD, que se ha convertido en una habilidad perdida.
    Aquí, “perdida” significa que la mayoría de los desarrolladores con los que uno se cruza hoy está más interesada en mover algoritmos y JSON de un lado a otro que en entender el dominio que maneja y modelar las entidades y sus interacciones. En los diseños modernos basados en AWS, eso aparece como conjuntos de DynamoDB GSI con poco sustento, objetos anémicos y capas de “servicios” que parecen scripts donde se acumulan parche sobre parche. Tal vez había una suposición implícita de que, dentro de los límites del servicio, el contexto del dominio quedaría suficientemente bien definido, pero no me parece una buena suposición.
    No sé en qué momento nuestra industria perdió el rigor del diseño. Si fue en la universidad, en el pipeline de entrevistas, por bajar los estándares, o si fue todo junto.

    • Creo que la industria nunca se tomó en serio el diseño de software. Siempre se trata en términos negativos, se lo vincula con personas consideradas políticamente incorrectas o irrelevantes, y atrae montones de comentarios de gente que quiere decir que todo es malo solo porque alguien hizo algo mal una vez.
      Peor todavía, el diseño cometió el gran pecado de no poder automatizarse fácilmente. Entonces la gente sigue sin espíritu crítico el diseño que las herramientas le imponen y se incomoda ante la idea de tener que pensar más a fondo en lo que hace. Todos quieren tercerizar ese pensamiento a “expertos”.
      También influye que no se enseña bien, que hay que aprenderlo por cuenta propia durante años y que se lo considera menos real que el código, así que se lo trata como menos importante. Pero esa creencia termina dejando el nivel de lo que se puede construir estancado en la etapa de principiante avanzado. Los programadores, como colectivo, parecen haber elegido mantener los estándares lo más bajos posible, y en este tema casi da la impresión de que existe una mentalidad de cangrejo entre ellos.
    • El modelo de dominio anémico fue identificado como un antipatrón hace bastante tiempo[1]. Suele aparecer junto con la obsesión por los tipos primitivos[2], y termina esparciendo por todas partes validaciones y comprobaciones sobre tipos primitivos como cadenas y números.
      También genera mucha duplicación de código que no parece duplicación porque no es idéntica en términos sintácticos, pero funcionalmente hace lo mismo.
      1 https://martinfowler.com/bliki/AnemicDomainModel.html
      2 https://wiki.c2.com/?PrimitiveObsession
    • La industria recompensa sobre todo escribir código, no diseñar software.
      Creo que es porque las consecuencias del mal código se notan menos. Un mal puente se cae, pero un mal código solo se refactoriza o se reemplaza con más código. No es más que un archivo de texto que la gerencia no entiende siendo cambiado por otro archivo de texto que la gerencia tampoco entiende.
      Y una vez que algo funciona, se produce un apagón mental. No hay nada más permanente que un hack temporal que terminó funcionando perfectamente. Pero 1000 hacks temporales no forman un sistema bien diseñado. Creo que madurar en desarrollo de software significa concentrarse más en los datos y en las relaciones que en escribir código. Hay que poder convertir eso en código, pero no transformar código que funciona en un modelo de datos, sino convertir los datos y las relaciones en código.
    • Todavía no he visto una razón convincente de por qué los objetos anémicos están tan mal vistos. La mayoría de las cosas de DDD que he visto no eran más que getters y setters verbosos.
      Que una entidad de dominio pueda contener toda la lógica no significa que necesariamente deba hacerlo. Por ejemplo, si hay que verificar si un nombre de usuario ya existe, ¿cómo se supone que debe hacerlo dentro de una entidad de dominio que “no puede depender” de la capa de acceso a datos? A menudo recomiendan algo como un “servicio de dominio”, pero entonces la lógica de negocio queda dispersa en varios lugares, lo que se siente contrario al propósito de DDD.
      Me gusta bastante DDD como filosofía, pero detesto los patrones de “DDD táctico”. Creo que demasiada gente equipara Domain-Driven Design con Domain-Driven Implementation. Intento construir un dominio rico cuando corresponde, pero no todo proyecto encaja con eso y trato de no obsesionarme con la terminología. No me importa si un tipo “Name” es un objeto de valor o una raíz de agregado. Lo más importante son los contextos delimitados. También reconozco que a veces DDD puede aumentar la complejidad de una aplicación y aportar muy poco a cambio. No diría jamás que es una solución universal.
      Voy a seguir usando DDD, pero me cuesta sacarme la sensación de que DDD intenta transmitir algo como “ves, la programación orientada a objetos tampoco está tan mal”. Y no estoy seguro de que siquiera logre ese objetivo.
    • Durante décadas, el rendimiento del CPU, el tamaño de la memoria, el espacio en disco y la velocidad de red crecieron de forma exponencial, y eso hizo desaparecer en gran medida el costo de un mal diseño. Por eso los picadores de código pudieron seguir produciendo código basura tan rápido como les daban las manos sobre el teclado y, en general, no pasaba nada.
  • Me resulta interesante porque, antes de empezar en la ingeniería profesional, hacía todos los días análisis de datos y estadística con sistemas estadísticos como Matlab, R y el Python temprano.
    Por eso mi perspectiva de ingeniería siempre estuvo basada en dos cosas: gestionar el estado funcional y los flujos de trabajo de datos.
    Tras 10 años trabajando profesionalmente en ingeniería de software, vi que la mayoría de los ingenieros más “científicos”, como Minsky o Shannon, describían el mundo de la computación en términos de gestión de estado, transformación de datos y manejo del overhead computacional. Todas las grandes figuras y pioneros del software daban muchísima importancia a los datos y al estado; en los inicios de la computación, de hecho, eso era prácticamente todo, y se esperaba que ese patrón continuara.
    En cambio, en el diseño de sistemas de ingeniería no hay ninguna consistencia en las suposiciones fundamentales que siempre serían ciertas y que todos seguirían; si existen, suelen parecer más bien modas. En la mayoría del software operativo, los plazos de negocio determinan mucho más las prioridades y la estructura de ingeniería que la robustez, la antifragilidad o la gestión del estado.
    Las organizaciones profesionales, como gremios o sindicatos, son rechazadas casi universalmente por los ingenieros de software. Como no hay ninguna desventaja por no tomarse en serio al IEEE, en la práctica nadie se lo toma en serio. Como resultado, no existe ningún mecanismo que obligue o autorregule la práctica, como sí ocurre en ingeniería civil o bioingeniería, y aun en esos campos se usa apenas lo justo.
    En general, el estado actual del desarrollo de software está completamente desconectado de sus raíces, que eran muy elevadas y filosóficas, y en la práctica lo conducen empresas que priorizan sistemas que hagan ganar dinero a quienes ya tienen dinero. Así que lo “bueno” casi no tiene relación con aquello para lo que existen incentivos.

  • «Muéstrame el diagrama de flujo [código] y escóndeme las tablas [estructuras de datos], y seguiré desconcertado. Muéstrame las tablas y, por lo general, no necesitaré el diagrama de flujo. Serán evidentes.» — Fred Brooks

    • Esta cita parece pasar por alto que el modelo de persistencia y las estructuras de datos reales pueden ser diferentes, e incluso quizá deberían serlo.
      Hacerlo coincidir 1:1 con las tablas subyacentes es extremadamente restrictivo y, en mi opinión, lleva a modelos que desaprovechan la expresividad que ofrecen los lenguajes modernos.
  • Esto es, en esencia, una perspectiva de la programación funcional y la teoría de categorías.
    Existe algún objeto de datos, y su estructura impone restricciones sobre cómo puede transformarse. Y toda la lógica del programa pasa a tratarse de transformaciones que preservan esa estructura.
    Las transformaciones se vuelven más simples y fáciles de razonar, y al final queda un grafo donde las transformaciones son aristas y la estructura son nodos. En general, es más fácil de razonar que un programa imperativo arbitrario.

    • Esa no es la perspectiva de la programación funcional ni de la teoría de categorías. Es la perspectiva de toda filosofía de lenguajes, y quienes prefieren lo orientado a objetos o lo procedimental afirmarían exactamente lo mismo. Definir correctamente los tipos de datos es importante y aplica a todos los lenguajes y paradigmas.
      La perspectiva de la programación funcional está más cerca de sostener que los objetos no deben transformarse y que hay que evitar la mutación, lo cual es aparte de esta discusión. El núcleo de la teoría de categorías trata sobre patrones de relaciones que aparecen en común en varias áreas de las matemáticas, y tampoco tiene nada que ver con lo que se discute aquí. Tal vez quería hablar de teoría de tipos, pero eso tampoco viene al caso.
  • La conclusión a la que llegué hace tiempo es esta. Es muy probable que todo el trabajo que hacemos en el código sobreviva mucho menos que una buena decisión tomada sobre los datos.
    https://www.swyx.io/data-outlasts-code-but

    • Las buenas decisiones no se ven. Solo parece que las malas sobreviven para siempre.
  • Este principio también aplica a nivel de negocio. Sigo tratando con analistas de negocio que se obsesionan con el proceso (código) y no dedican tiempo primero a entender las entidades y sus relaciones (datos).
    Como resultado, cuando llega el momento de construir algo, no logran comunicarles a los desarrolladores cómo debería verse el modelo de datos. Los procesos se implementan y el modelo de datos se arma sobre la marcha en vez de diseñarse con cuidado.