Estoy trabajando en un proyecto que procesa solicitudes, y la solicitud tiene dos componentes: el comando y los parámetros. El controlador para cada comando es muy simple (<10 líneas, a menudo <5). Hay al menos 20 comandos y probablemente tendrá más de 50.
Se me ocurrieron algunas soluciones:
- un gran interruptor / if-else en comandos
- mapa de comandos a funciones
- mapa de comandos a clases estáticas / singletons
Cada comando realiza una pequeña comprobación de errores, y el único bit que puede extraerse es verificar la cantidad de parámetros, que se define para cada comando.
¿Cuál sería la mejor solución a este problema y por qué? También estoy abierto a cualquier patrón de diseño que pueda haber pasado por alto.
Se me ocurrió la siguiente lista pro / con para cada uno:
cambiar
- pros
- mantiene todos los comandos en una función; como son simples, esto lo convierte en una tabla de búsqueda visual
- no es necesario abarrotar la fuente con toneladas de pequeñas funciones / clases que solo se usarán en un lugar
- contras
- muy largo
- difícil de agregar comandos mediante programación (es necesario encadenar usando el caso predeterminado)
comandos de mapa -> función
- pros
- trozos pequeños y del tamaño de un bocado
- puede agregar / eliminar comandos mediante programación
- contras
- si se hace en línea, lo mismo visualmente que el interruptor
- si no se hace en línea, muchas funciones solo se usan en un lugar
comandos de mapa -> clase estática / singleton
- pros
- puede usar polimorfismo para manejar la verificación de errores simple (solo como 3 líneas, pero aún así)
- beneficios similares al mapa -> solución de función
- contras
- muchas clases muy pequeñas desordenarán el proyecto
- la implementación no está en el mismo lugar, por lo que no es tan fácil escanear implementaciones
Notas adicionales:
Estoy escribiendo esto en Go, pero no creo que la solución sea específica del idioma. Estoy buscando una solución más general porque es posible que deba hacer algo muy similar en otros idiomas.
Un comando es una cadena, pero puedo asignarlo fácilmente a un número si es conveniente. La firma de la función es algo así como:
Reply Command(List<String> params)
Go tiene funciones de nivel superior, y otras plataformas que estoy considerando también tienen funciones de nivel superior, de ahí la diferencia entre las opciones segunda y tercera.
fuente
Respuestas:
Este es un gran ajuste para un mapa (segunda o tercera solución propuesta). Lo he usado docenas de veces, y es simple y efectivo. Realmente no distingo entre estas soluciones; El punto importante es que hay un mapa con nombres de funciones como las teclas.
La principal ventaja del enfoque del mapa, en mi opinión, es que la tabla son datos. Esto significa que se puede pasar, aumentar o modificar de otra manera en tiempo de ejecución; También es fácil codificar funciones adicionales que interpretan el mapa de formas nuevas y emocionantes. Esto no sería posible con la solución case / switch.
Realmente no he experimentado los inconvenientes que menciona, pero me gustaría mencionar un inconveniente adicional: el envío es fácil si solo importa el nombre de la cadena, pero si necesita tener en cuenta información adicional para decidir qué función ejecutar , es mucho menos limpio.
Tal vez nunca me he encontrado con un problema lo suficientemente difícil, pero veo poco valor tanto en el patrón de comando como en la codificación del despacho como una jerarquía de clases, como otros han mencionado. El núcleo del problema es asignar solicitudes a funciones; un mapa es simple, obvio y fácil de probar. Una jerarquía de clases requiere más código y diseño, aumenta el acoplamiento entre ese código y puede obligarlo a tomar más decisiones por adelantado que más tarde necesitará cambiar. No creo que el patrón de comando se aplique en absoluto.
fuente
Su problema se presta muy bien al patrón de diseño de Comando . Entonces, básicamente, tendrá una
Command
interfaz base y luego habrá variasCommandImpl
clases que implementarán esa interfaz. La interfaz esencialmente necesita tener un único métododoCommand(Args args)
. Puede pasar los argumentos a través de una instancia deArgs
clase. De esta manera, aprovecha el poder del polimorfismo en lugar de las declaraciones torpes if / else. Además, este diseño es fácilmente extensible.fuente
Cada vez que me pregunto si debo usar una declaración de cambio o un polimorfismo de estilo OO, me refiero al problema de expresión . Básicamente, si tiene diferentes "casos" para sus datos y una varita para admitir diferentes "acciones" (donde cada acción hace algo diferente para cada caso), entonces es realmente difícil crear un sistema que, naturalmente, le permita agregar nuevos casos y nuevas acciones en el futuro.
Si usa declaraciones de cambio (o el patrón Visitante), entonces es fácil agregar nuevas acciones (porque elimina todo en una sola función) pero es difícil agregar nuevos casos (porque necesita regresar y editar las funciones antiguas)
Por el contrario, si usa el polimorfismo de estilo OO, es fácil agregar nuevos casos (solo crear una nueva clase) pero difícil de agregar métodos a una interfaz (porque entonces necesita volver atrás y editar un montón de clases)
En su caso, solo tiene un método que necesita admitir (procesar una solicitud) pero muchos casos posibles (cada comando diferente). Dado que es más fácil agregar nuevos casos es más importante que agregar nuevos métodos, simplemente cree una clase separada para cada comando diferente.
Por cierto, desde el punto de vista de cuán extensibles son las cosas, no hace una gran diferencia si usas clases o funciones. Si estamos comparando con una declaración de cambio, lo que importa es cómo se "despachan" las cosas y tanto las clases como las funciones se despachan de la misma manera. Por lo tanto, solo use lo que sea más conveniente en su idioma (y dado que Go tiene un alcance y cierre léxico, la distinción entre clases y funciones es realmente muy pequeña).
Por ejemplo, generalmente puede usar la delegación para hacer la parte de verificación de errores en lugar de confiar en la herencia (mi ejemplo está en Javascript porque O no sé Sintaxis de Go, espero que no le importe)
Por supuesto, este ejemplo supone que hay una manera sensata de hacer que la función de contenedor o la clase padre verifique los argumentos para cada caso. Dependiendo de cómo vayan las cosas, podría ser más simple poner la llamada a check_arguments dentro de los comandos mismos (ya que cada comando puede necesitar llamar a la función de verificación con diferentes argumentos, debido a un número diferente de argumentos, diferentes tipos de comandos, etc.)
tl; dr: No existe la mejor manera de resolver todos los problemas. Desde la perspectiva de "hacer que las cosas funcionen", concéntrese en crear sus abstracciones de manera que impongan invariantes importantes y lo protejan de los errores. Desde una perspectiva "a prueba de futuro", tenga en cuenta qué partes del código tienen más probabilidades de ampliarse.
fuente
Nunca he usado go, como programador ac # probablemente iría a la siguiente línea, espero que esta arquitectura se ajuste a lo que estás haciendo.
Crearía una pequeña clase / objeto para cada uno con la función principal para ejecutar, cada uno debe conocer su representación de cadena. Esto le brinda la capacidad de conexión que suena como lo desea, ya que aumentará el número de funciones. Tenga en cuenta que no usaría estática a menos que realmente lo necesite, no ofrecen una gran ventaja.
Entonces tendría una fábrica que sabe cómo descubrir estas clases en tiempo de ejecución, modificándolas para que se carguen desde diferentes ensamblajes, etc. Esto significa que no las necesita todas en el mismo proyecto.
Por lo tanto, también lo hace más modular para las pruebas y debería hacer que el código sea agradable y pequeño, lo que es más fácil de mantener más adelante.
fuente
¿Cómo seleccionas tus comandos / funciones?
Si hay alguna forma "inteligente" de seleccionar la función correcta, entonces ese es el camino a seguir: significa que puede agregar nuevo soporte (tal vez en una biblioteca externa) sin modificar la lógica central.
Además, probar funciones individuales es más fácil de forma aislada que una declaración de cambio enorme.
Finalmente, solo se usa en un lugar: ¿puede encontrar que una vez que llegue a 50 que diferentes bits de diferentes funciones puedan reutilizarse?
fuente
No tengo idea de cómo funciona Go, pero una arquitectura que he usado en ActionScript es tener una Lista Doblemente Vinculada que actúa como una Cadena de Responsabilidad. Cada enlace tiene una función determinar Responsabilidad (que implementé como devolución de llamada, pero podría escribir cada enlace por separado si eso funciona mejor para lo que está tratando de hacer). Si un enlace determinaba que tenía responsabilidad, llamaría a meetResponibility (nuevamente, una devolución de llamada), y eso terminaría la cadena. Si no tuviera responsabilidad, pasaría la solicitud al siguiente eslabón de la cadena.
Se pueden agregar y eliminar fácilmente diferentes casos de uso simplemente agregando un nuevo enlace entre los enlaces de (o al final de) la cadena existente.
Esto es similar, pero sutilmente diferente, a su idea de un mapa de funciones. Lo bueno es que simplemente pasa la solicitud y hace lo suyo sin tener que hacer nada más. El inconveniente es que no funciona bien si necesita devolver un valor.
fuente