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
Optionalcuando 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
- Semana 12: la sintaxis de una lambda,
las interfaces de
java.util.function, referencias a métodos,comparing,thenComparing,reversed, streams,groupingBy,Optionaly cuándo no usar streams. - Semana 13: los principios SOLID,
cuándo no aplicarlos,
code smells,
characterization tests
y mezclar refactoring con cambios.
El ejemplo completo del mostrador de la biblioteca está en
code/week-13/library/, paso a paso. - Semana 14: los patrones son parte de P5, pero si al refactorizar encuentran una strategy sin nombre, ver Strategy.
- Eck: sec. 4.5 Lambda Expressions y sec. 10.6 Introduction the Stream API.
- API:
CollectorsyOptional.
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.productsSortedBycon comparadores comoComparator.comparingLong(Product::priceCents).reversed().Promotions.addcon un descuento escrito como lambda,(p, q) -> ...: por esoDiscounttení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:
- Nómbrenlo (método largo, código duplicado, clase con varias
responsabilidades,
switcho cadena deinstanceofque crece, feature envy, dependencia de una clase concreta, …) y digan qué principio SOLID viola, si viola alguno. - 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).
- Refactoricen en pasos pequeños, corriendo
make testdespué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.checkouthace todo en un solo método: validar, armar las líneas, descontar, registrar. Extract Function para cada paso.checkoutpreguntainstanceof PerishableProductpara 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 deProductal que las subclases le hacen override (abierto/cerrado).- Validaciones repetidas en
Product, las subclases,SaleLineyCart(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
forEachque modifica una colección de afuera donde uncollectbasta); - 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):
-
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).
-
El
README.mddel 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).