java - mvc - Patrón de estrategia con diferentes parámetros
spring security module (4)
Me encontré con un problema cuando usé el patrón de estrategia. Estoy implementando un servicio para crear tareas. Este servicio también resuelve el empleado responsable de esta tarea. La resolución del empleado se realiza mediante el patrón de estrategia, ya que hay diferentes formas de hacerlo. El punto es que cada estrategia podría necesitar diferentes parámetros para resolver el empleado.
Por ejemplo:
interface ClerkResolver {
String resolveClerk(String department);
}
class DefaultClerkResolver implements ClerkResolver {
public String resolveClerk(String department) {
// some stuff
}
}
class CountryClerkResolver implements ClerkResolver {
public String resolveClerk(String department) {
// I do not need the department name here. What I need is the country.
}
}
El problema es que cada resolver puede depender de diferentes parámetros para resolver el empleado responsable. Para mí, esto suena como un problema de diseño en mi código. También traté de tener una clase como parámetro para mantener todos los valores que las estrategias podrían necesitar, como:
class StrategyParameter {
private String department;
private String country;
public String getDepartment() ...
}
interface ClerkResolver {
String resolveClerk(StrategyParameter strategyParameter);
}
Pero para ser sincero, no estoy satisfecho con esta solución porque tengo que cambiar la clase de parámetros cada vez que una estrategia necesita un argumento nuevo / diferente. Y en segundo lugar, el que llama de la estrategia debe establecer todos los parámetros porque no sabe qué estrategia resolverá el empleado, por lo tanto, tiene que proporcionar todos los parámetros (pero esto no es tan malo).
Nuevamente, para mí esto suena como un problema de diseño en mi código, pero no puedo encontrar una mejor solución.
--- EDITAR
El principal problema con esta solución es al crear la tarea. El servicio de tareas se ve así:
class TaskService {
private List<ClerkResolver> clerkResolvers;
Task createTask(StrategyParamter ...) {
// some stuff
for(ClerkResolver clerkResolver : clerkResolvers) {
String clerk = clerkResolver.resolveClerk(StrategyParameter...)
...
}
// some other stuff
}
}
Como puede ver cuando se utiliza TaskService, el que llama debe proporcionar la información necesaria para resolver el empleado, es decir, el nombre del departamento y / o el país, porque TaskService en sí no tiene esta información.
Cuando se debe crear una tarea, la persona que llama debe proporcionar el parámetro de estrategia, ya que son necesarios para resolver el empleado. De nuevo, el problema es que la persona que llama no tiene toda la información, es decir, no tiene conocimiento del país. Él solo puede establecer el nombre del departamento. Es por eso que agregué un segundo método a la interfaz para asegurarme de que la estrategia pueda manejar la resolución del empleado:
interface ClerkResolver {
String resolveClerk(StrategyParameter strategyParameter);
boolean canHandle(StrategyParameter strategyParameter);
}
A riesgo de repetirme, esta solución no me suena bien.
Entonces, si alguien tiene una mejor solución para este problema, agradecería escucharlo.
¡Gracias por tus comentarios!
Comencemos suponiendo que su código se basa en bloques simples if-else-if.
En tal escenario, aún necesitará tener todas las entradas requeridas por adelantado. No hay forma de evitarlo.
Al usar el patrón de estrategia, comienzas a desacoplar tu código, es decir, defines la interfaz base y la implementación concreta.
Simplemente tener este diseño no es lo suficientemente bueno, porque aún necesita tener un bloque if-else-if.
En este punto, puede ver los siguientes cambios de diseño:
Use un patrón de fábrica para cargar todas las estrategias disponibles de este sistema. Esto podría basarse en metainformación, como el patrón del cargador de servicio que está disponible en el JDK.
Identifique una estrategia mediante la cual puede consultar las implementaciones disponibles para averiguar si pueden manejar el conjunto de parámetros de entrada dado. Esto puede ser tan simple como canYouResolve (input)! = Null . Al hacer esto, cambiamos de un bloque if-else-if a un ciclo for-each.
En su caso, también tiene una Implementación predeterminada . Por lo tanto, digamos que la implementación predeterminada es parte de su módulo y las otras estrategias provienen de las otras jarras (que se cargan a través del ServiceLoader desde el punto 1).
Cuando se activa su código, primero busca todas las estrategias disponibles; pregúnteles si pueden manejar el escenario actual; si ninguno de ellos puede manejarlo, entonces use la implementación predeterminada.
Si por alguna razón, tiene más de un resolutor capaz de manejar una entrada particular, debería considerar definir una prioridad para esos resolutores.
Ahora, al llegar a los parámetros de entrada, ¿pueden estos parámetros derivarse de algún objeto de entrada? Si es así, ¿por qué no enviar ese objeto de entrada al resolver?
Nota: Esto es muy similar a cómo funciona el ELEEVolver de JavaEE: en ese caso, el código marca el EL como resuelto, informando así a la clase raíz que la resolución está completa.
Nota: Si cree que el cargador de servicio es demasiado pesado, busque buscar META-INF / algún archivo que le guste para identificar los resolvedores que están disponibles en el sistema.
Desde mi propia experiencia, la mayoría de las veces, terminas escribiendo código que mezcla patrones para lograr el caso de uso en cuestión.
Espero que esto ayude a tu escenario.
Creo que hay cierta confusión acerca de qué es la tarea en realidad. En mi opinión, una tarea es algo que hace un empleado. Por lo tanto, puede crear una tarea sin saber nada de un empleado.
En función de esa tarea, puede elegir un empleado adecuado para ello. La asignación de la tarea al empleado puede ser envuelta en algún otro tipo de tarea. Entonces, una interfaz común para elegir un empleado sería:
interface ClerkResolver {
String resolveClerk(Task task);
}
Para implementar este tipo de resolver de empleado, puede usar el patrón de estrategia basado en el tipo real de la tarea, por ejemplo.
Dado que Java está tipado estáticamente, una buena forma de simular objetos dinámicos es mediante el uso de un Mapa. Haría esto para pasar parámetros dinámicos a mis resolutores:
class StrategyParameter extends Map {}
// Map could be used directly, but this make the code more readable
Entonces, mi patrón de estrategia se convierte en: interface ClerkResolver {String resolveClerk (StrategyParameter strategyParameter); }
class DefaultClerkResolver implements ClerkResolver {
public String resolveClerk(StrategyParameter strategyParameter) {
// strategyParameter.get("department");
}
}
class CountryClerkResolver implements ClerkResolver {
public String resolveClerk(StrategyParameter strategyParameter) {
// strategyParameter.get("country");
}
}
Me gustó mucho la sugerencia de SpaceTrucker, que a veces los problemas se resuelven moviendo la abstracción a otro nivel :)
Pero si su diseño original tiene más sentido (que solo usted puede decir, según su percepción de la especificación), en mi humilde opinión, uno puede: 1) Mantener su enfoque de "cargar todo en StrategyParameter" 2) O trasladar esta responsabilidad al Estrategia
Para la opción (2), supongo que hay alguna entidad común (¿cuenta? ¿Cliente?) De la cual se puede deducir el departamento / país. Luego tiene "CountryClerkResolver.resolveClerk (String accountId)" que buscaría el país.
En mi humilde opinión (1), (2) son legítimos, dependiendo del contexto. A veces (1) funciona para mí, porque todos los parámetros (departamento + país) son baratos de precargar. A veces incluso logro reemplazar el ''StrategyParameter'' sintético por una entidad intuitiva para el negocio (por ejemplo, Account). A veces (2) funciona mejor para mí, por ejemplo, si ''departamento'' y ''país'' requieren búsquedas separadas y costosas. Se nota especialmente con params complejos: por ejemplo, si una estrategia selecciona a los empleados en función de sus puntajes en las revisiones de "satisfacción del cliente", esa es una estructura compleja que no se debe cargar para estrategias más simples.