¿Cómo evitar que JavaScript se convierta en código de espagueti?

8

He hecho bastante javascript a lo largo de los años y estoy usando un enfoque más orientado a objetos, específicamente con el patrón del módulo. ¿Qué tipo de enfoque utiliza para evitar una base de código más grande para convertirse en espagueti? ¿Hay algún "mejor enfoque"?

marko
fuente
1
¿Cómo podría saber si ha escrito código legible y fácil de mantener? - Tu compañero te lo dice después de revisar el código.
mosquito
¿Pero si un desarrollador piensa que las cosas se complican después de 3 líneas de código? ¿Qué hacer entonces?
marko
2
No puede determinar esto usted mismo porque sabe más como autor de lo que el código dice por sí mismo. Una computadora no puede decirle, por las mismas razones que no puede decir si una pintura es arte o no. Por lo tanto, necesita otro ser humano, capaz de mantener el software, para ver lo que ha escrito y dar su opinión. El nombre formal de dicho proceso es "Revisión por pares" ( fuente de citas - respuesta más votada a una pregunta que es la misma que la suya)
mosquito
Eso sería increíble si lo hiciéramos en el trabajo.
marko
Refactorización continua. Las malas decisiones siempre se toman. Se convierten en deuda técnica una vez que dejas de preocuparte por ellos.
Pithikos

Respuestas:

7

Dan Wahlin brinda orientación específica sobre cómo evitar el código de spaghetti en JavaScript.

La mayoría de las personas (incluyéndome a mí) comienzan a escribir código JavaScript agregando función tras función en un archivo .js o HTML. Si bien ciertamente no hay nada de malo en ese enfoque, ya que hace el trabajo, puede salirse rápidamente de control cuando se trabaja con mucho código. Cuando agrupar funciones en un archivo, encontrar código puede ser difícil, refactorizar el código es una tarea enorme (a menos que tenga una buena herramienta como Resharper 6.0), el alcance variable puede convertirse en un problema, y ​​realizar mantenimiento en el código puede ser una pesadilla, especialmente si originalmente no lo escribiste.

Describe algunos patrones de diseño de JavaScript que dan estructura al código.

Prefiero el patrón prototipo revelador . Mi segundo favorito es el patrón de módulo revelador , que difiere ligeramente del patrón de módulo estándar en que puede declarar el ámbito público / privado.

Jim G.
fuente
Sí, es algo de lo que estoy haciendo cuando construyo cosas más grandes.
marko
1
Pero donde vi el patrón con nombre por primera vez, debe haber estado en Javascript Patterns - ( amazon.com/JavaScript-Patterns-Stoyan-Stefanov/dp/0596806752/… ).
marko
Odio ser el tipo que diga "solo use un marco" ... pero para mí, siempre estoy pensando en el tipo que viene después de mí, y tener un marco bien probado, documentado y respaldado por la comunidad ... No es obvio para mí. Mi favorito es Javascript MVC (que pronto será CanJS). Tiene scripts de andamios y un administrador de dependencias muy intuitivo ( steal()) que le permite tener una aplicación muy bien estructurada mientras compila scripts en 1 o 2 archivos minificados.
Ryan Wheale
4

Si está utilizando OOP en un idioma moderno, el mayor peligro generalmente no es el " código de espagueti " sino el " código de ravioles". Puede terminar dividiendo y conquistando problemas hasta donde su base de código se compone de piezas muy pequeñas, funciones y objetos pequeños, todo vagamente acoplado y desempeñando responsabilidades singulares pero pequeñas, todo probado contra pruebas unitarias, con una telaraña de interacciones abstractas Esto hace que sea muy difícil razonar sobre lo que está sucediendo en términos de cosas como los efectos secundarios. Y es fácil pensar obstinadamente que usted diseñó esto maravillosamente, ya que las piezas individuales pueden ser hermosas y todas se adhieren a SOLID, mientras aún encuentran su cerebro al borde de explotar por la complejidad de todas las interacciones al tratar de comprender el sistema en su totalidad.

Y si bien es muy fácil razonar sobre lo que cualquiera de estos objetos o funciones hace individualmente, ya que desempeñan una responsabilidad tan singular y simple y tal vez incluso hermosa al expresar al menos sus dependencias abstractas a través de DI, el problema es que cuando se desea Para analizar el panorama general, es difícil imaginar qué mil cosas pequeñas con una telaraña de interacciones suman en última instancia. Por supuesto, la gente dice, solo mira los grandes objetos y las grandes funciones que están documentados y no profundizas en los más pequeños, y por supuesto, eso ayuda a comprender al menos lo que se supone que debe suceder de una manera de alto nivel. ..

Sin embargo, eso no ayuda mucho cuando realmente necesita cambiar o depurar el código, momento en el que debe ser capaz de descubrir qué suman todas estas cosas en cuanto a ideas de nivel inferior como efectos secundarios y cambios de estado persistentes y cómo mantener invariantes en todo el sistema. Y es bastante difícil reconstruir los efectos secundarios que ocurren entre las interacciones de miles de cosas pequeñas, ya sea que estén usando interfaces abstractas para comunicarse entre sí o no.

ECS

Entonces, lo último que encontré para mitigar este problema es en realidad los sistemas de componentes de entidad, pero eso podría ser excesivo para muchos proyectos. Me he enamorado de ECS hasta el punto de que ahora, incluso cuando escribo pequeños proyectos, uso mi motor ECS (aunque ese pequeño proyecto solo puede tener uno o dos sistemas). Sin embargo, para las personas que no están interesadas en ECS, he estado tratando de entender por qué ECS simplificó tanto la capacidad de comprender el sistema y creo que estoy en algunas cosas que deberían ser aplicables para muchos proyectos, incluso cuando no lo hacen. usar una arquitectura ECS.

Bucles homogéneos

Un comienzo básico es favorecer bucles más homogéneos que tienden a implicar más pases sobre los mismos datos, pero pases más uniformes. Por ejemplo, en lugar de hacer esto:

for each entity:
    apply physics to entity
    apply AI to entity
    apply animation to entity
    update entity textures
    render entity

... de alguna manera parece ayudar mucho si haces esto en su lugar:

for each entity:
    apply physics to entity

for each entity:
    apply AI to entity

etc.

Y eso puede parecer un derroche en bucle sobre los mismos datos varias veces, pero ahora cada pasada es muy homogénea. Le permite pensar: "Muy bien, durante esta fase del sistema, no sucede nada con estos objetos, excepto la física. Si hay cosas que están cambiando y efectos secundarios, todos están cambiando de una manera muy uniforme. " Y de alguna manera encuentro que eso ayuda a razonar sobre la base de código tanto, mucho.

Si bien parece un desperdicio, también puede ayudarlo a encontrar más oportunidades para paralelizar el código cuando se aplican tareas uniformes sobre todo en cada ciclo. Y también tiende a alentarun mayor grado de desacoplamiento Solo por naturaleza, cuando tiene estos pases divorciados que no intentan hacer todo a un objeto en un solo pase, tiende a encontrar más oportunidades para desacoplar fácilmente el código y mantenerlo desacoplado. En ECS, los sistemas a menudo están completamente desacoplados entre sí y no existe una "clase" o "función" externa que los coordine manualmente. El ECS tampoco sufre fallos repetidos de caché necesariamente ya que no necesariamente recorre los mismos datos varias veces (cada ciclo puede acceder a diferentes componentes ubicados completamente en otra parte de la memoria, pero asociados a las mismas entidades). Los sistemas no tienen que coordinarse manualmente, ya que son autónomos y responsables del bucle. Solo necesitan acceso a los mismos datos centrales.

Entonces, esa es una manera de comenzar que puede ayudarlo a establecer un tipo de control más uniforme y simple sobre su sistema.

Aplanamiento de manejo de eventos

Otra es reducir la dependencia en el manejo de eventos. El manejo de eventos a menudo es necesario para descubrir cosas externas que ocurrieron sin sondeo, pero a menudo hay formas de evitar eventos de empuje en cascada que conducen a flujos de control y efectos secundarios muy difíciles de predecir. El manejo de eventos, por naturaleza, tiende a lidiar con cosas complejas que le suceden a un pequeño objeto a la vez, cuando queremos enfocarnos en cosas simples y uniformes que le suceden a muchos objetos a la vez.

Entonces, por ejemplo, en lugar de un evento de cambio de tamaño del sistema operativo, se cambia el tamaño de un control principal que luego comienza a cambiar los eventos de cambio de tamaño y pintura para cada niño, lo que podría generar más eventos en quién sabe dónde, solo puede activar eventos de cambio de tamaño y marcar el padre y los niños como dirtyy necesita ser repintado. Incluso puede marcar todos los controles como necesarios para cambiar su tamaño, en ese momento se LayoutSystempuede seleccionar eso y cambiar el tamaño de las cosas y activar eventos de cambio de tamaño para todos los controles relevantes.

Luego, su sistema de renderizado de GUI podría despertarse con una variable de condición y recorrer los controles sucios y volver a pintarlos con un pase amplio (no una cola de eventos), y ese pase completo se centra en nada más que pintar una interfaz de usuario. Si hay una dependencia de orden jerárquico para volver a pintar, descubra las regiones sucias o rectángulos y vuelva a dibujar todo en esas regiones en el orden correcto de z para que no tenga que hacer un recorrido de árbol y simplemente pueda recorrer los datos en un muy moda simple y "plana", no una moda recursiva y "profunda".

Parece una diferencia tan sutil, pero por alguna razón, encuentro bastante útil desde el punto de vista del flujo de control. Realmente se trata de reducir la cantidad de cosas que suceden a los objetos individuales a la vez, tratando de apuntar a algo similar a SRP pero aplicado en términos de bucles y efectos secundarios: el " Principio de bucle de tarea única ", " El tipo de lado único Efecto por principio de bucle ".

Este tipo de flujo de control le permite pensar más en el sistema en términos de tareas grandes, pesadas pero extremadamente uniformes aplicadas en bucles, no todas las funciones y efectos secundarios que pueden ocurrir con un objeto individual a la vez. Por mucho que esto parezca que no haría una gran diferencia, descubrí que hizo toda la diferencia en el mundo, al menos en cuanto a la capacidad de mi propia mente para comprender el comportamiento de la base de código en todas las áreas que importaban al hacer cambios o depuración (que también encontré mucho menos necesidad de hacer con este enfoque).

Flujo de dependencias hacia los datos

Esta es probablemente la parte más controvertida de ECS, e incluso puede ser desastrosa para algunos dominios. Es una violación directa del Principio de Inversión de Dependencia de SOLID que establece que las dependencias deben fluir hacia abstracciones, incluso para módulos de bajo nivel. También viola la ocultación de información, pero para ECS al menos, no tanto como parece, ya que generalmente solo uno o dos sistemas accederán a los datos de cualquier componente.

Y creo que la idea de que las dependencias fluyan hacia las abstracciones funciona maravillosamente si sus abstracciones son estables (como en, inmutables). Las dependencias deben fluir hacia la estabilidad . Sin embargo, al menos en mi experiencia, las abstracciones a menudo no eran estables. Los desarrolladores nunca los entenderían bien y encontrarían la necesidad de cambiar o eliminar funciones (agregar no era tan malo), así como también depreciar algunas interfaces un año o dos después. Los clientes cambiarían de opinión de una manera que rompa los conceptos cuidadosos que los desarrolladores construyeron, derribando la fábrica abstracta para la casa abstracta de tarjetas abstractas.

Mientras tanto, encuentro que los datos son mucho más estables. Como ejemplo, ¿qué datos necesita un componente de movimiento en un juego? La respuesta es bastante simple. Necesita algún tipo de matriz de transformación 4x4 y necesita una referencia / puntero a un padre para permitir la creación de jerarquías de movimiento. Eso es. Esa decisión de diseño podría durar la vida útil de todo el software.

Puede haber algunas sutilezas como si deberíamos usar coma flotante de precisión simple o coma flotante de precisión doble para la matriz, pero ambas son decisiones decentes. Si se utiliza SPFP, la precisión es un desafío. Si se usa DPFP, entonces la velocidad es un desafío, pero ambas son buenas opciones que no necesitan ser cambiadas o necesariamente ocultas detrás de una interfaz. Cualquiera de las representaciones es con la que podemos comprometernos y mantenernos estables.

Sin embargo, ¿cuáles son todas las funciones necesarias para una IMotioninterfaz abstracta y, lo que es más importante, cuáles son el conjunto mínimo ideal de funciones que debe proporcionar para hacer las cosas de manera efectiva contra las necesidades de todos los subsistemas que alguna vez lidiarán con el movimiento? Es mucho, mucho más difícil de responder sin comprender mucho más sobre la totalidad de las necesidades de diseño de la aplicación por adelantado. Y así, cuando tantas partes de la base de código terminan dependiendo de esto IMotion, es posible que tengamos que reescribir tanto con cada iteración de diseño a menos que podamos hacerlo bien la primera vez.

Por supuesto, en algunos casos la representación de datos podría ser muy inestable. Algo podría depender de una estructura de datos compleja que podría necesitar un reemplazo en el futuro debido a deficiencias en la estructura de datos, mientras que las necesidades funcionales del sistema asociado con la estructura de datos se anticipan fácilmente por adelantado. Por lo tanto, vale la pena ser pragmático y decidir las cosas caso por caso en cuanto a si las dependencias fluyen hacia abstracciones o datos, pero a veces al menos, los datos son más fáciles de estabilizar que las abstracciones, y no fue hasta que adopté ECS que incluso consideró hacer que las dependencias fluyan predominantemente hacia los datos (con efectos increíblemente simplificadores y estabilizadores).

Entonces, si bien esto puede parecer extraño, en los casos en que es mucho más fácil llegar a un diseño estable para los datos a través de una interfaz abstracta, en realidad sugiero dirigir las dependencias a datos antiguos simples. Esto podría ahorrarle muchas iteraciones repetidas de reescrituras. Sin embargo, en relación con los flujos de control y el código de espagueti y ravioles, esto también tenderá a simplificar sus flujos de control cuando no tenga que tener interacciones tan complejas antes de llegar finalmente a los datos relevantes.


fuente
1
Realmente disfruté esta respuesta, aunque podría no responder directamente a la pregunta, fue muy perspicaz. Gracias por contribuir, ¿tal vez podrías escribir una serie de publicaciones de blog sobre el tema? ¡Lo leería!
Ben
¡Salud! En cuanto al blog, ¡me gusta usar este lugar para volcar mis pensamientos! :-D
2

Los estándares de código en general son útiles.

Eso significa:

Los módulos son definitivamente necesarios. También necesita coherencia en la forma en que se implementan las "clases", es decir, "métodos en el prototipo" vs "métodos en la instancia". También debe decidir a qué versión de ECMAScript apuntar, y si está apuntando, es decir, ECMAScript 5 utilice las características de lenguaje proporcionadas (por ejemplo, getters y setters).

Consulte también: TypeScript, que podría ayudarlo a estandarizar, por ejemplo, las clases. Un poco nuevo en este momento, pero no veo ninguna desventaja en usarlo ya que casi no hay bloqueo (porque se compila en JavaScript).

Janus Troelsen
fuente
Creo que Typecript es innecesario.
marko
1
@marko: ¿innecesario para qué aplicación? ¿Implementarías un compilador de 25 kloc sin tipeo estático?
Janus Troelsen
2

Una separación entre el código de trabajo y el código implementado es útil. Utilizo una herramienta para combinar y comprimir mis archivos javascript. Entonces puedo tener cualquier número de módulos en una carpeta, todos como archivos separados, para cuando estoy trabajando en ese módulo específico. Pero por tiempo de implementación, esos archivos se combinan en un archivo comprimido.

Uso Chirpy http://chirpy.codeplex.com/ , que también es compatible con SASS y coffeeScript.

usuario69841
fuente