Nos encontramos frecuentemente con código de negocio que directamente utiliza sentenciasif para validar parámetros básicos. Aunque utilicemos retornos anticipados o sentencias de guarda (básicamente, retornar directamente cuando las condiciones no se cumplen para reducir el anidamiento de if), todo el código de negocio puede volverse extenso debido a estas verificaciones básicas de parámetros.
Cadena de Responsabilidad
Ventajas de la Cadena de Responsabilidad / Problemas en el código actual
- Responsabilidad única: El patrón Cadena de Responsabilidad permite encapsular cada lógica de validación en un procesador independiente, donde cada procesador se encarga de una sola responsabilidad de validación, cumpliendo con el principio de responsabilidad única
- Extensibilidad: Al añadir nueva lógica de validación solo necesitamos agregar nuevos procesadores sin modificar el código existente; para modificar la lógica de verificación solo necesitamos cambiar el procesador correspondiente
- Flujo claro: Toda la lógica de validación se organiza juntos, estructura de código más clara y fácil de entender
¿Qué es la Cadena de Responsabilidad?
Un patrón de diseño de comportamiento que permite pasar una solicitud a través de una cadena de manejadores. La solicitud se procesa dentro de la cadena hasta que algún objeto la maneje. De esta manera, el emisor no necesita saber qué objeto manejará la solicitud, todos los objetos manejadores pueden intentar procesarla o pasarla al siguiente objeto en la cadena.
Modificar el código correspondiente a (verificar si los datos cumplen con la lógica básica, relaciones de dependencia entre campos, corrección de los datos) para convertirlo en manejadores.
Lógica completa de la Cadena de Responsabilidad
- Crear una interfaz abstracta para la cadena de responsabilidad
- Implementar la lógica de procesamiento de pasos de la cadena mediante la interfaz abstracta
- Las clases de manejo heredan la interfaz de la cadena de responsabilidad e implementan la encapsulación interna de la lógica
- Colocar un grupo de cadenas de responsabilidad en un contenedor
Demostración de código:
Interfaz de la cadena de responsabilidad:
public interface ProveedorValidacionCadena<T> extends Ordenable {
/**
* Ejecutar la lógica de la cadena de responsabilidad
*
* @param parametroSolicitud Parámetros de entrada para la ejecución de la cadena
*/
void procesar(T parametroSolicitud);
/**
* @return Identificador del componente de la cadena de responsabilidad
*/
String identificador();
}
"Ejecutor" de la cadena de responsabilidad
@Component
public class ContextoCadenaProveedor<T> implements ConfiguracionAplicacion, InicioLineaComando {
/**
* Contexto de aplicación
* Obtener instancias de beans a través del contenedor IoC
*/
private ConfiguracionAplicacion contextoAplicacion;
/**
* Guardar implementaciones de cadenas de responsabilidad mediante HashMap
* Clave: {@link ProveedorValidacionCadena#identificador()}
* Valor: {@link ProveedorValidacionCadena} Colección de implementaciones de cadenas de responsabilidad Spring Bean
* <p>
* Ej: Cadena de creación de plantilla de cupones:
* Clave:CLAVE_CADENA_CREACION_PLANTILLA_PROVEEDOR
* Valor:
* - Verificar si los parámetros básicos del cupón son obligatorios —— Ejecutor {@link FiltroCadenaParametroNoNuloPlantillaCupon}
* - Verificar si los parámetros básicos del cupón se pasan según formato —— Ejecutor {@link FiltroCadenaVerificacionBasicaParametroPlantillaCupon}
* - Verificar si los parámetros básicos del cupón son correctos, por ejemplo, si existen los datos del producto —— Ejecutor {@link FiltroCadenaVerificacionBasicaParametroPlantillaCupon}
*/
private final Map<String, List<ProveedorValidacionCadena>> contenedorCadenaProveedor = new HashMap<>();
/**
* Ejecución del componente de la cadena de responsabilidad
*
* @param identificador Identificador del componente de la cadena de responsabilidad
* @param parametroSolicitud Parámetros de solicitud
*/
public void procesar(String identificador, T parametroSolicitud) {
//Obtener colección de beans de implementación de cadena de responsabilidad desde el contenedor según identificador
List<ProveedorValidacionCadena> proveedoresCadena = contenedorCadenaProveedor.get(identificador);
if (ColeccionUtil.esVacia(proveedoresCadena)) {
throw new RuntimeException(String.format("[%s] ID de Cadena de Responsabilidad no definido.", identificador));
}
proveedoresCadena.forEach(cada -> cada.procesar(parametroSolicitud));
}
@Override
public void ejecutar(String... argumentos) throws Exception {
//Obtener del contenedor IoC la colección de beans del tipo de interfaz especificado (aquí la interfaz correspondiente al procesador)
//String corresponde al nombre de clase (primera letra minúscula)
Map<String, ProveedorValidacionCadena> mapaFiltroCadena = contextoAplicacion.getBeanPorTipo(ProveedorValidacionCadena.class);
mapaFiltroCadena.forEach(
//El método forEach de Map requiere un parámetro de tipo BiConsumidor<k,v>, esta es una interfaz funcional con un método abstracto void aceptar (correspondiente a clave, valor del Map) (expresión lambda abajo)
(nombreBean, bean) -> {
//Verificar si el identificador existe en la cadena de creación de plantillas, si existe directamente agregar, si no crear el identificador y conjunto correspondiente
List<ProveedorValidacionCadena> proveedoresCadena = contenedorCadenaProveedor.obtenerPredeterminado(bean.identificador(), new ArrayList<>());
proveedoresCadena.add(bean);
//Devolver el conjunto de procesadores al contenedor
contenedorCadenaProveedor.put(bean.identificador(), proveedoresCadena);
});
//Ordenar según implementación de cadena de responsabilidad, prioridad menor va primero (más rápido en ejecutarse)
contenedorCadenaProveedor.forEach(
//Igual que arriba
(identificador, manejadoresCadenaSinOrdenar) -> {
manejadoresCadenaSinOrdenar.sort(
//Usar método estático para crear comparador en lugar de new Comparador()
//Pero esta función recibe una función como parámetro, cuya función: extraer la clave utilizada para ordenar desde el objeto comparado
//Requisito: la clase superior/correspondiente implementa la interfaz Ordenable
Comparador.comparando(Ordenable::obtenerOrden));
// (o1,o2)->return o1.valor-o2.valor;
});
}
@Override
public void configurarConfiguracionAplicacion(ConfiguracionAplicacion contextoAplicacion) throws ExcepcionBeans {
this.contextoAplicacion = contextoAplicacion;
}
}
Ejemplo de procesador de cadena de responsabilidad:
/**
* Validar parámetros de interfaz de creación de cupones responsabilidad cadena|validar relación básica de datos de parámetros sea correcta
*/
@Component
public class FiltroCadenaVerificacionBasicaParametroPlantillaCupon implements ProveedorValidacionCadena<DtoGuardarPlantillaCuponSolicitud> {
private final int stockMaximo = 20000000;
@Override
public void procesar(DtoGuardarPlantillaCuponSolicitud parametroSolicitud) {
boolean coincideAlguno = Arrays.stream(EnumObjetivoDescuento.valores())
.cualquieraCoincide(constanteEnum -> constanteEnum.tipo() == parametroSolicitud.objetivo());
if (!coincideAlguno) {
// Aquí ya podemos determinar que la solicitud de datos probablemente sea ataque malicioso, se puede reportar al centro de control de riesgos para prohibir cuenta
throw new ExcepcionCliente("Valor de objeto de descuento no existe");
}
if (ObjetoUtil.igual(parametroSolicitud.objetivo(), EnumObjetivoDescuento.TODA_TIENDA_GENERAL.tipo())
&& UtilCadena.noEstaVacia(parametroSolicitud.bienes())) {
throw new ExcepcionCliente("Cupón universal para toda tienda no puede establecer productos específicos");
}
if (ObjetoUtil.igual(parametroSolicitud.objetivo(), EnumObjetivoDescuento.PRODUCTO_ESPECIFICO.tipo())
&& UtilCadena.estaVacia(parametroSolicitud.bienes())) {
throw new ExcepcionCliente("Cupón específico para producto no tiene producto específico establecido");
}
boolean tipoCoincideAlguno = Arrays.stream(EnumTipoDescuento.valores())
.cualquieraCoincide(constanteEnum -> constanteEnum.tipo() == parametroSolicitud.tipo());
if (!tipoCoincideAlguno) {
// Aquí ya podemos determinar que la solicitud de datos probablemente sea ataque malicioso, se puede reportar al centro de control de riesgos para prohibir cuenta
throw new ExcepcionCliente("Tipo de descuento no existe");
}
Fecha ahora = new Fecha();
if (parametroSolicitud.tiempoInicioValido().antes(ahora)) {
// Para facilitar pruebas, ignoraremos esta hora, aquí cancelamos lanzamiento de excepción
// throw new ExcepcionCliente("Tiempo inicio de validez no puede ser anterior a tiempo actual");
}
if (parametroSolicitud.stock() <= 0 || parametroSolicitud.stock() > stockMaximo) {
// Aquí ya podemos determinar que la solicitud de datos probablemente sea ataque malicioso, se puede reportar al centro de control de riesgos para prohibir cuenta
throw new ExcepcionCliente("Cantidad de stock configurada anormalmente");
}
if (!JSON.esValido(parametroSolicitud.reglaRecibir())) {
// Aquí ya podemos determinar que la solicitud de datos probablemente sea ataque malicioso, se puede reportar al centro de control de riesgos para prohibir cuenta
throw new ExcepcionCliente("Formato de regla de recepción incorrecto");
}
if (!JSON.esValido(parametroSolicitud.reglaConsumir())) {
// Aquí ya podemos determinar que la solicitud de datos probablemente sea ataque malicioso, se puede reportar al centro de control de riesgos para prohibir cuenta
throw new ExcepcionCliente("Formato de regla de consumo incorrecto");
}
}
@Override
public String identificador() {
return CLAVE_CADENA_CREACION_PLANTILLA_PROVEEDOR.nombre();
}
@Override
//El contexto de cadena de responsabilidad ordena de menor a mayor según este valor
public int obtenerOrden() {
return 8;
}
}
Llamada a la cadena de responsabilidad para procesamiento dentro del servicio:
public void crearPlantillaCupon(DtoGuardarPlantillaCuponSolicitud parametroSolicitud) {
//Validar parámetros sean correctos mediante cadena de responsabilidad
//Primer parámetro corresponde al identificador del procesador de cadena de responsabilidad
contextoCadenaProveedor.procesar(CLAVE_CADENA_CREACION_PLANTILLA_PROVEEDOR.nombre(), parametroSolicitud);
//Código de negocio
}
Preguntas comunes:
- ¿Por qué necesitamos la interfaz Ordenable?
- La implementación de la cadena de responsabilidad tendrá múltiples procesadores, para el negocio de creación de cupones se deben procesar primero los que tengan mejor rendimiento, luego validar progresivamente
- ¿La cadena de responsabilidad cumple con el principio abierto-cerrado?
- Se obtienen los procesadores de cadena de responsabilidad mediante el contenedor SpringIoC, ya sea para añadir o eliminar no se necesita cambiar la lógica de obtención. Para añadir simplemente creamos el procesador correspondiente, cumpliendo con el principio abierto-cerrado
Principio Abierto-Cerrado
Uno de los principios de diseño fundamentales en programación orientada a objetos. Indica que: una entidad de software (clase, módulo, función) debería estar abierta para extensión pero cerrada para modificación, lo que significa poder añadir nuevas funciones sin modificar el código existente.
Idea principal
Construir marcos mediante abstracción, extender detalles medainte implementación. Cuando el software necesita cambios, se debe intentar lograrlo mediante extensión del comportamiento del código existente en lugar de modificar el código ya escrito.