Refactorización de lógica de prorrateo de descuentos mediante Java Streams

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 ifPresent asegura 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.

Etiquetas: java Streams API Lambda Expressions Refactoring Bigdecimal

Publicado el 6-5 16:46