Semana 13 — Principios de diseño y refactoring
Objetivos
- Evaluar un diseño por su cohesión (cohesion) y su acoplamiento (coupling).
- Enunciar los cinco principios SOLID, reconocer cuándo se violan y cuándo aplicarlos sería exagerar.
- Refactorizar en pasos pequeños con las pruebas como red de seguridad.
- Reconocer los code smells más comunes y el refactoring que corresponde a cada uno.
Apuntes de la semana: notes/week-13.md · Código: code/week-13
Sesión A — Principios de diseño: cohesión, acoplamiento, SOLID
Lecturas (Eck no trata SOLID):
- Eck, sec. 4.1 Black Boxes and Procedural Abstraction: interfaz e implementación, la idea detrás de bajo acoplamiento.
- Robert C. Martin, The Single Responsibility Principle y The Open Closed Principle.
- Robert C. Martin, Solid Relevance: su autor repasa los cinco principios y responde a quienes dicen que ya no importan.
Lecturas complementarias:
- B. Liskov y J. Wing, A Behavioral Notion of Subtyping (relectura de la semana 7, para la L de SOLID).
- Martin Fowler, Inversion of Control Containers and the Dependency Injection pattern: secciones A Naive Example, Inversion of Control y el inicio de Forms of Dependency Injection. Los contenedores que menciona no se usan en el curso; nosotros inyectamos a mano, por el constructor.
- Martin Fowler, Beck Design Rules: las cuatro reglas del diseño simple, como contrapeso a aplicar principios por aplicar.
Temas:
- Cohesión: lo que está en una clase pertenece junto. Acoplamiento: cuánto tiene que saber una clase de otra. Objetivo: alta cohesión, bajo acoplamiento.
- S, responsabilidad única: una clase tiene una sola razón para cambiar. Ejemplo propio: una clase
Invoiceque calcula, da formato e imprime, partida en tres (code/week-13/invoice/beforeyinvoice/after). - O, abierto/cerrado: agregar comportamiento nuevo con una clase nueva y no editando un
switchexistente. Relación con el polimorfismo de la semana 6 y con elswitchsobre un tipo sealed de la semana 8 (que cierra la otra dirección). - L, sustitución de Liskov: repaso de la semana 7 con
throwsy contratos de la semana 9. - I, segregación de interfaces: interfaces pequeñas y específicas; ninguna clase obligada a implementar métodos que no usa.
- D, inversión de dependencias: las clases de alto nivel dependen de interfaces, no de clases concretas; las dependencias entran por el constructor. Es lo que ya hicimos para usar test doubles en la semana 7. Ejemplo:
InvoiceServicerecibe unChannely se prueba con unSpyChannel. - Error común: aplicar SOLID a ciegas y terminar con diez interfaces de un método para un programa de cien líneas. Un principio se aplica cuando hay un cambio concreto que lo justifica.
Sesión B — Refactoring guiado por pruebas
Lecturas (Eck no trata refactoring):
- Martin Fowler, Definition Of Refactoring y Code Smell.
- Catálogo de refactoring de Martin Fowler (refactoring.com/catalog), entradas: Extract Function, Extract Class, Move Function y Replace Conditional with Polymorphism.
- Martin Fowler, Test Driven Development: el ciclo rojo, verde, refactorizar.
Temas:
- Refactoring: cambiar la estructura sin cambiar el comportamiento observable. Las pruebas fijan ese comportamiento; sin esa red de seguridad, refactorizar es de alto riesgo, y antes se agregan characterization tests.
- Pasos pequeños: un cambio, correr las pruebas, hacer commit. Si algo falla, deshacer el último paso en lugar de depurar un cambio grande.
- Code smells frecuentes: método largo, clase grande, código duplicado, lista larga de parámetros, feature envy (un método que usa más los datos de otra clase que los suyos), cadenas de
if/switchpor tipo. - Refactorings del catálogo aplicados en vivo sobre un ejemplo propio, las multas de una biblioteca (
code/week-13/library/step0astep5, con las mismas pruebas en cada paso): renombrar, extraer método, mover método a la clase que tiene los datos, extraer clase, reemplazar condicional por polimorfismo. - Characterization tests: antes de refactorizar código sin pruebas, escribir pruebas que fijen lo que hace hoy, aunque parezca raro.
- Error común: mezclar en un mismo paso refactoring y cambio de funcionalidad; si una prueba falla, ya no se sabe cuál de los dos la rompió.
Laboratorio — Refactorizar el proyecto
Código inicial en code/week-13/lab/: GradeBook, un registro de notas que funciona, tiene pruebas y está mal diseñado a propósito. Cópienlo a su repositorio para trabajar con commits.
- Listar los code smells del código dado y el principio SOLID que cada uno viola.
- Refactorizar en pasos pequeños, corriendo
make testdespués de cada uno y haciendo un commit por paso con un mensaje que nombre el refactoring. - Aplicar inversión de dependencias en una clase que crea sus colaboradores con
newy probarla con un test double. - Repetir el ejercicio sobre el propio P4: elegir dos code smells y corregirlos sin cambiar ninguna prueba existente.
Entregas
- P4 en curso (Reportes). Entrega en semana 15. Esta semana cubre la segunda mitad del enunciado: refactorizar con principios SOLID lo que P1 a P3 dejaron acoplado.
Preguntas de repaso
- Den un ejemplo de clase con baja cohesión. ¿Cómo la partirían y qué principio aplican?
- ¿Qué relación hay entre el principio de inversión de dependencias y la posibilidad de usar test doubles?
- Un
switchsobre el tipo de producto aparece en cuatro métodos distintos. ¿Qué code smell es y qué refactoring corresponde? ¿Cambiaría la respuesta si el tipo fuera sealed? - ¿Por qué es arriesgado cambiar la estructura de un código que no tiene pruebas? ¿Qué se hace primero?
- Describan una situación en la que aplicar abierto/cerrado sería un exceso.
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (41)Pruebas (7)Laboratorio (4)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
package library;
import java.time.LocalDate;
import java.util.List;
// El mismo estado de cuenta con el código de antes (step0) y el de después
// (step5): el refactoring cambió la estructura, no lo que el programa hace.
public class LibraryDemo {
public static void main(String[] args) {
LocalDate due = LocalDate.parse("2026-10-01");
var before = new library.step0.LibraryDesk();
var alice0 = new library.step0.Member("alice", library.step0.MemberType.STUDENT);
var loans0 = List.of(
new library.step0.Loan("Clean Code", library.step0.ItemKind.BOOK, due, due.plusDays(5)),
new library.step0.Loan("Matrix", library.step0.ItemKind.DVD, due, due.plusDays(30)));
IO.println("== step0");
IO.print(before.statement(alice0, loans0));
var after = new library.step5.LibraryDesk();
var alice5 = new library.step5.Member("alice", library.step5.MemberType.STUDENT);
var loans5 = List.of(
new library.step5.Loan("Clean Code", library.step5.ItemKind.BOOK, due, due.plusDays(5)),
new library.step5.Loan("Matrix", library.step5.ItemKind.DVD, due, due.plusDays(30)));
IO.println("== step5");
IO.print(after.statement(alice5, loans5));
}
}package invoice.after;
/** Un canal por donde sale un documento: consola, archivo, correo... */
public interface Channel {
void send(String recipient, String text);
}package invoice.after;
/** Canal que escribe el documento en la salida estándar. */
public class ConsoleChannel implements Channel {
@Override
public void send(String recipient, String text) {
IO.print("[para " + recipient + "]\n" + text);
}
}package invoice.after;
import java.util.ArrayList;
import java.util.List;
/** Una factura: sus líneas y las reglas para calcular el total. Nada de texto. */
public class Invoice {
/** Una línea: cantidad y precio unitario en céntimos. */
public record Line(String description, int quantity, int unitPrice) {
public int amount() {
return quantity * unitPrice;
}
}
private static final int TAX_PERCENT = 18; // IGV
private final String customer;
private final List<Line> lines = new ArrayList<>();
public Invoice(String customer) {
this.customer = customer;
}
public void add(String description, int quantity, int unitPrice) {
lines.add(new Line(description, quantity, unitPrice));
}
public String customer() {
return customer;
}
public List<Line> lines() {
return List.copyOf(lines);
}
public int subtotal() {
int subtotal = 0;
for (Line line : lines) {
subtotal += line.amount();
}
return subtotal;
}
public int tax() {
return (subtotal() * TAX_PERCENT + 50) / 100; // redondeo al céntimo
}
public int total() {
return subtotal() + tax();
}
}package invoice.after;
// Arma las piezas en un solo lugar (main) y emite una factura por consola.
public class InvoiceDemo {
public static void main(String[] args) {
Invoice invoice = new Invoice("alice");
invoice.add("cuaderno", 3, 450);
invoice.add("lapicero", 2, 150);
InvoiceService service = new InvoiceService(new InvoiceFormatter(), new ConsoleChannel());
service.issue(invoice);
}
}package invoice.after;
/** Arma el texto de una factura. Si cambia el diseño, cambia solo esta clase. */
public class InvoiceFormatter {
public String format(Invoice invoice) {
StringBuilder text = new StringBuilder("Factura para " + invoice.customer() + "\n");
for (Invoice.Line line : invoice.lines()) {
text.append(String.format(" %2d x %-12s %s\n",
line.quantity(), line.description(), money(line.amount())));
}
text.append("Total con IGV: ").append(money(invoice.total())).append("\n");
return text.toString();
}
private static String money(int cents) {
return String.format("S/ %d.%02d", cents / 100, cents % 100);
}
}package invoice.after;
/**
* Emite facturas: les da formato y las envía. Depende de la interfaz Channel,
* no de un canal concreto; el canal entra por el constructor.
*/
public class InvoiceService {
private final InvoiceFormatter formatter;
private final Channel channel;
public InvoiceService(InvoiceFormatter formatter, Channel channel) {
this.formatter = formatter;
this.channel = channel;
}
public void issue(Invoice invoice) {
channel.send(invoice.customer(), formatter.format(invoice));
}
}package invoice.after;
import java.util.ArrayList;
import java.util.List;
/** Test double (un spy): no envía nada, recuerda lo que le pidieron enviar. */
class SpyChannel implements Channel {
final List<String> recipients = new ArrayList<>();
final List<String> texts = new ArrayList<>();
@Override
public void send(String recipient, String text) {
recipients.add(recipient);
texts.add(text);
}
}package invoice.before;
import java.util.ArrayList;
import java.util.List;
// Una factura que hace de todo: guarda las líneas, calcula el IGV, arma el
// texto y lo imprime. Tiene tres razones para cambiar (ver la semana 13).
public class Invoice {
private record Line(String description, int quantity, int unitPrice) {
}
private final String customer;
private final List<Line> lines = new ArrayList<>();
public Invoice(String customer) {
this.customer = customer;
}
public void add(String description, int quantity, int unitPrice) {
lines.add(new Line(description, quantity, unitPrice));
}
// Razón 1: las reglas del negocio (el IGV es 18 %).
public int total() {
int subtotal = 0;
for (Line line : lines) {
subtotal += line.quantity() * line.unitPrice();
}
return subtotal + (subtotal * 18 + 50) / 100;
}
// Razón 2: el diseño del documento.
public String format() {
StringBuilder text = new StringBuilder("Factura para " + customer + "\n");
for (Line line : lines) {
int amount = line.quantity() * line.unitPrice();
text.append(String.format(" %2d x %-12s S/ %d.%02d\n",
line.quantity(), line.description(), amount / 100, amount % 100));
}
int total = total();
text.append(String.format("Total con IGV: S/ %d.%02d\n", total / 100, total % 100));
return text.toString();
}
// Razón 3: a dónde va la factura.
public void print() {
System.out.print(format());
}
}package library.step0;
/** Qué se prestó. */
public enum ItemKind {
BOOK, DVD
}package library.step0;
import java.time.temporal.ChronoUnit;
import java.util.List;
// Mostrador de la biblioteca: calcula las multas de un socio y arma su estado
// de cuenta. Funciona y tiene pruebas, pero está mal diseñado a propósito:
// es el punto de partida del refactoring de la semana 13.
public class LibraryDesk {
public String statement(Member m, List<Loan> l) {
String s = "Multas de " + m.name() + " (" + m.type() + ")\n";
int t = 0;
for (Loan x : l) {
int d = (int) ChronoUnit.DAYS.between(x.due(), x.returned());
if (d < 0) {
d = 0;
}
int a = 0;
switch (m.type()) {
case STUDENT:
if (d > 2) {
a = (d - 2) * (x.kind() == ItemKind.DVD ? 100 : 50);
}
if (a > 1000) {
a = 1000;
}
break;
case TEACHER:
if (x.kind() == ItemKind.DVD) {
a = d * 100;
}
break;
case VISITOR:
a = d * (x.kind() == ItemKind.DVD ? 100 : 50) * 2;
break;
}
t += a;
s += String.format(" %-18s %3d días S/ %d.%02d\n", x.title(), d, a / 100, a % 100);
}
s += String.format("Total: S/ %d.%02d\n", t / 100, t % 100);
return s;
}
public int totalFine(Member m, List<Loan> l) {
int t = 0;
for (Loan x : l) {
int d = (int) ChronoUnit.DAYS.between(x.due(), x.returned());
if (d < 0) {
d = 0;
}
int a = 0;
switch (m.type()) {
case STUDENT:
if (d > 2) {
a = (d - 2) * (x.kind() == ItemKind.DVD ? 100 : 50);
}
if (a > 1000) {
a = 1000;
}
break;
case TEACHER:
if (x.kind() == ItemKind.DVD) {
a = d * 100;
}
break;
case VISITOR:
a = d * (x.kind() == ItemKind.DVD ? 100 : 50) * 2;
break;
}
t += a;
}
return t;
}
}package library.step0;
import java.time.LocalDate;
/** Un préstamo ya devuelto: qué se prestó, cuándo vencía y cuándo volvió. */
public record Loan(String title, ItemKind kind, LocalDate due, LocalDate returned) {
}package library.step0;
/** Un socio: nombre y tipo. */
public record Member(String name, MemberType type) {
}package library.step0;
/** Tipo de socio de la biblioteca. */
public enum MemberType {
STUDENT, TEACHER, VISITOR
}package library.step1;
/** Qué se prestó. */
public enum ItemKind {
BOOK, DVD
}package library.step1;
import java.time.temporal.ChronoUnit;
import java.util.List;
// Paso 1 · Rename: nombres que dicen qué guardan. Nada más cambió.
public class LibraryDesk {
public String statement(Member member, List<Loan> loans) {
String text = "Multas de " + member.name() + " (" + member.type() + ")\n";
int total = 0;
for (Loan loan : loans) {
int daysLate = (int) ChronoUnit.DAYS.between(loan.due(), loan.returned());
if (daysLate < 0) {
daysLate = 0;
}
int fine = 0;
switch (member.type()) {
case STUDENT:
if (daysLate > 2) {
fine = (daysLate - 2) * (loan.kind() == ItemKind.DVD ? 100 : 50);
}
if (fine > 1000) {
fine = 1000;
}
break;
case TEACHER:
if (loan.kind() == ItemKind.DVD) {
fine = daysLate * 100;
}
break;
case VISITOR:
fine = daysLate * (loan.kind() == ItemKind.DVD ? 100 : 50) * 2;
break;
}
total += fine;
text += String.format(" %-18s %3d días S/ %d.%02d\n", loan.title(), daysLate, fine / 100, fine % 100);
}
text += String.format("Total: S/ %d.%02d\n", total / 100, total % 100);
return text;
}
public int totalFine(Member member, List<Loan> loans) {
int total = 0;
for (Loan loan : loans) {
int daysLate = (int) ChronoUnit.DAYS.between(loan.due(), loan.returned());
if (daysLate < 0) {
daysLate = 0;
}
int fine = 0;
switch (member.type()) {
case STUDENT:
if (daysLate > 2) {
fine = (daysLate - 2) * (loan.kind() == ItemKind.DVD ? 100 : 50);
}
if (fine > 1000) {
fine = 1000;
}
break;
case TEACHER:
if (loan.kind() == ItemKind.DVD) {
fine = daysLate * 100;
}
break;
case VISITOR:
fine = daysLate * (loan.kind() == ItemKind.DVD ? 100 : 50) * 2;
break;
}
total += fine;
}
return total;
}
}package library.step1;
import java.time.LocalDate;
/** Un préstamo ya devuelto: qué se prestó, cuándo vencía y cuándo volvió. */
public record Loan(String title, ItemKind kind, LocalDate due, LocalDate returned) {
}package library.step1;
/** Un socio: nombre y tipo. */
public record Member(String name, MemberType type) {
}package library.step1;
/** Tipo de socio de la biblioteca. */
public enum MemberType {
STUDENT, TEACHER, VISITOR
}package library.step2;
/** Qué se prestó. */
public enum ItemKind {
BOOK, DVD
}package library.step2;
import java.time.temporal.ChronoUnit;
import java.util.List;
// Paso 2 · Extract Function: el cálculo de una multa y el formato del dinero
// tienen nombre y viven en un solo lugar. Se acabó el código duplicado.
public class LibraryDesk {
public String statement(Member member, List<Loan> loans) {
String text = "Multas de " + member.name() + " (" + member.type() + ")\n";
for (Loan loan : loans) {
text += String.format(" %-18s %3d días %s\n",
loan.title(), daysLate(loan), money(fineFor(member, loan)));
}
return text + "Total: " + money(totalFine(member, loans)) + "\n";
}
public int totalFine(Member member, List<Loan> loans) {
int total = 0;
for (Loan loan : loans) {
total += fineFor(member, loan);
}
return total;
}
private int fineFor(Member member, Loan loan) {
int daysLate = daysLate(loan);
int fine = 0;
switch (member.type()) {
case STUDENT:
if (daysLate > 2) {
fine = (daysLate - 2) * (loan.kind() == ItemKind.DVD ? 100 : 50);
}
if (fine > 1000) {
fine = 1000;
}
break;
case TEACHER:
if (loan.kind() == ItemKind.DVD) {
fine = daysLate * 100;
}
break;
case VISITOR:
fine = daysLate * (loan.kind() == ItemKind.DVD ? 100 : 50) * 2;
break;
}
return fine;
}
private int daysLate(Loan loan) {
int days = (int) ChronoUnit.DAYS.between(loan.due(), loan.returned());
return Math.max(days, 0);
}
private static String money(int cents) {
return String.format("S/ %d.%02d", cents / 100, cents % 100);
}
}package library.step2;
import java.time.LocalDate;
/** Un préstamo ya devuelto: qué se prestó, cuándo vencía y cuándo volvió. */
public record Loan(String title, ItemKind kind, LocalDate due, LocalDate returned) {
}package library.step2;
/** Un socio: nombre y tipo. */
public record Member(String name, MemberType type) {
}package library.step2;
/** Tipo de socio de la biblioteca. */
public enum MemberType {
STUDENT, TEACHER, VISITOR
}package library.step3;
/** Qué se prestó, con su multa por día de atraso en céntimos. */
public enum ItemKind {
BOOK(50), DVD(100);
private final int dailyRate;
ItemKind(int dailyRate) {
this.dailyRate = dailyRate;
}
public int dailyRate() {
return dailyRate;
}
}package library.step3;
import java.util.List;
// Paso 3 · Move Function: cada cálculo se mudó a la clase que tiene los datos.
// Los días de atraso, a Loan; la tarifa diaria, a ItemKind; la multa, a
// MemberType (y Member la ofrece). En LibraryDesk ya no queda feature envy.
public class LibraryDesk {
public String statement(Member member, List<Loan> loans) {
String text = "Multas de " + member.name() + " (" + member.type() + ")\n";
for (Loan loan : loans) {
text += String.format(" %-18s %3d días %s\n",
loan.title(), loan.daysLate(), money(member.fineFor(loan)));
}
return text + "Total: " + money(totalFine(member, loans)) + "\n";
}
public int totalFine(Member member, List<Loan> loans) {
int total = 0;
for (Loan loan : loans) {
total += member.fineFor(loan);
}
return total;
}
private static String money(int cents) {
return String.format("S/ %d.%02d", cents / 100, cents % 100);
}
}package library.step3;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
/** Un préstamo ya devuelto: qué se prestó, cuándo vencía y cuándo volvió. */
public record Loan(String title, ItemKind kind, LocalDate due, LocalDate returned) {
/** Días de atraso; 0 si se devolvió a tiempo o antes. */
public int daysLate() {
int days = (int) ChronoUnit.DAYS.between(due, returned);
return Math.max(days, 0);
}
}package library.step3;
/** Un socio: nombre y tipo. */
public record Member(String name, MemberType type) {
/** Multa de uno de sus préstamos, en céntimos. */
public int fineFor(Loan loan) {
return type.fineFor(loan);
}
}package library.step3;
/** Tipo de socio de la biblioteca. */
public enum MemberType {
STUDENT, TEACHER, VISITOR;
/** Multa de un préstamo para este tipo de socio, en céntimos. */
int fineFor(Loan loan) {
int daysLate = loan.daysLate();
int rate = loan.kind().dailyRate();
return switch (this) {
case STUDENT -> Math.min(Math.max(daysLate - 2, 0) * rate, 1000);
case TEACHER -> loan.kind() == ItemKind.DVD ? daysLate * rate : 0;
case VISITOR -> daysLate * rate * 2;
};
}
}package library.step4;
/** Qué se prestó, con su multa por día de atraso en céntimos. */
public enum ItemKind {
BOOK(50), DVD(100);
private final int dailyRate;
ItemKind(int dailyRate) {
this.dailyRate = dailyRate;
}
public int dailyRate() {
return dailyRate;
}
}package library.step4;
import java.util.List;
// Paso 4 · Extract Class: el formato del estado de cuenta se fue a
// StatementFormatter. LibraryDesk solo coordina: pide multas y las junta.
public class LibraryDesk {
private final StatementFormatter formatter = new StatementFormatter();
public String statement(Member member, List<Loan> loans) {
StringBuilder text = new StringBuilder(formatter.header(member));
for (Loan loan : loans) {
text.append(formatter.line(loan, member.fineFor(loan)));
}
text.append(formatter.footer(totalFine(member, loans)));
return text.toString();
}
public int totalFine(Member member, List<Loan> loans) {
int total = 0;
for (Loan loan : loans) {
total += member.fineFor(loan);
}
return total;
}
}package library.step4;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
/** Un préstamo ya devuelto: qué se prestó, cuándo vencía y cuándo volvió. */
public record Loan(String title, ItemKind kind, LocalDate due, LocalDate returned) {
/** Días de atraso; 0 si se devolvió a tiempo o antes. */
public int daysLate() {
int days = (int) ChronoUnit.DAYS.between(due, returned);
return Math.max(days, 0);
}
}package library.step4;
/** Un socio: nombre y tipo. */
public record Member(String name, MemberType type) {
/** Multa de uno de sus préstamos, en céntimos. */
public int fineFor(Loan loan) {
return type.fineFor(loan);
}
}package library.step4;
/** Tipo de socio de la biblioteca. */
public enum MemberType {
STUDENT, TEACHER, VISITOR;
/** Multa de un préstamo para este tipo de socio, en céntimos. */
int fineFor(Loan loan) {
int daysLate = loan.daysLate();
int rate = loan.kind().dailyRate();
return switch (this) {
case STUDENT -> Math.min(Math.max(daysLate - 2, 0) * rate, 1000);
case TEACHER -> loan.kind() == ItemKind.DVD ? daysLate * rate : 0;
case VISITOR -> daysLate * rate * 2;
};
}
}package library.step4;
/** Da formato al estado de cuenta: solo sabe de texto, no de multas. */
class StatementFormatter {
String header(Member member) {
return "Multas de " + member.name() + " (" + member.type() + ")\n";
}
String line(Loan loan, int fine) {
return String.format(" %-18s %3d días %s\n", loan.title(), loan.daysLate(), money(fine));
}
String footer(int total) {
return "Total: " + money(total) + "\n";
}
static String money(int cents) {
return String.format("S/ %d.%02d", cents / 100, cents % 100);
}
}package library.step5;
/** Qué se prestó, con su multa por día de atraso en céntimos. */
public enum ItemKind {
BOOK(50), DVD(100);
private final int dailyRate;
ItemKind(int dailyRate) {
this.dailyRate = dailyRate;
}
public int dailyRate() {
return dailyRate;
}
}package library.step5;
import java.util.List;
// Paso 5 (final): igual que el paso 4; lo que cambió está en MemberType.
public class LibraryDesk {
private final StatementFormatter formatter = new StatementFormatter();
public String statement(Member member, List<Loan> loans) {
StringBuilder text = new StringBuilder(formatter.header(member));
for (Loan loan : loans) {
text.append(formatter.line(loan, member.fineFor(loan)));
}
text.append(formatter.footer(totalFine(member, loans)));
return text.toString();
}
public int totalFine(Member member, List<Loan> loans) {
int total = 0;
for (Loan loan : loans) {
total += member.fineFor(loan);
}
return total;
}
}package library.step5;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
/** Un préstamo ya devuelto: qué se prestó, cuándo vencía y cuándo volvió. */
public record Loan(String title, ItemKind kind, LocalDate due, LocalDate returned) {
/** Días de atraso; 0 si se devolvió a tiempo o antes. */
public int daysLate() {
int days = (int) ChronoUnit.DAYS.between(due, returned);
return Math.max(days, 0);
}
}package library.step5;
/** Un socio: nombre y tipo. */
public record Member(String name, MemberType type) {
/** Multa de uno de sus préstamos, en céntimos. */
public int fineFor(Loan loan) {
return type.fineFor(loan);
}
}package library.step5;
/**
* Tipo de socio de la biblioteca. Paso 5 · Replace Conditional with
* Polymorphism: cada constante hace override de fineFor con su propia regla,
* así que ya no hay switch. Un tipo de socio nuevo es una constante nueva.
*/
public enum MemberType {
STUDENT {
@Override
int fineFor(Loan loan) {
int charged = Math.max(loan.daysLate() - GRACE_DAYS, 0);
return Math.min(charged * loan.kind().dailyRate(), CAP);
}
},
TEACHER {
@Override
int fineFor(Loan loan) {
return loan.kind() == ItemKind.DVD ? loan.daysLate() * loan.kind().dailyRate() : 0;
}
},
VISITOR {
@Override
int fineFor(Loan loan) {
return loan.daysLate() * loan.kind().dailyRate() * 2;
}
};
private static final int GRACE_DAYS = 2;
private static final int CAP = 1000; // tope por préstamo, en céntimos
/** Multa de un préstamo para este tipo de socio, en céntimos. */
abstract int fineFor(Loan loan);
}package library.step5;
/** Da formato al estado de cuenta: solo sabe de texto, no de multas. */
class StatementFormatter {
String header(Member member) {
return "Multas de " + member.name() + " (" + member.type() + ")\n";
}
String line(Loan loan, int fine) {
return String.format(" %-18s %3d días %s\n", loan.title(), loan.daysLate(), money(fine));
}
String footer(int total) {
return "Total: " + money(total) + "\n";
}
static String money(int cents) {
return String.format("S/ %d.%02d", cents / 100, cents % 100);
}
}Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
package invoice.after;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.List;
import org.junit.jupiter.api.Test;
class InvoiceServiceTest {
private static Invoice sample() {
Invoice invoice = new Invoice("alice");
invoice.add("cuaderno", 3, 450);
invoice.add("lapicero", 2, 150);
return invoice;
}
@Test
void totalIncludesTax() {
Invoice invoice = sample();
assertEquals(1650, invoice.subtotal());
assertEquals(297, invoice.tax());
assertEquals(1947, invoice.total());
}
@Test
void formatterBuildsTheText() {
String expected = """
Factura para alice
3 x cuaderno S/ 13.50
2 x lapicero S/ 3.00
Total con IGV: S/ 19.47
""";
assertEquals(expected, new InvoiceFormatter().format(sample()));
}
@Test
void issueSendsOneDocumentToTheCustomer() {
SpyChannel spy = new SpyChannel();
InvoiceService service = new InvoiceService(new InvoiceFormatter(), spy);
service.issue(sample());
assertEquals(List.of("alice"), spy.recipients);
assertEquals(new InvoiceFormatter().format(sample()), spy.texts.get(0));
}
}package library.step0;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.time.LocalDate;
import java.util.List;
import org.junit.jupiter.api.Test;
// Characterization tests: fijan lo que LibraryDesk hace HOY, antes de
// refactorizar. Los valores esperados se copiaron de la salida real del
// programa, aunque algo parezca raro. Son los mismos en todos los pasos.
class LibraryDeskTest {
private final LibraryDesk desk = new LibraryDesk();
private static Loan loan(String title, ItemKind kind, String due, String returned) {
return new Loan(title, kind, LocalDate.parse(due), LocalDate.parse(returned));
}
private static final List<Loan> ALICE_LOANS = List.of(
loan("Clean Code", ItemKind.BOOK, "2026-10-01", "2026-10-06"),
loan("Matrix", ItemKind.DVD, "2026-10-03", "2026-10-04"),
loan("SICP", ItemKind.BOOK, "2026-10-10", "2026-10-08"));
@Test
void studentStatement() {
Member alice = new Member("alice", MemberType.STUDENT);
String expected = """
Multas de alice (STUDENT)
Clean Code 5 días S/ 1.50
Matrix 1 días S/ 0.00
SICP 0 días S/ 0.00
Total: S/ 1.50
""";
assertEquals(expected, desk.statement(alice, ALICE_LOANS));
}
@Test
void totalFineMatchesStatement() {
Member alice = new Member("alice", MemberType.STUDENT);
assertEquals(150, desk.totalFine(alice, ALICE_LOANS));
}
@Test
void studentFineIsCappedPerLoan() {
Member alice = new Member("alice", MemberType.STUDENT);
List<Loan> loans = List.of(
loan("Matrix", ItemKind.DVD, "2026-09-01", "2026-10-01"),
loan("Clean Code", ItemKind.BOOK, "2026-09-01", "2026-09-05"));
// 28 días cobrables de DVD serían S/ 28.00, pero el tope es S/ 10.00 por préstamo.
assertEquals(1000 + 100, desk.totalFine(alice, loans));
}
@Test
void teacherPaysOnlyForDvds() {
Member carol = new Member("carol", MemberType.TEACHER);
List<Loan> loans = List.of(
loan("SICP", ItemKind.BOOK, "2026-10-01", "2026-10-11"),
loan("Matrix", ItemKind.DVD, "2026-10-01", "2026-10-04"));
assertEquals(300, desk.totalFine(carol, loans));
}
@Test
void visitorPaysDoubleWithoutGrace() {
Member bob = new Member("bob", MemberType.VISITOR);
List<Loan> loans = List.of(
loan("Clean Code", ItemKind.BOOK, "2026-10-01", "2026-10-05"),
loan("Matrix", ItemKind.DVD, "2026-10-01", "2026-10-02"));
assertEquals(400 + 200, desk.totalFine(bob, loans));
}
@Test
void visitorStatementWithoutCap() {
Member bob = new Member("bob", MemberType.VISITOR);
List<Loan> loans = List.of(loan("Matrix", ItemKind.DVD, "2026-09-01", "2026-10-01"));
String expected = """
Multas de bob (VISITOR)
Matrix 30 días S/ 60.00
Total: S/ 60.00
""";
assertEquals(expected, desk.statement(bob, loans));
}
@Test
void emptyStatement() {
Member bob = new Member("bob", MemberType.VISITOR);
assertEquals("Multas de bob (VISITOR)\nTotal: S/ 0.00\n", desk.statement(bob, List.of()));
}
}package library.step1;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.time.LocalDate;
import java.util.List;
import org.junit.jupiter.api.Test;
// Characterization tests: fijan lo que LibraryDesk hace HOY, antes de
// refactorizar. Los valores esperados se copiaron de la salida real del
// programa, aunque algo parezca raro. Son los mismos en todos los pasos.
class LibraryDeskTest {
private final LibraryDesk desk = new LibraryDesk();
private static Loan loan(String title, ItemKind kind, String due, String returned) {
return new Loan(title, kind, LocalDate.parse(due), LocalDate.parse(returned));
}
private static final List<Loan> ALICE_LOANS = List.of(
loan("Clean Code", ItemKind.BOOK, "2026-10-01", "2026-10-06"),
loan("Matrix", ItemKind.DVD, "2026-10-03", "2026-10-04"),
loan("SICP", ItemKind.BOOK, "2026-10-10", "2026-10-08"));
@Test
void studentStatement() {
Member alice = new Member("alice", MemberType.STUDENT);
String expected = """
Multas de alice (STUDENT)
Clean Code 5 días S/ 1.50
Matrix 1 días S/ 0.00
SICP 0 días S/ 0.00
Total: S/ 1.50
""";
assertEquals(expected, desk.statement(alice, ALICE_LOANS));
}
@Test
void totalFineMatchesStatement() {
Member alice = new Member("alice", MemberType.STUDENT);
assertEquals(150, desk.totalFine(alice, ALICE_LOANS));
}
@Test
void studentFineIsCappedPerLoan() {
Member alice = new Member("alice", MemberType.STUDENT);
List<Loan> loans = List.of(
loan("Matrix", ItemKind.DVD, "2026-09-01", "2026-10-01"),
loan("Clean Code", ItemKind.BOOK, "2026-09-01", "2026-09-05"));
// 28 días cobrables de DVD serían S/ 28.00, pero el tope es S/ 10.00 por préstamo.
assertEquals(1000 + 100, desk.totalFine(alice, loans));
}
@Test
void teacherPaysOnlyForDvds() {
Member carol = new Member("carol", MemberType.TEACHER);
List<Loan> loans = List.of(
loan("SICP", ItemKind.BOOK, "2026-10-01", "2026-10-11"),
loan("Matrix", ItemKind.DVD, "2026-10-01", "2026-10-04"));
assertEquals(300, desk.totalFine(carol, loans));
}
@Test
void visitorPaysDoubleWithoutGrace() {
Member bob = new Member("bob", MemberType.VISITOR);
List<Loan> loans = List.of(
loan("Clean Code", ItemKind.BOOK, "2026-10-01", "2026-10-05"),
loan("Matrix", ItemKind.DVD, "2026-10-01", "2026-10-02"));
assertEquals(400 + 200, desk.totalFine(bob, loans));
}
@Test
void visitorStatementWithoutCap() {
Member bob = new Member("bob", MemberType.VISITOR);
List<Loan> loans = List.of(loan("Matrix", ItemKind.DVD, "2026-09-01", "2026-10-01"));
String expected = """
Multas de bob (VISITOR)
Matrix 30 días S/ 60.00
Total: S/ 60.00
""";
assertEquals(expected, desk.statement(bob, loans));
}
@Test
void emptyStatement() {
Member bob = new Member("bob", MemberType.VISITOR);
assertEquals("Multas de bob (VISITOR)\nTotal: S/ 0.00\n", desk.statement(bob, List.of()));
}
}package library.step2;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.time.LocalDate;
import java.util.List;
import org.junit.jupiter.api.Test;
// Characterization tests: fijan lo que LibraryDesk hace HOY, antes de
// refactorizar. Los valores esperados se copiaron de la salida real del
// programa, aunque algo parezca raro. Son los mismos en todos los pasos.
class LibraryDeskTest {
private final LibraryDesk desk = new LibraryDesk();
private static Loan loan(String title, ItemKind kind, String due, String returned) {
return new Loan(title, kind, LocalDate.parse(due), LocalDate.parse(returned));
}
private static final List<Loan> ALICE_LOANS = List.of(
loan("Clean Code", ItemKind.BOOK, "2026-10-01", "2026-10-06"),
loan("Matrix", ItemKind.DVD, "2026-10-03", "2026-10-04"),
loan("SICP", ItemKind.BOOK, "2026-10-10", "2026-10-08"));
@Test
void studentStatement() {
Member alice = new Member("alice", MemberType.STUDENT);
String expected = """
Multas de alice (STUDENT)
Clean Code 5 días S/ 1.50
Matrix 1 días S/ 0.00
SICP 0 días S/ 0.00
Total: S/ 1.50
""";
assertEquals(expected, desk.statement(alice, ALICE_LOANS));
}
@Test
void totalFineMatchesStatement() {
Member alice = new Member("alice", MemberType.STUDENT);
assertEquals(150, desk.totalFine(alice, ALICE_LOANS));
}
@Test
void studentFineIsCappedPerLoan() {
Member alice = new Member("alice", MemberType.STUDENT);
List<Loan> loans = List.of(
loan("Matrix", ItemKind.DVD, "2026-09-01", "2026-10-01"),
loan("Clean Code", ItemKind.BOOK, "2026-09-01", "2026-09-05"));
// 28 días cobrables de DVD serían S/ 28.00, pero el tope es S/ 10.00 por préstamo.
assertEquals(1000 + 100, desk.totalFine(alice, loans));
}
@Test
void teacherPaysOnlyForDvds() {
Member carol = new Member("carol", MemberType.TEACHER);
List<Loan> loans = List.of(
loan("SICP", ItemKind.BOOK, "2026-10-01", "2026-10-11"),
loan("Matrix", ItemKind.DVD, "2026-10-01", "2026-10-04"));
assertEquals(300, desk.totalFine(carol, loans));
}
@Test
void visitorPaysDoubleWithoutGrace() {
Member bob = new Member("bob", MemberType.VISITOR);
List<Loan> loans = List.of(
loan("Clean Code", ItemKind.BOOK, "2026-10-01", "2026-10-05"),
loan("Matrix", ItemKind.DVD, "2026-10-01", "2026-10-02"));
assertEquals(400 + 200, desk.totalFine(bob, loans));
}
@Test
void visitorStatementWithoutCap() {
Member bob = new Member("bob", MemberType.VISITOR);
List<Loan> loans = List.of(loan("Matrix", ItemKind.DVD, "2026-09-01", "2026-10-01"));
String expected = """
Multas de bob (VISITOR)
Matrix 30 días S/ 60.00
Total: S/ 60.00
""";
assertEquals(expected, desk.statement(bob, loans));
}
@Test
void emptyStatement() {
Member bob = new Member("bob", MemberType.VISITOR);
assertEquals("Multas de bob (VISITOR)\nTotal: S/ 0.00\n", desk.statement(bob, List.of()));
}
}package library.step3;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.time.LocalDate;
import java.util.List;
import org.junit.jupiter.api.Test;
// Characterization tests: fijan lo que LibraryDesk hace HOY, antes de
// refactorizar. Los valores esperados se copiaron de la salida real del
// programa, aunque algo parezca raro. Son los mismos en todos los pasos.
class LibraryDeskTest {
private final LibraryDesk desk = new LibraryDesk();
private static Loan loan(String title, ItemKind kind, String due, String returned) {
return new Loan(title, kind, LocalDate.parse(due), LocalDate.parse(returned));
}
private static final List<Loan> ALICE_LOANS = List.of(
loan("Clean Code", ItemKind.BOOK, "2026-10-01", "2026-10-06"),
loan("Matrix", ItemKind.DVD, "2026-10-03", "2026-10-04"),
loan("SICP", ItemKind.BOOK, "2026-10-10", "2026-10-08"));
@Test
void studentStatement() {
Member alice = new Member("alice", MemberType.STUDENT);
String expected = """
Multas de alice (STUDENT)
Clean Code 5 días S/ 1.50
Matrix 1 días S/ 0.00
SICP 0 días S/ 0.00
Total: S/ 1.50
""";
assertEquals(expected, desk.statement(alice, ALICE_LOANS));
}
@Test
void totalFineMatchesStatement() {
Member alice = new Member("alice", MemberType.STUDENT);
assertEquals(150, desk.totalFine(alice, ALICE_LOANS));
}
@Test
void studentFineIsCappedPerLoan() {
Member alice = new Member("alice", MemberType.STUDENT);
List<Loan> loans = List.of(
loan("Matrix", ItemKind.DVD, "2026-09-01", "2026-10-01"),
loan("Clean Code", ItemKind.BOOK, "2026-09-01", "2026-09-05"));
// 28 días cobrables de DVD serían S/ 28.00, pero el tope es S/ 10.00 por préstamo.
assertEquals(1000 + 100, desk.totalFine(alice, loans));
}
@Test
void teacherPaysOnlyForDvds() {
Member carol = new Member("carol", MemberType.TEACHER);
List<Loan> loans = List.of(
loan("SICP", ItemKind.BOOK, "2026-10-01", "2026-10-11"),
loan("Matrix", ItemKind.DVD, "2026-10-01", "2026-10-04"));
assertEquals(300, desk.totalFine(carol, loans));
}
@Test
void visitorPaysDoubleWithoutGrace() {
Member bob = new Member("bob", MemberType.VISITOR);
List<Loan> loans = List.of(
loan("Clean Code", ItemKind.BOOK, "2026-10-01", "2026-10-05"),
loan("Matrix", ItemKind.DVD, "2026-10-01", "2026-10-02"));
assertEquals(400 + 200, desk.totalFine(bob, loans));
}
@Test
void visitorStatementWithoutCap() {
Member bob = new Member("bob", MemberType.VISITOR);
List<Loan> loans = List.of(loan("Matrix", ItemKind.DVD, "2026-09-01", "2026-10-01"));
String expected = """
Multas de bob (VISITOR)
Matrix 30 días S/ 60.00
Total: S/ 60.00
""";
assertEquals(expected, desk.statement(bob, loans));
}
@Test
void emptyStatement() {
Member bob = new Member("bob", MemberType.VISITOR);
assertEquals("Multas de bob (VISITOR)\nTotal: S/ 0.00\n", desk.statement(bob, List.of()));
}
}package library.step4;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.time.LocalDate;
import java.util.List;
import org.junit.jupiter.api.Test;
// Characterization tests: fijan lo que LibraryDesk hace HOY, antes de
// refactorizar. Los valores esperados se copiaron de la salida real del
// programa, aunque algo parezca raro. Son los mismos en todos los pasos.
class LibraryDeskTest {
private final LibraryDesk desk = new LibraryDesk();
private static Loan loan(String title, ItemKind kind, String due, String returned) {
return new Loan(title, kind, LocalDate.parse(due), LocalDate.parse(returned));
}
private static final List<Loan> ALICE_LOANS = List.of(
loan("Clean Code", ItemKind.BOOK, "2026-10-01", "2026-10-06"),
loan("Matrix", ItemKind.DVD, "2026-10-03", "2026-10-04"),
loan("SICP", ItemKind.BOOK, "2026-10-10", "2026-10-08"));
@Test
void studentStatement() {
Member alice = new Member("alice", MemberType.STUDENT);
String expected = """
Multas de alice (STUDENT)
Clean Code 5 días S/ 1.50
Matrix 1 días S/ 0.00
SICP 0 días S/ 0.00
Total: S/ 1.50
""";
assertEquals(expected, desk.statement(alice, ALICE_LOANS));
}
@Test
void totalFineMatchesStatement() {
Member alice = new Member("alice", MemberType.STUDENT);
assertEquals(150, desk.totalFine(alice, ALICE_LOANS));
}
@Test
void studentFineIsCappedPerLoan() {
Member alice = new Member("alice", MemberType.STUDENT);
List<Loan> loans = List.of(
loan("Matrix", ItemKind.DVD, "2026-09-01", "2026-10-01"),
loan("Clean Code", ItemKind.BOOK, "2026-09-01", "2026-09-05"));
// 28 días cobrables de DVD serían S/ 28.00, pero el tope es S/ 10.00 por préstamo.
assertEquals(1000 + 100, desk.totalFine(alice, loans));
}
@Test
void teacherPaysOnlyForDvds() {
Member carol = new Member("carol", MemberType.TEACHER);
List<Loan> loans = List.of(
loan("SICP", ItemKind.BOOK, "2026-10-01", "2026-10-11"),
loan("Matrix", ItemKind.DVD, "2026-10-01", "2026-10-04"));
assertEquals(300, desk.totalFine(carol, loans));
}
@Test
void visitorPaysDoubleWithoutGrace() {
Member bob = new Member("bob", MemberType.VISITOR);
List<Loan> loans = List.of(
loan("Clean Code", ItemKind.BOOK, "2026-10-01", "2026-10-05"),
loan("Matrix", ItemKind.DVD, "2026-10-01", "2026-10-02"));
assertEquals(400 + 200, desk.totalFine(bob, loans));
}
@Test
void visitorStatementWithoutCap() {
Member bob = new Member("bob", MemberType.VISITOR);
List<Loan> loans = List.of(loan("Matrix", ItemKind.DVD, "2026-09-01", "2026-10-01"));
String expected = """
Multas de bob (VISITOR)
Matrix 30 días S/ 60.00
Total: S/ 60.00
""";
assertEquals(expected, desk.statement(bob, loans));
}
@Test
void emptyStatement() {
Member bob = new Member("bob", MemberType.VISITOR);
assertEquals("Multas de bob (VISITOR)\nTotal: S/ 0.00\n", desk.statement(bob, List.of()));
}
}package library.step5;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.time.LocalDate;
import java.util.List;
import org.junit.jupiter.api.Test;
// Characterization tests: fijan lo que LibraryDesk hace HOY, antes de
// refactorizar. Los valores esperados se copiaron de la salida real del
// programa, aunque algo parezca raro. Son los mismos en todos los pasos.
class LibraryDeskTest {
private final LibraryDesk desk = new LibraryDesk();
private static Loan loan(String title, ItemKind kind, String due, String returned) {
return new Loan(title, kind, LocalDate.parse(due), LocalDate.parse(returned));
}
private static final List<Loan> ALICE_LOANS = List.of(
loan("Clean Code", ItemKind.BOOK, "2026-10-01", "2026-10-06"),
loan("Matrix", ItemKind.DVD, "2026-10-03", "2026-10-04"),
loan("SICP", ItemKind.BOOK, "2026-10-10", "2026-10-08"));
@Test
void studentStatement() {
Member alice = new Member("alice", MemberType.STUDENT);
String expected = """
Multas de alice (STUDENT)
Clean Code 5 días S/ 1.50
Matrix 1 días S/ 0.00
SICP 0 días S/ 0.00
Total: S/ 1.50
""";
assertEquals(expected, desk.statement(alice, ALICE_LOANS));
}
@Test
void totalFineMatchesStatement() {
Member alice = new Member("alice", MemberType.STUDENT);
assertEquals(150, desk.totalFine(alice, ALICE_LOANS));
}
@Test
void studentFineIsCappedPerLoan() {
Member alice = new Member("alice", MemberType.STUDENT);
List<Loan> loans = List.of(
loan("Matrix", ItemKind.DVD, "2026-09-01", "2026-10-01"),
loan("Clean Code", ItemKind.BOOK, "2026-09-01", "2026-09-05"));
// 28 días cobrables de DVD serían S/ 28.00, pero el tope es S/ 10.00 por préstamo.
assertEquals(1000 + 100, desk.totalFine(alice, loans));
}
@Test
void teacherPaysOnlyForDvds() {
Member carol = new Member("carol", MemberType.TEACHER);
List<Loan> loans = List.of(
loan("SICP", ItemKind.BOOK, "2026-10-01", "2026-10-11"),
loan("Matrix", ItemKind.DVD, "2026-10-01", "2026-10-04"));
assertEquals(300, desk.totalFine(carol, loans));
}
@Test
void visitorPaysDoubleWithoutGrace() {
Member bob = new Member("bob", MemberType.VISITOR);
List<Loan> loans = List.of(
loan("Clean Code", ItemKind.BOOK, "2026-10-01", "2026-10-05"),
loan("Matrix", ItemKind.DVD, "2026-10-01", "2026-10-02"));
assertEquals(400 + 200, desk.totalFine(bob, loans));
}
@Test
void visitorStatementWithoutCap() {
Member bob = new Member("bob", MemberType.VISITOR);
List<Loan> loans = List.of(loan("Matrix", ItemKind.DVD, "2026-09-01", "2026-10-01"));
String expected = """
Multas de bob (VISITOR)
Matrix 30 días S/ 60.00
Total: S/ 60.00
""";
assertEquals(expected, desk.statement(bob, loans));
}
@Test
void emptyStatement() {
Member bob = new Member("bob", MemberType.VISITOR);
assertEquals("Multas de bob (VISITOR)\nTotal: S/ 0.00\n", desk.statement(bob, List.of()));
}
}Laboratorio
Archivos de partida del laboratorio de la semana.
package lab;
// El "periódico mural" del curso: por ahora, la consola.
public class BulletinBoard {
public void post(String text) {
IO.println("==================== MURAL ====================");
IO.print(text);
IO.println("===============================================");
}
}package lab;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
// Registro de notas de un curso. Funciona y tiene pruebas, pero está mal
// diseñado a propósito: es el punto de partida del laboratorio de la semana
// 13. No lo arreglen aquí; cópienlo a su repositorio y refactoricen allá.
//
// Cada fila de datos: código;nombre;sistema;nota1;nota2;nota3
// sistema A: promedio simple de las tres notas
// sistema B: la tercera nota (examen final) pesa doble
// sistema C: la mejor de las dos primeras, promediada con la tercera
public class GradeBook {
public List<String[]> rows = new ArrayList<>();
public void load(String text) {
for (String line : text.split("\n")) {
if (!line.isBlank()) {
rows.add(line.split(";"));
}
}
}
public String report() {
String r = "";
int ok = 0;
int ko = 0;
int sum = 0;
for (String[] x : rows) {
int a = Integer.parseInt(x[3]);
int b = Integer.parseInt(x[4]);
int c = Integer.parseInt(x[5]);
double p;
if (x[2].equals("A")) {
p = (a + b + c) / 3.0;
} else if (x[2].equals("B")) {
p = (a + b + 2 * c) / 4.0;
} else if (x[2].equals("C")) {
p = (Math.max(a, b) + c) / 2.0;
} else {
p = 0;
}
int f = (int) Math.round(p);
String st;
if (f >= 11) {
st = "aprobado";
ok++;
} else {
st = "desaprobado";
ko++;
}
r += x[0] + " " + x[1] + ": " + f + " (" + st + ")\n";
sum += f;
}
int tenths = (int) Math.round(sum * 10.0 / rows.size());
r += "aprobados: " + ok + ", desaprobados: " + ko + "\n";
r += "promedio del curso: " + tenths / 10 + "." + tenths % 10 + "\n";
return r;
}
public int finalGrade(String system, int a, int b, int c) {
double p;
if (system.equals("A")) {
p = (a + b + c) / 3.0;
} else if (system.equals("B")) {
p = (a + b + 2 * c) / 4.0;
} else if (system.equals("C")) {
p = (Math.max(a, b) + c) / 2.0;
} else {
p = 0;
}
return (int) Math.round(p);
}
public void publish() {
BulletinBoard board = new BulletinBoard();
board.post("Notas al " + LocalDate.now() + "\n" + report());
}
}package lab;
public class GradeBookDemo {
public static void main(String[] args) {
GradeBook book = new GradeBook();
book.load("""
20261001;alice;A;12;14;10
20261002;bob;B;8;10;11
20261003;carol;C;6;15;9
""");
IO.print(book.report());
}
}package lab;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
// Pruebas del registro de notas tal como funciona hoy. Al refactorizar, no
// las cambien: si alguna falla, el último paso cambió el comportamiento.
class GradeBookTest {
private static final String DATA = """
20261001;alice;A;12;14;10
20261002;bob;B;8;10;11
20261003;carol;C;6;15;9
""";
@Test
void finalGradeBySystem() {
GradeBook book = new GradeBook();
assertEquals(12, book.finalGrade("A", 12, 14, 10));
assertEquals(10, book.finalGrade("B", 8, 10, 11));
assertEquals(12, book.finalGrade("C", 6, 15, 9));
}
@Test
void halfRoundsUp() {
assertEquals(11, new GradeBook().finalGrade("C", 10, 9, 11));
}
@Test
void report() {
GradeBook book = new GradeBook();
book.load(DATA);
String expected = """
20261001 alice: 12 (aprobado)
20261002 bob: 10 (desaprobado)
20261003 carol: 12 (aprobado)
aprobados: 2, desaprobados: 1
promedio del curso: 11.3
""";
assertEquals(expected, book.report());
}
}Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 13: principios de diseño y refactoring guiado por pruebas.
# make compila todo a build/
# make test comprueba que los pasos del refactoring tienen las mismas
# pruebas, corre las demostraciones y todas las pruebas JUnit
# make clean borra build/
JAVAC = javac
JFLAGS = -Xlint:all -Werror
SRC = $(shell find invoice library lab -name '*.java')
DEMOS = invoice.after.InvoiceDemo library.LibraryDemo lab.GradeBookDemo
STEPS = step1 step2 step3 step4 step5
all: build/.stamp
build/.stamp: $(SRC)
mkdir -p build
$(JAVAC) $(JFLAGS) -cp $(JUNIT_JAR) -d build $(SRC)
touch $@
# Las pruebas de cada paso deben ser las de step0, salvo la línea del paquete.
same-tests: all
@sed 1d library/step0/LibraryDeskTest.java > build/step0-tests.txt
@for s in $(STEPS); do \
sed 1d library/$$s/LibraryDeskTest.java | cmp -s - build/step0-tests.txt \
|| { echo "library/$$s: sus pruebas no son las de step0"; exit 1; }; \
done
@echo "== las pruebas de step1..step5 son idénticas a las de step0"
JUNIT = java -jar $(JUNIT_JAR) execute --class-path build --disable-banner --disable-ansi-colors
# Cada paso del refactoring se prueba por separado, con un resumen de una línea.
test: same-tests
@for d in $(DEMOS); do echo "== $$d"; java -cp build $$d || exit 1; done
@for s in step0 $(STEPS); do \
out=$$($(JUNIT) --select-package library.$$s --details=summary) || { echo "$$out"; exit 1; }; \
echo "== pruebas de library.$$s: $$(echo "$$out" | grep -o '[0-9]* tests successful')"; \
done
$(JUNIT) --select-package invoice --select-package lab --details=tree
clean:
rm -rf build
.PHONY: all same-tests test clean