¿Cómo evitas recorrer infinitamente diseños igualmente subóptimos?

10

Entonces, probablemente, como muchos, a menudo me encuentro con dolores de cabeza con problemas de diseño en los que, por ejemplo, hay algún patrón / enfoque de diseño que parece ajustarse intuitivamente al problema y tiene los beneficios deseados. Muy a menudo hay algunas advertencias que dificultan la implementación del patrón / enfoque sin algún tipo de trabajo en torno al cual luego se niega el beneficio del patrón / enfoque. Puedo terminar fácilmente iterando a través de muchos patrones / enfoques porque, como era de esperar, casi todos tienen algunas advertencias muy significativas en situaciones del mundo real en las que simplemente no hay una solución fácil.


Ejemplo:

Te daré un ejemplo hipotético basado libremente en uno real que encontré recientemente. Digamos que quiero usar la composición sobre la herencia porque las jerarquías de herencia han obstaculizado la escalabilidad del código en el pasado. Podría refactorizar el código pero luego encontrar que hay algunos contextos en los que la superclase / clase base simplemente necesita llamar a la funcionalidad en la subclase, a pesar de los intentos de evitarla.

El siguiente mejor enfoque parece ser la implementación de un patrón de medio delegado / observador y medio patrón de composición para que la superclase pueda delegar el comportamiento o para que la subclase pueda observar eventos de superclase. Entonces, la clase es menos escalable y mantenible porque no está claro cómo se debe extender, también es difícil extender los oyentes / delegados existentes. Además, la información no se oculta bien porque uno comienza a necesitar conocer la implementación para ver cómo extender la superclase (a menos que use comentarios muy extensamente).

Entonces, después de esto, podría optar por simplemente usar observadores o delegados por completo para alejarnos de los inconvenientes que conlleva mezclar los enfoques en gran medida. Sin embargo, esto viene con sus propios problemas. Por ejemplo, podría encontrar que termino necesitando observadores o delegados para una cantidad cada vez mayor de comportamientos hasta que termino necesitando observadores / delegados para prácticamente cada comportamiento. Una opción podría ser tener un gran oyente / delegado para todo el comportamiento, pero luego la clase implementadora termina con muchos métodos vacíos, etc.

Entonces podría intentar otro enfoque, pero hay tantos problemas con eso. Luego el siguiente, y el siguiente, etc.


Este proceso iterativo se vuelve muy difícil cuando cada enfoque parece tener tantos problemas como cualquiera de los otros y conduce a una especie de parálisis de decisión de diseño . También es difícil aceptar que el código terminará siendo igualmente problemático independientemente de qué patrón de diseño o enfoque se use. Si termino en esta situación, ¿significa que el problema en sí necesita ser repensado? ¿Qué hacen los demás cuando se encuentran con esta situación?

Editar: Parece que hay una serie de interpretaciones de la pregunta que quiero aclarar:

  • He eliminado la OOP de la pregunta por completo porque resulta que en realidad no es específica de la OOP, además es demasiado fácil interpretar mal algunos de los comentarios que hice al pasar sobre la OOP.
  • Algunos han afirmado que debería adoptar un enfoque iterativo e intentar diferentes patrones, o que debería descartar un patrón cuando deje de funcionar. Este es el proceso al que me refería en primer lugar. Pensé que esto estaba claro en el ejemplo, pero podría haberlo dejado más claro, así que he editado la pregunta para hacerlo.
Jonathan
fuente
3
No se "suscriba a la filosofía OO". Es una herramienta, no una decisión importante de la vida. Úselo cuando ayude y no lo use cuando no lo haga.
user253751
Si está atrapado pensando en términos de ciertos patrones, tal vez intente escribir algún proyecto moderadamente complejo en C, será interesante experimentar cuánto puede hacer sin esos patrones.
user253751
2
Oh C tiene patrones. Son simplemente patrones muy diferentes. :)
candied_orange
He aclarado la mayoría de estas malas interpretaciones en la edición
Jonathan
Tener estos problemas aparece de vez en cuando es normal. Hacer que ocurran a menudo no debería ser el caso. Lo más probable es que esté utilizando el paradigma de programación incorrecto para el problema o sus diseños son una mezcla de paradigmas en los lugares equivocados.
Dunk

Respuestas:

8

Cuando llego a una decisión difícil como esa, generalmente me hago tres preguntas:

  1. ¿Cuáles son los pros y los contras de todas las soluciones disponibles?

  2. ¿Hay alguna solución que aún no haya considerado?

  3. Lo más importante:
    ¿Cuáles son exactamente mis requisitos? ¿No son los requisitos en papel, los verdaderos fundamentales?
    ¿Puedo reformular de alguna manera el problema / ajustar sus requisitos, de modo que permita una solución simple y directa?
    ¿Puedo representar mis datos de alguna manera diferente, de modo que permita una solución tan simple y directa?

    Estas son las preguntas sobre los fundamentos del problema percibido. Puede resultar que en realidad estabas tratando de resolver el problema incorrecto. Su problema puede tener requisitos específicos que permitan una solución mucho más simple que el caso general. ¡Cuestiona tu formulación de tu problema!

Me resulta muy importante pensar mucho en las tres preguntas antes de continuar. Salga a caminar, pasee por su oficina, haga lo que sea necesario para reflexionar sobre las respuestas a estas preguntas. Sin embargo, responder a estas preguntas no debería llevar mucho tiempo. Los tiempos adecuados pueden variar de 15 minutos a algo así como una semana, dependen de la maldad de las soluciones que ya ha encontrado y su impacto en general.

El valor de este enfoque es que a veces encontrarás soluciones sorprendentemente buenas. Soluciones elegantes Las soluciones bien valen el tiempo que invirtió en responder estas tres preguntas. Y no encontrará esa solución, si solo ingresa la siguiente iteración inmediatamente.

Por supuesto, a veces no parece existir una buena solución. En ese caso, está atrapado con sus respuestas a la pregunta uno, y lo bueno es simplemente lo menos malo. En este caso, el valor de tomarse el tiempo para responder estas preguntas es que posiblemente evite las iteraciones que están destinadas a fallar. Solo asegúrese de volver a la codificación dentro de un período de tiempo adecuado.

cmaster - restablecer monica
fuente
Bien podría marcar esto como la respuesta, esto tiene más sentido para mí y parece haber un consenso sobre simplemente reevaluar el problema en sí.
Jonathan
También me gusta cómo lo dividió en pasos, por lo que hay algún tipo de procedimiento suelto para evaluar la situación.
Jonathan
OP, te veo atrapado en la mecánica de la solución de código, así que sí, "también podrías marcar esta respuesta". El mejor código que hemos escrito fue después de un análisis muy cuidadoso de los requisitos, los requisitos derivados, los diagramas de clase, las interacciones de clase, etc. Gracias a Dios, nos resistimos a las molestias de los asientos baratos (leer gerencia y compañeros de trabajo): "Usted estamos pasando demasiado tiempo en el diseño "," ¡apúrate y codifica! "," ¡son demasiadas clases! ", etc. Una vez que lo hicimos, la codificación fue una alegría, más que divertida. Objetivamente, ese fue el mejor código de todo el proyecto.
radarbob
13

Lo primero es lo primero: los patrones son abstracciones útiles, no el final, todo es diseño, y mucho menos el diseño OO.

En segundo lugar, la OO moderna es lo suficientemente inteligente como para saber que no todo es un objeto. A veces, el uso de funciones antiguas simples, o incluso algunos scripts de estilo imperativos, dará una mejor solución para ciertos problemas.

Ahora a la carne de las cosas:

Esto se vuelve muy difícil cuando cada enfoque parece tener tantos problemas como cualquiera de los otros.

¿Por qué? ¡Cuando tienes un montón de opciones similares, tu decisión debería ser más fácil! No perderá mucho eligiendo la opción "incorrecta". Y realmente, el código no es fijo. Pruebe algo, vea si es bueno. Iterar .

También es difícil aceptar que el código terminará siendo muy problemático independientemente de qué patrón de diseño o enfoque se use.

Nueces duras. Problemas difíciles: los problemas matemáticos reales son simplemente difíciles. Probablemente difícil. Literalmente no hay una buena solución para ellos. Y resulta que los problemas fáciles no son realmente valiosos.

Pero ten cuidado. Con demasiada frecuencia, he visto a personas frustradas por no tener buenas opciones porque están estancados mirando el problema de cierta manera, o porque han reducido sus responsabilidades de una manera que no es natural para el problema en cuestión. "No es una buena opción" puede ser un olor que hay algo fundamentalmente malo con su enfoque.

Esto resulta en una pérdida de motivación porque el código "limpio" deja de ser una opción a veces.

Lo perfecto es enemigo de lo bueno. Haga que algo funcione, luego refactorícelo.

¿Existen enfoques de diseño que puedan ayudar a minimizar este problema? ¿Existe una práctica aceptada para lo que se debe hacer en una situación como esta?

Como he mencionado, el desarrollo iterativo generalmente minimiza este problema. Una vez que algo funciona, está más familiarizado con el problema que lo coloca en una mejor posición para resolverlo. Tiene un código real para mirar y evaluar, no un diseño abstracto que no parece correcto.

Telastyn
fuente
Solo pensé que debería decir que he aclarado algunas malas interpretaciones en la edición. Generalmente hago el enfoque de "hacer que funcione" y refactorizar. Sin embargo, a menudo esto termina conduciendo a una gran deuda técnica en mi experiencia. Por ejemplo, si solo hago que algo funcione, pero luego yo u otros creamos, algunas de esas cosas pueden tener dependencias inevitables que también necesitan una tonelada de refactorización. Por supuesto, no siempre se puede saber esto de antemano. Pero si ya sabe que el enfoque es defectuoso, ¿debería uno hacerlo funcionar y luego refactorizar iterativamente antes de construir sobre el código?
Jonathan
@ Jonathan: todos los diseños son una serie de compensaciones. Sí, debe iterar de inmediato si el diseño es defectuoso y tiene una forma clara de mejorarlo. Si no hay una mejora clara, deja de joder.
Telastyn
"Han reducido sus responsabilidades de una manera que no es natural para el problema en cuestión. Ninguna buena opción puede ser el olor de que hay algo fundamentalmente malo en su enfoque". Apostaría a este. Algunos desarrolladores nunca se encuentran con los problemas descritos por OP y otros parecen hacerlo con bastante frecuencia. A menudo, la solución no es fácil de ver cuando la presenta el desarrollador original porque ya han sesgado el diseño en la forma en que plantean el problema. A veces, la única forma de encontrar la solución bastante obvia es comenzar desde los requisitos del diseño.
Dunk
8

La situación que está describiendo parece un enfoque de abajo hacia arriba. Toma una hoja, trata de arreglarla y descubre que está conectada a una rama que también está conectada a otra rama, etc.

Es como tratar de construir un automóvil comenzando con un neumático.

Lo que necesita es dar un paso atrás y mirar la imagen más grande. ¿Cómo se sienta esa hoja en el diseño general? ¿Sigue siendo actual y correcto?

¿Cómo se vería el módulo si lo diseñaras y lo implementaras desde cero? Cuán lejos de ese "ideal" está su implementación actual.

De esa manera, tiene una idea más amplia de hacia qué trabajar. (O si decide que es demasiado trabajo, cuáles son los problemas).

Pieter B
fuente
4

Su ejemplo describe una situación que surge típicamente con piezas más grandes de código heredado, y cuando intenta hacer una refactorización "demasiado grande".

El mejor consejo que puedo darle para esta situación es:

  • no intentes alcanzar tu objetivo principal en una "gran explosión" ,

  • ¡aprende a mejorar tu código en pasos más pequeños!

Por supuesto, eso es más fácil de escribir que de hacer, entonces, ¿cómo lograr esto en realidad? Bueno, necesita práctica y experiencia, depende mucho del caso, y no existe una regla estricta que diga "haga esto o aquello" que se ajuste a cada caso. Pero déjame usar tu ejemplo hipotético. "composición sobre herencia" no es un objetivo de diseño de todo o nada, es un candidato perfecto para lograrlo en varios pasos pequeños.

Digamos que notó que "composición sobre herencia" es la herramienta adecuada para el caso. Debe haber algunas indicaciones para que este sea un objetivo sensato, de lo contrario no habría elegido esto. Así que supongamos que hay una gran cantidad de funcionalidades en la superclase que simplemente se "llama" desde las subclases, por lo que esta funcionalidad es candidata para no permanecer en esa superclase.

Si observa que no puede eliminar inmediatamente la superclase de las subclases, puede comenzar primero refactorizando la superclase en componentes más pequeños que encapsulan la funcionalidad mencionada anteriormente. Comience con las frutas más bajas, primero extraiga algunos componentes más simples, lo que ya hará que su superclase sea menos compleja. Cuanto más pequeña sea la superclase, más fáciles serán las refactorizaciones adicionales. Utilice estos componentes de las subclases, así como de la superclase.

Si tiene suerte, el código restante en la superclase será tan simple a lo largo de este proceso que puede eliminar la superclase de las subclases sin más problemas. O notará que mantener la superclase ya no es un problema, ya que ya extrajo suficiente código en los componentes que desea reutilizar sin herencia.

Si no está seguro de por dónde comenzar, porque no sabe si una refactorización resultará simple, a veces el mejor enfoque es hacer una refactorización por rasguño .

Por supuesto, su situación real podría ser más complicada. Así que aprende, reúne experiencia y sé paciente, hacer esto bien lleva años. Aquí puedo recomendar dos libros, tal vez los encuentre útiles:

Doc Brown
fuente
1
Esto también es muy útil. La refactorización pequeña por pasos o desde cero es una buena idea porque luego se convierte en algo que se puede completar progresivamente junto con otro trabajo sin obstaculizarlo demasiado. ¡Ojalá pudiera aprobar múltiples respuestas!
Jonathan
1

A veces, los dos mejores principios de diseño son KISS * y YAGNI **. No sienta la necesidad de incluir cada patrón de diseño conocido en un programa que solo tenga que imprimir "¡Hola, mundo!".

 * Keep It Simple, Stupid
 ** You Ain't Gonna Need It

Edite después de la actualización de la pregunta (y refleje lo que Pieter B dice hasta cierto punto):

A veces tomas una decisión arquitectónica desde el principio que te lleva a un diseño particular que conduce a todo tipo de fealdad cuando intentas implementarlo. Desafortunadamente, en ese punto, la solución "adecuada" es dar un paso atrás y averiguar cómo llegaste a esa posición. Si aún no puede ver la respuesta, siga retrocediendo hasta que lo haga.

Pero si el trabajo para hacerlo sería desproporcionado, se necesita una decisión pragmática para encontrar la solución menos fea para el problema.

Simon B
fuente
He aclarado algo de esto en la edición, pero en general me estoy refiriendo exactamente a esos problemas en los que no hay una solución simple que esté disponible.
Jonathan
Ah bueno, sí, parece haber un consenso emergente sobre dar un paso atrás y reevaluar el problema. Esta es una buena idea.
Jonathan
1

Cuando estoy en esta situación, lo primero que hago es parar. Me cambio a otro problema y trabajo en eso por un tiempo. Tal vez una hora, tal vez un día, tal vez más. Esa no siempre es una opción, pero mi subconsciente trabajará en cosas mientras mi cerebro consciente hace algo más productivo. Finalmente, vuelvo a hacerlo con ojos nuevos e intento nuevamente.

Otra cosa que hago es preguntarle a alguien más inteligente que yo. Esto puede tomar la forma de preguntar en Stack Exchange, leer un artículo en la web sobre el tema o preguntarle a un colega que tenga más experiencia en esta área. A menudo, el método que creo que es el enfoque correcto resulta ser completamente incorrecto para lo que intento hacer. He caracterizado mal algunos aspectos del problema y, en realidad, no se ajusta al patrón que creo que sí. Cuando eso sucede, que alguien más diga: "Sabes, esto se parece más a ..." puede ser de gran ayuda.

Relacionado con lo anterior está el diseño o la depuración por confesión. Vas a un colega y le dices: "Te diré el problema que tengo y luego te explicaré las soluciones que tengo. Señalas los problemas en cada enfoque y sugieres otros enfoques. ". A menudo, incluso antes de que la otra persona hable, como estoy explicando, empiezo a darme cuenta de que un camino que parecía igual a los demás es en realidad mucho mejor o peor de lo que pensaba originalmente. La conversación resultante puede reforzar esa percepción o señalar cosas nuevas en las que no pensé.

Entonces TL; DR: Tómate un descanso, no lo fuerces, pide ayuda.

usuario1118321
fuente