¿Las abstracciones tienen que reducir la legibilidad del código?

19

Un buen desarrollador con el que trabajo me contó recientemente sobre algunas dificultades que tenía para implementar una función en algún código que habíamos heredado; Dijo que el problema era que el código era difícil de seguir. A partir de eso, miré más profundamente en el producto y me di cuenta de lo difícil que era ver la ruta del código.

Usó tantas interfaces y capas abstractas, que tratar de entender dónde comenzaron y terminaron las cosas fue bastante difícil. Me hizo pensar en las veces que había visto proyectos anteriores (antes de que fuera tan consciente de los principios de código limpio) y me resultó extremadamente difícil avanzar en el proyecto, principalmente porque mis herramientas de navegación de código siempre me llevaban a una interfaz. Se necesitaría mucho esfuerzo adicional para encontrar la implementación concreta o dónde algo estaba conectado en alguna arquitectura de tipo de complemento.

Sé que algunos desarrolladores rechazan estrictamente los contenedores de inyección de dependencia por esta misma razón. Confunde tanto la ruta del software que la dificultad de la navegación de código aumenta exponencialmente.

Mi pregunta es: cuando un marco o patrón introduce tanta sobrecarga como esta, ¿vale la pena? ¿Es un síntoma de un patrón mal implementado?

Supongo que un desarrollador debería mirar la imagen más amplia de lo que esas abstracciones aportan al proyecto para ayudarlo a superar la frustración. Por lo general, sin embargo, es difícil hacer que vean ese panorama general. Sé que no pude vender las necesidades de COI y DI con TDD. Para esos desarrolladores, el uso de esas herramientas simplemente obstaculiza demasiado la legibilidad del código.

Martin Blore
fuente

Respuestas:

17

Este es realmente un comentario largo sobre la respuesta de @kevin cline.

A pesar de que los idiomas en sí mismos no necesariamente causan o previenen esto, creo que hay algo en su noción de que está relacionado con los idiomas (o al menos las comunidades lingüísticas) en algún grado de todos modos. En particular, aunque puede encontrarse con el mismo problema en diferentes idiomas, a menudo tomará formas bastante diferentes en diferentes idiomas.

Solo por ejemplo, cuando te encuentras con esto en C ++, lo más probable es que sea menos un resultado de demasiada abstracción y más un resultado de demasiada inteligencia. Solo por ejemplo, el programador ha ocultado la transformación crucial que está ocurriendo (que no puede encontrar) en un iterador especial, por lo que parece que solo está copiando datos de un lugar a otro realmente tiene una serie de efectos secundarios que no tienen nada que ver hacer con esa copia de los datos. Solo para mantener las cosas interesantes, esto se entrelaza con la salida que se crea como un efecto secundario de la creación de un objeto temporal en el curso de lanzar un tipo de objeto a otro.

Por el contrario, cuando te lo encuentras en Java, es mucho más probable que veas alguna variante del conocido "mundo empresarial hola", donde en lugar de una sola clase trivial que hace algo simple, obtienes una clase base abstracta y una clase derivada concreta que implementa la interfaz X, y es creada por una clase de fábrica en un marco DI, etc. Las 10 líneas de código que hacen el trabajo real están enterradas bajo 5000 líneas de infraestructura.

Parte de esto depende del entorno al menos tanto como del idioma: trabajar directamente con entornos de ventanas como X11 y MS Windows es conocido por convertir un programa trivial de "hola mundo" en más de 300 líneas de basura casi indescifrable. Con el tiempo, hemos desarrollado varios juegos de herramientas para aislarnos de eso también, pero 1) esos juegos de herramientas son bastante triviales, y 2) el resultado final no solo es más grande y complejo, sino que también es menos flexible que un equivalente en modo de texto (por ejemplo, aunque solo está imprimiendo algo de texto, rara vez es posible / compatible redirigirlo a un archivo).

Para responder (al menos parte de) la pregunta original: al menos cuando la he visto, se trataba menos de una implementación deficiente de un patrón que de simplemente aplicar un patrón que era inapropiado para la tarea en cuestión, la mayoría a menudo de intentar aplicar algún patrón que bien podría ser útil en un programa que es inevitablemente enorme y complejo, pero cuando se aplica a un problema más pequeño termina haciéndolo también enorme y complejo, aunque en este caso el tamaño y la complejidad realmente eran evitables .

Jerry Coffin
fuente
7

Encuentro que esto a menudo es causado por no adoptar un enfoque YAGNI . Todo lo que pasa por las interfaces, a pesar de que solo hay una implementación concreta y no hay planes actuales para introducir otras, es un excelente ejemplo de agregar complejidad que no vas a necesitar. Probablemente sea una herejía, pero siento lo mismo por el uso de la inyección de dependencia.

Carson63000
fuente
+1 por mencionar YAGNI y abstracciones con puntos de referencia únicos. El papel principal de hacer una abstracción es factorizar el punto común de múltiples cosas. Si se hace referencia a una abstracción solo desde un punto, no podemos hablar de factorizar cosas comunes, una abstracción como esta solo contribuye al problema del yoyo. Extendería esto porque esto es cierto para todo tipo de abstracciones: funciones, genéricos, macros, lo que sea ...
Calmarius
3

Bueno, no hay suficiente abstracción y su código es difícil de entender porque no puede aislar qué partes hacen qué.

Demasiada abstracción y ves la abstracción pero no el código en sí mismo, y luego hace que sea difícil seguir el hilo de ejecución real.

Para lograr una buena abstracción, uno debe BESAR: vea mi respuesta a estas preguntas para saber qué seguir para evitar ese tipo de problemas .

Creo que evitar la jerarquía profunda y los nombres son el punto más importante a tener en cuenta para el caso que describe. Si las abstracciones estuvieran bien nombradas, no tendría que profundizar demasiado, solo al nivel de abstracción donde necesita comprender lo que sucede. Los nombres le permiten identificar dónde está este nivel de abstracción.

El problema surge en el código de bajo nivel, cuando realmente necesita que se entienda todo el proceso. Entonces, la encapsulación a través de módulos claramente aislados es la única ayuda.

Klaim
fuente
3
Bueno, no hay suficiente abstracción y su código es difícil de entender porque no puede aislar qué partes hacen qué. Eso es encapsulación, no abstracción. Puede aislar partes en clases concretas sin mucha abstracción.
Declaración del
Las clases no son las únicas abstracciones que estamos usando: funciones, módulos / bibliotecas, servicios, etc. En sus clases, generalmente abstrae cada funcionalidad detrás de una función / método, que puede llamar a otro método que abstraiga cada una de ellas.
Klaim
1
@Statement: encapsular datos es, por supuesto, una abstracción.
Ed S.
Sin embargo, las jerarquías de espacio de nombres son realmente agradables.
JAB
2

Para mí es un problema de acoplamiento y está relacionado con la granularidad del diseño. Incluso la forma más flexible de acoplamiento introduce dependencias de una cosa a otra. Si eso se hace entre cientos y miles de objetos, incluso si todos son relativamente simples, adhiérase a SRP, e incluso si todas las dependencias fluyen hacia abstracciones estables, eso produce una base de código que es muy difícil de razonar como un todo interrelacionado.

Hay cosas prácticas que lo ayudan a medir la complejidad de una base de código, que no se discute con frecuencia en la SE teórica, como qué tan profundo puede llegar a la pila de llamadas antes de llegar al final y qué tan profundo debe llegar antes de poder hacerlo, con mucha confianza, entienda todos los posibles efectos secundarios que podrían ocurrir en ese nivel de la pila de llamadas, incluso en el caso de una excepción.

Y descubrí, solo en mi experiencia, que los sistemas más planos con pilas de llamadas menos profundas tienden a ser mucho más fáciles de razonar. Un ejemplo extremo sería un sistema de entidad-componente donde los componentes son solo datos sin procesar. Solo los sistemas tienen funcionalidad, y en el proceso de implementación y uso de un ECS, encontré que es el sistema más fácil, hasta ahora, para razonar sobre cuándo las bases de código complejas que abarcan cientos de miles de líneas de código básicamente se reducen a unas pocas docenas de sistemas que Contiene toda la funcionalidad.

Demasiadas cosas proporcionan funcionalidad

La alternativa anterior cuando trabajaba en bases de códigos anteriores era un sistema con cientos a miles de objetos en su mayoría pequeños, en lugar de unas pocas docenas de sistemas voluminosos con algunos objetos utilizados solo para pasar mensajes de un objeto a otro ( Messageobjeto, por ejemplo, que tenía su interfaz pública propia). Eso es básicamente lo que obtienes de forma analógica cuando reviertes el ECS a un punto donde los componentes tienen funcionalidad y cada combinación única de componentes en una entidad produce su propio tipo de objeto. Y eso tenderá a producir funciones más pequeñas y simples heredadas y proporcionadas por infinitas combinaciones de objetos que modelan ideas juveniles ( Particleobjeto vs.Physics System, p.ej). Sin embargo, también tiende a generar un gráfico complejo de interdependencias que dificulta razonar sobre lo que sucede desde el nivel amplio, simplemente porque hay muchas cosas en la base de código que realmente pueden hacer algo y, por lo tanto, pueden hacer algo mal. - tipos que no son tipos de "datos", sino tipos de "objeto" con funcionalidad asociada. Los tipos que sirven como datos puros sin funcionalidad asociada no pueden salir mal ya que no pueden hacer nada por sí mismos.

Las interfaces puras no ayudan mucho a este problema de comprensión porque, incluso si eso hace que las "dependencias de tiempo de compilación" sean menos complicadas y proporcione más espacio para respirar para el cambio y la expansión, no hace que las "dependencias de tiempo de ejecución" y las interacciones sean menos complicadas. El objeto del cliente aún termina invocando funciones en un objeto de cuenta concreto, incluso si se están llamando IAccount. El polimorfismo y las interfaces abstractas tienen sus usos, pero no desacoplan las cosas de la manera que realmente te ayuda a razonar sobre todos los efectos secundarios que podrían ocurrir en un punto dado. Para lograr este tipo de desacoplamiento efectivo, necesita una base de código que tenga muchas menos cosas que contengan funcionalidad.

Más datos, menos funcionalidad

Por lo tanto, he encontrado que el enfoque ECS, incluso si no lo aplica completamente, es extremadamente útil, ya que convierte lo que habrían sido cientos de objetos en datos sin procesar con sistemas voluminosos, más gruesos, que proporcionan todos los funcionalidad Maximiza el número de tipos de "datos" y minimiza el número de tipos de "objetos" y, por lo tanto, minimiza absolutamente el número de lugares en su sistema que realmente pueden salir mal. El resultado final es un sistema muy "plano" sin un gráfico complejo de dependencias, solo sistemas a componentes, nunca al revés, y nunca componentes a otros componentes. Básicamente, son muchos más datos sin procesar y muchas menos abstracciones lo que tiene el efecto de centralizar y aplanar la funcionalidad de la base de código a áreas clave, abstracciones clave.

30 cosas más simples no son necesariamente más simples de razonar sobre 1 cosa más compleja, si esas 30 cosas más simples están interrelacionadas mientras la cosa compleja se sostiene por sí misma. Entonces, mi sugerencia es en realidad transferir la complejidad de las interacciones entre objetos y más hacia objetos más voluminosos que no tienen que interactuar con nada más para lograr el desacoplamiento masivo, a "sistemas" completos (no monolitos y objetos divinos, fíjate, y no clases con 200 métodos, sino algo considerablemente más alto que un Messageo un Particlea pesar de tener una interfaz minimalista). Y favorezca tipos de datos antiguos más simples. Cuanto más dependas de ellos, menos acoplamiento tendrás. Incluso si eso contradice algunas ideas de SE, he descubierto que realmente ayuda mucho.


fuente
0

Mi pregunta es, cuando un marco o patrón introduce tanta sobrecarga como esta, ¿vale la pena? ¿Es un síntoma de un patrón mal implementado?

Tal vez sea un síntoma de elegir el lenguaje de programación incorrecto.

Kevin Cline
fuente
1
No veo cómo esto tiene algo que ver con el lenguaje de elección. Las abstracciones son un concepto de alto nivel independiente del lenguaje.
Ed S.
@Ed: Algunas abstracciones son más fáciles de realizar en algunos idiomas que en otros.
Kevin Cline
Sí, pero eso no significa que no pueda escribir una abstracción perfectamente mantenible y fácil de entender en esos idiomas. Mi punto fue que su respuesta no responde la pregunta ni ayuda al OP de ninguna manera.
Ed S.
0

La mala comprensión de los patrones de diseño tiende a ser una causa importante de este problema. Uno de los peores que he visto para este yo-yo'ing y rebotando de una interfaz a otra sin muchos datos concretos en el medio fue una extensión para el Control de cuadrícula de Oracle.
Sinceramente, parecía que alguien había tenido un método de fábrica abstracto y un orgasmo de patrón de decorador en todo mi código Java. Y me dejó sintiéndome tan vacía y sola.

Jeff Langemeier
fuente
-1

También advertiría contra el uso de las funciones IDE que facilitan el resumen de cosas.

Christopher Mahan
fuente