Vi la charla de Stuart Sierra " Thinking In Data " y tomé una de las ideas como principio de diseño en este juego que estoy haciendo. La diferencia es que él está trabajando en Clojure y yo estoy trabajando en JavaScript. Veo algunas diferencias importantes entre nuestros idiomas en eso:
- Clojure es una programación idiomáticamente funcional
- La mayoría del estado es inmutable.
Tomé la idea de la diapositiva "Todo es un mapa" (de 11 minutos, 6 segundos a> 29 minutos). Algunas cosas que dice son:
- Siempre que vea una función que tome 2-3 argumentos, puede hacer un caso para convertirla en un mapa y simplemente pasar un mapa. Hay muchas ventajas en eso:
- No tiene que preocuparse por el orden de los argumentos.
- No tiene que preocuparse por ninguna información adicional. Si hay claves adicionales, esa no es realmente nuestra preocupación. Simplemente fluyen, no interfieren.
- No tienes que definir un esquema
- A diferencia de pasar un objeto, no hay datos ocultos. Pero argumenta que el ocultamiento de datos puede causar problemas y está sobrevalorado:
- Actuación
- Facilidad de implementación
- Tan pronto como se comunique a través de la red o entre procesos, debe hacer que ambas partes acuerden la representación de datos de todos modos. Ese es un trabajo adicional que puede omitir si solo trabaja en datos.
Lo más relevante para mi pregunta. Esto es 29 minutos en: "Haga que sus funciones sean componibles". Aquí está el ejemplo de código que usa para explicar el concepto:
;; Bad (defn complex-process [] (let [a (get-component @global-state) b (subprocess-one a) c (subprocess-two a b) d (subprocess-three a b c)] (reset! global-state d))) ;; Good (defn complex-process [state] (-> state subprocess-one subprocess-two subprocess-three))
Entiendo que la mayoría de los programadores no están familiarizados con Clojure, por lo que volveré a escribir esto en un estilo imperativo:
;; Good def complex-process(State state) state = subprocess-one(state) state = subprocess-two(state) state = subprocess-three(state) return state
Aquí están las ventajas:
- Fácil de probar
- Fácil de ver esas funciones de forma aislada
- Es fácil comentar una línea de esto y ver cuál es el resultado al eliminar un solo paso
- Cada subproceso podría agregar más información al estado. Si el subproceso uno necesita comunicar algo al subproceso tres, es tan simple como agregar una clave / valor.
- No hay repeticiones para extraer los datos que necesita fuera del estado solo para que pueda guardarlos nuevamente. Simplemente pase todo el estado y deje que el subproceso asigne lo que necesita.
Ahora, volviendo a mi situación: tomé esta lección y la apliqué a mi juego. Es decir, casi todas mis funciones de alto nivel toman y devuelven un gameState
objeto. Este objeto contiene todos los datos del juego. EG: Una lista de badGuys, una lista de menús, el botín en el suelo, etc. Aquí hay un ejemplo de mi función de actualización:
update(gameState)
...
gameState = handleUnitCollision(gameState)
...
gameState = handleLoot(gameState)
...
De lo que estoy aquí para preguntar es, ¿he creado alguna abominación que pervierte una idea que solo es práctica en un lenguaje de programación funcional? JavaScript no es idiomáticamente funcional (aunque se puede escribir de esa manera) y es realmente difícil escribir estructuras de datos inmutables. Una cosa que me preocupa es que asume que cada uno de esos subprocesos es puro. ¿Por qué es necesario hacer esa suposición? Es raro que cualquiera de mis funciones sean puras (con eso quiero decir que a menudo modifican gameState
. No tengo otros efectos secundarios complicados aparte de eso). ¿Estas ideas se desmoronan si no tienes datos inmutables?
Me preocupa que algún día me despierte y me dé cuenta de que todo este diseño es una farsa y realmente he estado implementando el antipatrón Big Ball Of Mud .
Honestamente, he estado trabajando en este código durante meses y ha sido genial. Siento que estoy obteniendo todas las ventajas que él reclama. Mi código es muy fácil de razonar. Pero soy un equipo de un solo hombre, así que tengo la maldición del conocimiento.
Actualizar
He estado codificando más de 6 meses con este patrón. Por lo general, en este momento me olvido de lo que he hecho y ahí es donde "¿escribí esto de manera limpia?" entra en juego. Si no lo hubiera hecho, realmente lucharía. Hasta ahora, no estoy luchando en absoluto.
Entiendo cómo sería necesario otro par de ojos para validar su mantenibilidad. Todo lo que puedo decir es que me importa la mantenibilidad en primer lugar. Siempre soy el evangelista más ruidoso para el código limpio, sin importar dónde trabajo.
Quiero responder directamente a aquellos que ya tienen una mala experiencia personal con esta forma de codificación. Entonces no lo sabía, pero creo que realmente estamos hablando de dos formas diferentes de escribir código. La forma en que lo hice parece estar más estructurada de lo que otros han experimentado. Cuando alguien tiene una mala experiencia personal con "Todo es un mapa", hablan de lo difícil que es mantenerlo porque:
- Nunca se sabe la estructura del mapa que requiere la función
- Cualquier función puede mutar la entrada de formas que nunca esperarías. Debe buscar en toda la base del código para descubrir cómo una clave en particular ingresó al mapa o por qué desapareció.
Para aquellos con tal experiencia, tal vez la base del código fue: "Todo toma 1 de N tipos de mapas". El mío es, "Todo toma 1 de 1 tipo de mapa". Si conoce la estructura de ese tipo 1, conoce la estructura de todo. Por supuesto, esa estructura generalmente crece con el tiempo. Es por eso...
Hay un lugar para buscar la implementación de referencia (es decir, el esquema). Esta implementación de referencia es un código que usa el juego para que no se desactualice.
En cuanto al segundo punto, no agrego / elimino claves al mapa fuera de la implementación de referencia, solo muto lo que ya está allí. También tengo un gran conjunto de pruebas automatizadas.
Si esta arquitectura finalmente colapsa bajo su propio peso, agregaré una segunda actualización. De lo contrario, suponga que todo va bien :)
fuente
Respuestas:
He admitido una aplicación donde 'todo es un mapa' antes. Es una idea terrible. ¡POR FAVOR no lo hagas!
Cuando especifica los argumentos que se pasan a la función, eso hace que sea muy fácil saber qué valores necesita la función. Evita pasar datos extraños a la función que solo distrae al programador: cada valor pasado implica que es necesario, y eso hace que el programador que admite su código tenga que descubrir por qué se necesitan los datos.
Por otro lado, si pasa todo como un mapa, el programador que admite su aplicación tendrá que comprender completamente la función llamada en todos los sentidos para saber qué valores debe contener el mapa. Peor aún, es muy tentador reutilizar el mapa pasado a la función actual para pasar datos a las siguientes funciones. Esto significa que el programador que admite su aplicación necesita conocer todas las funciones llamadas por la función actual para comprender qué hace la función actual. Eso es exactamente lo opuesto al propósito de escribir funciones: abstraer los problemas para que no tenga que pensar en ellos. Ahora imagine 5 llamadas de profundidad y 5 llamadas de ancho cada una. Es mucho tener en mente y muchos errores para cometer.
"todo es un mapa" también parece llevar a usar el mapa como valor de retorno. Lo he visto. Y, de nuevo, es un dolor. Las funciones llamadas nunca deben sobrescribirse entre sí, a menos que conozca la funcionalidad de todo y sepa que el valor X del mapa de entrada debe reemplazarse para la siguiente llamada a la función. Y la función actual necesita modificar el mapa para devolver su valor, que a veces debe sobrescribir el valor anterior y otras no.
editar - ejemplo
Aquí hay un ejemplo de dónde esto fue problemático. Esta fue una aplicación web. La entrada del usuario se aceptó desde la capa de IU y se colocó en un mapa. Luego se llamaron a las funciones para procesar la solicitud. El primer conjunto de funciones verificaría la entrada errónea. Si hubiera un error, el mensaje de error se colocaría en el mapa. La función de llamada verificaría el mapa para esta entrada y escribiría el valor en la interfaz de usuario si existiera.
El siguiente conjunto de funciones iniciaría la lógica empresarial. Cada función tomaría el mapa, eliminaría algunos datos, modificaría algunos datos, operaría sobre los datos en el mapa y colocaría el resultado en el mapa, etc. Las funciones posteriores esperarían resultados de funciones anteriores en el mapa. Para corregir un error en una función posterior, tuvo que investigar todas las funciones anteriores, así como la persona que llama para determinar en todas partes el valor esperado podría haberse establecido.
Las siguientes funciones extraerían datos de la base de datos. O, más bien, pasarían un mapa a la capa de acceso a datos. El DAL verificará si el mapa contiene ciertos valores para controlar cómo se ejecutó la consulta. Si 'justcount' fuera una clave, entonces la consulta sería 'count select foo from bar'. Cualquiera de las funciones que se llamó anteriormente podría haber sido la que agregó 'justcount' al mapa. Los resultados de la consulta se agregarían al mismo mapa.
Los resultados llegarían a la persona que llama (lógica de negocios) que verificaría el mapa para saber qué hacer. Algo de esto provendría de cosas que fueron agregadas al mapa por la lógica comercial inicial. Algunos vendrían de los datos de la base de datos. La única forma de saber de dónde vino fue encontrar el código que lo agregó. Y la otra ubicación que también puede agregarlo.
El código era efectivamente un desastre monolítico, que tenía que entender en su totalidad para saber de dónde provenía una sola entrada en el mapa.
fuente
gameState
sin saber nada sobre lo que sucedió antes o después. Simplemente reacciona a los datos que se proporcionan. ¿Cómo llegaste a una situación en la que las funciones pisarían los dedos de los demás? ¿Puede dar un ejemplo?Personalmente, no recomendaría ese patrón en ninguno de los paradigmas. Hace que sea más fácil escribir inicialmente a expensas de que sea más difícil razonar más adelante.
Por ejemplo, intente responder las siguientes preguntas sobre cada función de subproceso:
state
requiere?Con este patrón, no puede responder esas preguntas sin leer toda la función.
En un lenguaje orientado a objetos, el patrón tiene aún menos sentido, porque el estado de seguimiento es lo que hacen los objetos.
fuente
Lo que parece estar haciendo es, efectivamente, una mónada de Estado manual; lo que haría es construir un combinador de enlaces (simplificado) y volver a expresar las conexiones entre sus pasos lógicos usando eso:
Incluso puede usar
stateBind
para construir los diversos subprocesos a partir de subprocesos y continuar por un árbol de combinadores de enlace para estructurar su cálculo de manera adecuada.Para obtener una explicación de la mónada estatal completa y no simplificada, y una excelente introducción a las mónadas en general en JavaScript, consulte esta publicación de blog .
fuente
stateBind
combinador que le di es un ejemplo muy simple de eso.Entonces, parece haber mucha discusión entre la efectividad de este enfoque en Clojure. Creo que podría ser útil analizar la filosofía de Rich Hickey de por qué creó Clojure para admitir abstracciones de datos de esta manera :
Fogus reitera estos puntos en su libro Functional Javascript :
fuente
El abogado del diablo
Creo que esta pregunta merece un abogado del diablo (pero, por supuesto, soy parcial). Creo que @KarlBielefeldt está haciendo muy buenos puntos y me gustaría abordarlos. Primero quiero decir que sus puntos son geniales.
Como mencionó que este no es un buen patrón incluso en la programación funcional, consideraré JavaScript y / o Clojure en mis respuestas. Una similitud extremadamente importante entre estos dos idiomas es que están tipificados dinámicamente. Estaría más de acuerdo con sus puntos si implementara esto en un lenguaje de tipo estático como Java o Haskell. Pero voy a considerar que la alternativa al patrón "Todo es un mapa" es un diseño OOP tradicional en JavaScript y no en un lenguaje estáticamente escrito (espero no estar creando un argumento de sorpresa al hacer esto, Por favor hagamelo saber).
En un lenguaje de tipo dinámico, ¿cómo respondería normalmente estas preguntas? El primer parámetro de una función puede ser nombrado
foo
, pero ¿qué es eso? ¿Una matriz? ¿Un objeto? ¿Un objeto de matrices de objetos? ¿Cómo te enteraste? La única forma que conozco esNo creo que el patrón "Todo es un mapa" haga alguna diferencia aquí. Estas son todavía las únicas formas que conozco para responder estas preguntas.
También tenga en cuenta que en JavaScript y en la mayoría de los lenguajes de programación imperativos, cualquiera
function
puede requerir, modificar e ignorar cualquier estado al que pueda acceder y la firma no hace ninguna diferencia: la función / método podría hacer algo con un estado global o con un singleton. Las firmas a menudo mienten.No estoy tratando de establecer una falsa dicotomía entre "Todo es un mapa" y un código OO mal diseñado . Solo estoy tratando de señalar que tener firmas que toman menos / más parámetros de grano fino / grueso no garantiza que sepa cómo aislar, configurar y llamar a una función.
Pero, si me permitiera usar esa falsa dicotomía: en comparación con la escritura de JavaScript en la forma tradicional de OOP, "Todo es un mapa" parece mejor. En la forma tradicional de OOP, la función puede requerir, modificar o ignorar el estado que pasa o el estado que no pasa. Con este patrón "Todo es un mapa", solo requiere, modifica o ignora el estado que pasa en.
En mi código sí. Vea mi segundo comentario a la respuesta de @ Evicatos. Quizás esto sea solo porque estoy haciendo un juego, no puedo decirlo. En un juego que se actualiza 60 veces por segundo, realmente no importa si
dead guys drop loot
entoncesgood guys pick up loot
o viceversa. Cada función sigue haciendo exactamente lo que se supone que debe hacer, independientemente del orden en que se ejecuten. Los mismos datos solo se envían a ellos enupdate
llamadas diferentes si intercambias el pedido. Si esgood guys pick up loot
asídead guys drop loot
, los buenos recogerán el botín en el próximoupdate
y no es gran cosa. Un humano no podrá notar la diferencia.Al menos esta ha sido mi experiencia general. Me siento realmente vulnerable admitiendo esto públicamente. Quizás considerar que esto está bien es algo muy , muy malo. Avísame si he cometido un terrible error aquí. Pero, si tengo, es muy fácil para reorganizar las funciones por lo que el orden es
dead guys drop loot
entoncesgood guys pick up loot
de nuevo. Tomará menos tiempo que el tiempo que tomó escribir este párrafo: PTal vez pienses que "los muertos deberían tirar el botín primero. Sería mejor si tu código aplicara ese orden". Pero, ¿por qué los enemigos tienen que soltar botín antes de que puedas recoger el botín? Para mí eso no tiene sentido. Tal vez el botín se dejó caer
updates
hace 100 . No es necesario verificar si un malo arbitrario tiene que recoger el botín que ya está en el suelo. Por eso creo que el orden de estas operaciones es completamente arbitrario.Es natural escribir pasos desacoplados con este patrón, pero es difícil notar sus pasos acoplados en OOP tradicional. Si escribiera OOP tradicional, la forma natural e ingenua de pensar es hacer que el
dead guys drop loot
retorno sea unLoot
objeto que tengo que pasargood guys pick up loot
. No podría reordenar esas operaciones ya que la primera devuelve la entrada de la segunda.Los objetos tienen estado y es idiomático mutar el estado haciendo que su historia simplemente desaparezca ... a menos que escriba manualmente el código para realizar un seguimiento. ¿De qué manera el estado de seguimiento es "lo que hacen"?
Bien, como dije, "Es raro que alguna de mis funciones sea pura". Ellos siempre solamente operan en sus parámetros, pero mutan sus parámetros. Este es un compromiso que sentí que tenía que hacer al aplicar este patrón a JavaScript.
fuente
parent
? ¿Es unrepetitions
conjunto de números o cadenas o no importa? ¿O tal vez las repeticiones son solo un número para representar la cantidad de repeticiones que quiero? Hay muchas apis por ahí que solo toman un objeto de opciones . El mundo es un lugar mejor si nombra las cosas correctamente, pero no garantiza que sepa cómo usar la API, sin hacer preguntas.He descubierto que mi código tiende a terminar estructurado así:
No me propuse crear esta distinción, pero así es como termina en mi código. No creo que usar un estilo necesariamente niegue el otro.
Las funciones puras son fáciles de probar por unidad. Los más grandes con mapas entran más en el área de prueba de "integración" ya que tienden a involucrar más partes móviles.
En javascript, una cosa que ayuda mucho es usar algo como la biblioteca Meteor's Match para realizar la validación de parámetros. Deja muy claro lo que la función espera y puede manejar los mapas de manera bastante limpia.
Por ejemplo,
Ver http://docs.meteor.com/#match para más información.
:: ACTUALIZACIÓN ::
La grabación de video de Stuart Sierra de "Clojure in the Large" de Clojure / West también toca este tema. Al igual que el OP, controla los efectos secundarios como parte del mapa, por lo que las pruebas se vuelven mucho más fáciles. También tiene una publicación en el blog que describe su flujo de trabajo actual de Clojure que parece relevante.
fuente
El argumento principal que puedo pensar en contra de esta práctica es que es muy difícil saber qué datos necesita realmente una función.
Lo que eso significa es que los futuros programadores en la base de código tendrán que saber cómo funciona internamente la función que se llama, y cualquier llamada de función anidada, para poder llamarla.
Cuanto más lo pienso, más huele tu objeto gameState como un global. Si así es como se usa, ¿por qué pasarlo?
fuente
gameState = f(gameState)
af()
, es mucho más difícil de probarf
.f()
puede devolver algo diferente cada vez que lo llamo. Pero es fácil hacer quef(gameState)
devuelva lo mismo cada vez que recibe la misma entrada.Hay un nombre más apropiado para lo que estás haciendo que Big ball of mud . Lo que estás haciendo se llama el patrón de objeto de Dios . A primera vista no parece así, pero en Javascript hay muy poca diferencia entre
y
Si es o no una buena idea probablemente depende de las circunstancias. Pero ciertamente está en línea con la forma Clojure. Uno de los objetivos de Clojure es eliminar lo que Rich Hickey llama "complejidad incidental". Múltiples objetos comunicantes es ciertamente más complejo que un solo objeto. Si divide la funcionalidad en varios objetos, de repente tiene que preocuparse por la comunicación y la coordinación y la división de las responsabilidades. Esas son complicaciones que solo son incidentales a su objetivo original de escribir un programa. Debería ver la charla de Rich Hickey Simple hecho fácil . Creo que esta es una muy buena idea.
fuente
Acabo de enfrentar este tema más temprano hoy mientras jugaba con un nuevo proyecto. Estoy trabajando en Clojure para hacer un juego de póker. Representé los valores nominales y los trajes como palabras clave, y decidí representar una tarjeta como un mapa como
También podría haberlos hecho listas o vectores de los dos elementos de palabras clave. No sé si hace una diferencia de memoria / rendimiento, así que solo voy con mapas por ahora.
Sin embargo, en caso de que cambie de opinión más tarde, decidí que la mayoría de las partes de mi programa deberían pasar por una "interfaz" para acceder a las piezas de una tarjeta, de modo que los detalles de la implementación estén controlados y ocultos. Tengo funciones
que usa el resto del programa. Las tarjetas se transmiten a las funciones como mapas, pero las funciones usan una interfaz acordada para acceder a los mapas y, por lo tanto, no deberían poder estropearse.
En mi programa, una tarjeta probablemente solo sea un mapa de 2 valores. En la pregunta, todo el estado del juego se pasa como un mapa. El estado del juego será mucho más complicado que una sola carta, pero no creo que sea culpable usar un mapa. En un lenguaje de objeto imperativo, podría tener un solo objeto GameState grande y llamar a sus métodos, y tener el mismo problema:
Ahora está orientado a objetos. ¿Hay algo particularmente malo en ello? No lo creo, solo está delegando el trabajo a funciones que saben cómo manejar un objeto State. Y ya sea que trabaje con mapas u objetos, debe tener cuidado con cuándo dividirlo en partes más pequeñas. Así que digo que usar mapas está perfectamente bien, siempre y cuando uses la misma atención que usarías con los objetos.
fuente
Por lo que (poco) he visto, usar mapas u otras estructuras anidadas para hacer un solo objeto de estado inmutable global como este es bastante común en lenguajes funcionales, al menos los puros, especialmente cuando se usa la Mónada de Estado como @Ptharien'sFlame mentioend .
Dos obstáculos para usar esto de manera efectiva que he visto / leído (y otras respuestas aquí han mencionado) son:
Hay un par de diferentes técnicas / patrones comunes que pueden ayudar a aliviar estos problemas:
El primero son las cremalleras : estas permiten atravesar y mutar en un estado profundo dentro de una jerarquía anidada inmutable.
Otro es Lentes : estos le permiten enfocarse en la estructura a una ubicación específica y leer / mutar el valor allí. Puede combinar diferentes lentes para enfocarse en diferentes cosas, como una cadena de propiedades ajustable en OOP (¡donde puede sustituir variables por nombres de propiedades reales!)
Prismatic recientemente hizo una publicación de blog sobre el uso de este tipo de técnica, entre otras cosas, en JavaScript / ClojureScript, que debe consultar. Utilizan los cursores (que comparan con las cremalleras) para mostrar el estado de la ventana para las funciones:
IIRC, también mencionan la inmutabilidad en JavaScript en esa publicación.
fuente
assoc-in
otros). Supongo que acabo de tener a Haskell en el cerebro. Me pregunto si alguien ha hecho un puerto de JavaScript. La gente probablemente no lo mencionó porque (como yo) no vieron la charla :)Si es una buena idea o no, dependerá realmente de lo que esté haciendo con el estado dentro de esos subprocesos. Si entiendo el ejemplo de Clojure correctamente, los diccionarios de estado que se devuelven no son los mismos diccionarios de estado que se pasan. Son copias, posiblemente con adiciones y modificaciones, que (supongo) que Clojure puede crear eficientemente porque La naturaleza funcional del lenguaje depende de ello. Los diccionarios de estado originales para cada función no se modifican de ninguna manera.
Si lo entiendo correctamente, está modificando los objetos de estado que pasa en sus funciones de JavaScript en lugar de devolver una copia, lo que significa que está haciendo algo muy, muy diferente de lo que está haciendo ese código Clojure. Como señaló Mike Partridge, esto es básicamente un global al que pasas explícitamente y regresas de las funciones sin ninguna razón real. En este punto, creo que simplemente te hace pensar que estás haciendo algo que en realidad no estás haciendo.
Si realmente está haciendo copias explícitamente del estado, modificándolo y luego devolviendo esa copia modificada, continúe. No estoy seguro de que sea necesariamente la mejor manera de lograr lo que está tratando de hacer en Javascript, pero probablemente sea "similar" a lo que está haciendo ese ejemplo de Clojure.
fuente
update()
función. Moví uno hacia arriba y otro hacia abajo. Todas mis pruebas todavía pasaron y cuando jugué mi juego no noté efectos negativos. Siento que mis funciones son tan componibles como el ejemplo de Clojure. Ambos estamos desechando nuestros datos anteriores después de cada paso.(-> 10 (- 5) (/ 2))
devuelve 2.5.(-> 10 (/ 2) (- 5))
devuelve 0.Si tiene un objeto de estado global, a veces llamado "objeto de dios", que se pasa a cada proceso, termina confundiendo una serie de factores, todos los cuales aumentan el acoplamiento y disminuyen la cohesión. Todos estos factores tienen un impacto negativo en la mantenibilidad a largo plazo.
Acoplamiento de vagabundo Esto surge del paso de datos a través de varios métodos que no necesitan casi todos los datos, con el fin de llevarlos al lugar que realmente puede manejarlos. Este tipo de acoplamiento es similar al uso de datos globales, pero puede estar más contenido. El acoplamiento de vagabundos es lo opuesto a "necesidad de saber", que se utiliza para localizar efectos y contener el daño que un código erróneo puede tener en todo el sistema.
Navegación de datos Cada subproceso en su ejemplo necesita saber cómo llegar exactamente a los datos que necesita, y debe ser capaz de procesarlos y quizás construir un nuevo objeto de estado global. Esa es la consecuencia lógica del acoplamiento de vagabundos; Se requiere todo el contexto de un dato para poder operar en el dato. Nuevamente, el conocimiento no local es algo malo.
Si estaba pasando una "cremallera", "lente" o "cursor", como se describe en la publicación de @paul, eso es una cosa. Estaría conteniendo el acceso y permitiendo que la cremallera, etc., controle la lectura y escritura de los datos.
Infracción de responsabilidad única Reclamar que cada uno de "subproceso-uno", "subproceso-dos" y "subproceso-tres" tiene una sola responsabilidad, es decir, producir un nuevo objeto de estado global con los valores correctos, es un reduccionismo atroz. Es todo al final, ¿no?
Mi punto aquí es que tener todos los componentes principales de tu juego tienen las mismas responsabilidades ya que tu juego derrota el propósito de la delegación y el factoring.
Impacto de sistemas
El mayor impacto de su diseño es la baja mantenibilidad. El hecho de que puedas mantener todo el juego en tu cabeza dice que es muy probable que seas un excelente programador. Hay muchas cosas que he diseñado que podría tener en mente durante todo el proyecto. Sin embargo, ese no es el objetivo de la ingeniería de sistemas. El punto es hacer un sistema que pueda funcionar para algo más grande que una persona puede tener en su cabeza al mismo tiempo .
Agregar otro programador, o dos u ocho, hará que su sistema se desmorone casi de inmediato.
Finalmente
fuente