P4 — Reportes

Publicación: semana 12 · Entrega: semana 15 · En parejas · Reglas generales de entrega: projects/README.md.

Con P3, Bodega registra ventas. Ahora doña Rosa quiere saber: cuánto vendió cada día, qué categoría deja más, cuáles son sus productos estrella, quiénes son sus caseros (los clientes que vuelven). Son consultas sobre listas de ventas, y se escriben con lambdas y streams.

La segunda mitad de la entrega no agrega funciones: mejora lo que ya existe. Después de cuatro entregas, el código de P1 a P3 tiene partes que costó escribir y cuestan cambiar. Con las pruebas como red de seguridad, van a refactorizarlo aplicando los principios SOLID, sin cambiar lo que hace y sin tocar ninguna prueba pública.

Objetivos

  • Escribir consultas con lambdas, referencias a métodos y streams (filter, map, flatMap, sorted, limit, collect, groupingBy).
  • Devolver Optional cuando un resultado puede no existir.
  • Diseñar métodos que reciben comportamiento (Predicate, Comparator).
  • Reconocer code smells y los principios que violan, y refactorizar en pasos pequeños con las pruebas como red de seguridad.

Lo que necesitan de las semanas 12 a 14

Parte A: reportes

1. ProductTotal

public record ProductTotal(String code, String name, long cents) {}

Lo que se vendió de un producto, en céntimos, en un conjunto de ventas. Viene completo en los archivos provistos.

2. SalesReport

Un reporte sobre una lista de ventas. No cambia: cada consulta devuelve un resultado nuevo. Todos los montos son los totales con descuento (totalCents() de la venta o de la línea).

Método Qué devuelve
SalesReport(List<Sale> sales) Guarda una copia de la lista: si después cambia la original, el reporte no cambia.
int count() Cuántas ventas hay.
long totalCents() La suma de los totales; 0 si no hay ventas.
SalesReport between(LocalDate from, LocalDate to) Un reporte nuevo con las ventas cuya fecha está entre from y to, ambos incluidos, en el mismo orden. Si from es posterior a to: IllegalArgumentException.
Map<LocalDate, Long> totalByDay() Total por día, ordenado por fecha de la más antigua a la más reciente. Solo aparecen los días con ventas.
Map<String, Long> totalByCategory() Total de las líneas por categoría, ordenado por categoría (orden de String). La suma de los valores es totalCents().
List<ProductTotal> topProducts(int n) Los n productos (por código) que más dinero dieron, de mayor a menor; si empatan, por código. Si hay menos de n, todos; n = 0 da una lista vacía; n negativo: IllegalArgumentException. El nombre es el de la primera línea de ese código en las ventas.
long quantitySold(String code) La suma de las cantidades vendidas de ese código (unidades, o gramos si es a granel); 0 si no se vendió.
Optional<Sale> largestSale() La venta de mayor total; si hay empate, la primera de la lista. Vacío si no hay ventas.
Set<String> frequentCustomers(int minSales) Los clientes con al menos minSales ventas, en orden alfabético, sin el anónimo "". minSales menor que 1: IllegalArgumentException.

Para que el orden de los mapas quede garantizado, recolecten en un TreeMap: Collectors.groupingBy(clave, TreeMap::new, Collectors.summingLong(...)). Un HashMap puede devolver las claves en cualquier orden, y las pruebas privadas lo notarán.

Escriban cada reporte con streams. Si alguno queda más claro con un bucle (ver cuándo no usar streams), pueden dejarlo así y explicar por qué en el README. Un buen ejercicio, como en el laboratorio de la semana 12: escribir primero la versión con bucles, con su prueba, y después reemplazarla.

3. Métodos que reciben lambdas

Método nuevo Qué hace
List<Product> Inventory.filter(Predicate<? super Product> condition) Los productos que cumplen la condición, en orden de inserción, en una lista no modificable.
List<Sale> SalesHistory.filter(Predicate<? super Sale> condition) Las ventas que cumplen la condición, en orden de registro, no modificable.

Además, lo que ya escribieron debe aceptar lambdas sin cambios, y las pruebas públicas lo verifican:

  • Inventory.productsSortedBy con comparadores como Comparator.comparingLong(Product::priceCents).reversed().
  • Promotions.add con un descuento escrito como lambda, (p, q) -> ... : por eso Discount tenía un solo método abstracto (P2).

Con filter a mano, lowStock, productsIn y salesOn se pueden escribir en una línea. Háganlo: es parte del refactoring.

Parte B: refactoring con SOLID

4. Lo que se pide

Elijan al menos tres code smells de su propio proyecto (P0 a P3) y corríjanlos con refactorings. Para cada uno:

  1. Nómbrenlo (método largo, código duplicado, clase con varias responsabilidades, switch o cadena de instanceof que crece, feature envy, dependencia de una clase concreta, …) y digan qué principio SOLID viola, si viola alguno.
  2. Antes de tocarlo, asegúrense de que hay pruebas que cubren ese código. Si no las hay, escriban primero characterization tests (pruebas que fijan lo que el código hace hoy).
  3. Refactoricen en pasos pequeños, corriendo make test después de cada uno. Si usan git, un commit por paso con el nombre del refactoring en el mensaje (Extract Function: validar el carrito en checkout).

Candidatos que suelen aparecer en este proyecto (no es obligatorio usar estos; lo importante es que sean code smells de su código):

  • Register.checkout hace todo en un solo método: validar, armar las líneas, descontar, registrar. Extract Function para cada paso.
  • checkout pregunta instanceof PerishableProduct para saber si se puede vender. ¿Y si mañana hay otro tipo que no se puede vender en ciertas fechas? Replace Conditional with Polymorphism: un método de Product al que las subclases le hacen override (abierto/cerrado).
  • Validaciones repetidas en Product, las subclases, SaleLine y Cart (código vacío, cantidad no positiva).
  • Bucles de P1 a P3 que repiten el mismo recorrido con otro filtro (ahora, filter).
  • Los métodos de leer y escribir soles repetidos en los tres programas de P0 (ahora existe Soles; ojo: la salida de esos programas no puede cambiar ni un carácter).
  • Los comparadores de P3: pueden quedar como clases, pero con el cuerpo escrito con Comparator.comparing(...).

5. Reglas del refactoring

  • Ninguna prueba pública cambia, y todas siguen pasando: las de P0 a P4.
  • No se renombra, no se mueve ni se cambia la firma de nada que usen las pruebas públicas (clases, constructores, métodos, el paquete por defecto). Lo interno sí: campos privados, métodos privados, clases nuevas.
  • Un refactoring no cambia lo que hace el programa. Si encuentran un error en el camino, corríjanlo en un paso aparte, con una prueba que lo muestre, y anótenlo en el README.
  • Sus pruebas propias pueden cambiar si prueban detalles internos que ya no existen, pero no borren pruebas de comportamiento.

Archivos provistos

Archivo Para qué
Makefile El mismo de siempre.
P4ReportsPublicTest.java, P4LambdasPublicTest.java Pruebas públicas de P4. No las modifiquen.
ProductTotal.java Completo.
SalesReport.java Esqueleto.

Para que todo compile, agreguen primero a Inventory y a SalesHistory sus métodos filter, aunque sea con throw new UnsupportedOperationException("por hacer");. Algunas pruebas de P4LambdasPublicTest pasan desde el principio si su P2 y su P3 quedaron bien: prueban que lo que ya existe acepta lambdas.

Cómo se prueba

Pruebas públicas

make test corre lo de P0 a P3 (8 casos de entrada y salida y 110 pruebas JUnit) y las 18 de P4: 12 de reportes y 6 de lambdas.

Pruebas privadas y diseño

Las pruebas privadas usan otras ventas: varios días, empates en cada ranking, clientes con nombres que se ordenan distinto con y sin tildes, rangos de fechas en los bordes. El diseño, además, tiene que cumplir esto:

  • que los reportes usen streams con claridad (sin forEach que modifica una colección de afuera donde un collect basta);
  • que el refactoring sea real: el código de P1 a P3 quedó más simple, con responsabilidades más claras, y se nota en el diff;
  • la sección de refactoring del README.

Qué entregar

Para la entrega de la semana 15 (si usan git, el tag p4, con la historia de commits del refactoring):

  1. Todo el código, las pruebas públicas de P0 a P4 sin cambios y sus pruebas propias (al menos una por reporte, con su caso vacío y sus empates).

  2. El README.md del equipo con el diagrama de clases actualizado y una sección “Refactoring” con una tabla, una fila por code smell:

    Code smell Dónde estaba Principio Refactorings aplicados Qué prueba lo cubre

    y, debajo, un párrafo por code smell: cómo estaba (un fragmento corto de código), cómo quedó y por qué es mejor. Digan también qué code smells vieron y decidieron no corregir, y por qué (cuándo no aplicar SOLID).