En el desarrollo de sistemas de facturación, es común encontrarse con la necesidad de procesar líneas de descuento globales que deben distribuirse proporcionalmente entre el resto de los artículos de un comprobante. A continuación, se analiza una transición desde un enfoque imperativo tradicional hacia una implementación funcional más limpia y eficiente utilizando la API de Streams de Java.
Implementación inicial: Enfoque imperativo
El siguiente fragmento de código muestra una lógica basada en bucles for y estados mutables para identificar una línea de descuento, calcular el volumen total de productos y aplicar el ajuste correspondiente a cada partida.
// Verificación inicial de existencia de descuentos
boolean tieneAjuste = false;
for (DetalleFactura item : listaDetalles) {
if (item.getDescripcion().contains("LINEA_DESCUENTO")) {
tieneAjuste = true;
break;
}
}
if (tieneAjuste) {
BigDecimal totalUnidades = BigDecimal.ZERO;
DetalleFactura lineaReferencia = null;
// Primer recorrido para consolidar cantidades y localizar el descuento
for (DetalleFactura item : listaDetalles) {
if (!item.getDescripcion().contains("LINEA_DESCUENTO")) {
totalUnidades = totalUnidades.add(new BigDecimal(item.getCantidad()));
} else {
lineaReferencia = item;
}
}
// Cálculo de factores de prorrateo
BigDecimal factorIva = new BigDecimal(lineaReferencia.getImpuesto()).divide(totalUnidades, 2, RoundingMode.HALF_UP);
BigDecimal factorBase = new BigDecimal(lineaReferencia.getBaseImponible()).divide(totalUnidades, 2, RoundingMode.HALF_UP);
BigDecimal factorTotal = new BigDecimal(lineaReferencia.getImporteTotal()).divide(totalUnidades, 2, RoundingMode.HALF_UP);
// Segundo recorrido para aplicar el ajuste proporcional
for (DetalleFactura item : listaDetalles) {
if (!item.getDescripcion().contains("LINEA_DESCUENTO")) {
BigDecimal unidades = new BigDecimal(item.getCantidad());
item.setBaseImponible(new BigDecimal(item.getBaseImponible()).add(factorBase.multiply(unidades)).toString());
item.setImpuesto(new BigDecimal(item.getImpuesto()).add(factorIva.multiply(unidades)).toString());
item.setImporteTotal(new BigDecimal(item.getImporteTotal()).add(factorTotal.multiply(unidades)).toString());
}
}
// Limpieza de la lista
listaDetalles.removeIf(it -> it.getDescripcion().contains("LINEA_DESCUENTO"));
}
Implementación optimizada: Enfoque declarativo
Mediante el uso de Optional y la API de Streams, podemos reducir la complejidad ciclomática y mejorar la legibilidad del código. Esta versión utiliza reduce para la agregación de valores y un procesamiento paralelo para la actualización de los objetos.
// Localización elegante mediante Optional
listaDetalles.stream()
.filter(linea -> linea.getDescripcion().contains("LINEA_DESCUENTO"))
.findFirst()
.ifPresent(descuento -> {
// Cálculo del denominador común mediante reducción
BigDecimal totalQty = listaDetalles.stream()
.filter(l -> !l.getDescripcion().contains("LINEA_DESCUENTO"))
.map(l -> new BigDecimal(l.getCantidad()))
.reduce(BigDecimal.ZERO, BigDecimal::add);
// Definición de ratios de distribución
BigDecimal ratioBase = new BigDecimal(descuento.getBaseImponible()).divide(totalQty, 2, RoundingMode.HALF_UP);
BigDecimal ratioTax = new BigDecimal(descuento.getImpuesto()).divide(totalQty, 2, RoundingMode.HALF_UP);
BigDecimal ratioTotal = new BigDecimal(descuento.getImporteTotal()).divide(totalQty, 2, RoundingMode.HALF_UP);
// Aplicación del ajuste mediante procesamiento paralelo
listaDetalles.parallelStream()
.filter(l -> !l.getDescripcion().contains("LINEA_DESCUENTO"))
.forEach(l -> {
BigDecimal qty = new BigDecimal(l.getCantidad());
l.setBaseImponible(new BigDecimal(l.getBaseImponible()).add(ratioBase.multiply(qty)).toString());
l.setImpuesto(new BigDecimal(l.getImpuesto()).add(ratioTax.multiply(qty)).toString());
l.setImporteTotal(new BigDecimal(l.getImporteTotal()).add(ratioTotal.multiply(qty)).toString());
});
// Remoción de la línea de ajuste procesada
listaDetalles.removeIf(l -> l.getDescripcion().contains("LINEA_DESCUENTO"));
});
Ventajas de la refactorización
La transición a un modelo basado en Streams ofrece beneficios tangibles en el mantenimiento del software:
- Inmutabilidad y Claridad: Se elimina la necesidad de banderas booleanas (
tieneAjuste) y punteros nulos manuales. - Concisión: El uso de
ifPresentasegura que la lógica solo se ejecute si existe un descuento, encapsulando el comportamiento en un bloque cohesivo. - Rendimiento: El uso de
parallelStream()permite que, en colecciones extensas de líneas de factura, la aplicación de los cálculos aproveche múltiples núcleos del procesador.