Semana 7 — Interfaces y composición
Objetivos
- Definir e implementar interfaces (interfaces) y usarlas como tipos.
- Decidir entre interfaz y clase abstracta.
- Elegir entre herencia y composición (composition): reconocer cuándo la herencia rompe el encapsulamiento y cuándo una subclase no es un buen sustituto.
- Enunciar el principio de sustitución de Liskov y aplicarlo a ejemplos concretos.
- Escribir test doubles a mano gracias a una interfaz.
Apuntes de la semana: notes/week-07.md · Código: code/week-07
Sesión A — Interfaces
Lecturas:
- Eck, sec. 5.7 Interfaces, subsecciones 5.7.1 a 5.7.3.
Lecturas complementarias:
- dev.java, Interfaces, Implementing an Interface y Using an Interface as a Type.
- API de Java 25:
Comparable, como ejemplo de interfaz de la biblioteca estándar.
Temas:
- Interfaz como contrato: un conjunto de métodos sin estado que una clase promete ofrecer. Ejemplo propio:
interface Payable { long amountDue(); }implementada por una factura (Invoice) y por un empleado (elEmployeede la semana 6), que no tienen ninguna superclase en común. implements: una clase puede implementar varias interfaces pero extender una sola clase. Por qué Java evita la herencia múltiple de estado.- La interfaz como tipo: variables, parámetros y listas de tipo
Payable. El dynamic dispatch de la semana 6 funciona igual. - Métodos
defaultystaticen interfaces: agregar comportamiento sin romper a quienes ya la implementan. Constantes en interfaces y por qué no abusar de ellas. Comparable<T>como primer ejemplo de la API: una sola operación,compareTo, que permite ordenar. Ejemplo:record Songordenado por duración. El orden y los comparadores se estudian en la semana 11.- ¿Interfaz o clase abstracta? Interfaz para una capacidad (“se puede pagar”, “se puede comparar”); clase abstracta para compartir estado y código entre parientes cercanos.
- Error común: crear una interfaz por cada clase “por si acaso” (
FooImplimplementaFooy nadie más lo hará).
Sesión B — Composición vs. herencia · Sustitución de Liskov
Lecturas (Eck no trata estos temas):
- B. Liskov y J. Wing, A Behavioral Notion of Subtyping, ACM TOPLAS 16(6), 1994. Leer la introducción y la sección 2 (motivación); el resto es formal y opcional.
- Martin Fowler, catálogo de refactoring: Replace Superclass with Delegate.
- API de Java 25:
java.util.Stack, que extiendeVector: mirar la lista de métodos heredados. - Opcional: J. Bloch, Effective Java, 3.ª ed., ítem 18 (Favor composition over inheritance).
Temas:
- Composición: un objeto tiene a otro como campo y le delega (delegation) trabajo. Ejemplo propio: una
Playlistque tiene unArrayList<Song>frente a unaPlaylist extends ArrayList<Song>, que heredaclear,sety todo lo demás, y ya no puede proteger su invariante. - La herencia acopla la subclase a los detalles de la superclase: si la superclase cambia cómo llama a sus propios métodos, la subclase puede romperse sin haber cambiado. Ejemplo: un
CountingSet extends HashSet<String>que cuenta doble conaddAll(superclase frágil), frente a su versión por composición. - Principio de sustitución de Liskov (LSP): todo lo que vale para un objeto del supertipo debe valer para uno del subtipo. Precondiciones que no se refuerzan, postcondiciones que no se debilitan, invariantes que se mantienen.
- Casos clásicos:
Square extends Rectanglecon setters independientes de ancho y alto;java.util.Stackconget(i)yadd(i, x)heredados, que permiten saltarse el orden LIFO del stack. - El
ColorPointde la semana 5, rehecho por composición (tiene unPoint):equalsvuelve a ser simétrico. - Regla práctica: herencia solo si la relación “es un” se cumple también en el comportamiento; si no, composición más una interfaz común.
Test doubles (para el laboratorio):
- Martin Fowler, Test Double (dummy, stub, fake, spy, mock) y, opcional, Mocks Aren’t Stubs.
Laboratorio — Interfaces y test doubles
Código inicial en code/week-07/: Greeter y GreeterTest (pasos 1–3), UndoHistory y UndoHistoryTest (paso 4). Ejemplo resuelto de referencia en los apuntes: StockMonitor con un stub y un spy.
Greeter.greetsaluda según la hora real (LocalTime.now()), así que hoy no se puede probar qué saludo da a las 9 o a las 21. Extraer una interfaz para “la hora actual” y hacer queGreeterla reciba por el constructor.- Escribir a mano, como clases que implementan esa interfaz, un stub (devuelve una hora fija) y un spy (recuerda cuántas veces se le preguntó la hora). Sin bibliotecas de mocks.
- Probar
Greeterpasándole el test double por el constructor: una prueba por cada saludo y por cada borde (11:59, 12:00, 18:59, 19:00). UndoHistoryextiendejava.util.Stack. Convertirla en composición más delegación (Replace Superclass with Delegate) conUndoHistoryTestcomo red de seguridad, y agregar una prueba de algo que la versión heredada permitía y la nueva ya no debe permitir.
Entregas
- P2 en curso (Catálogo). Entrega en semana 9. Las reglas de precio y descuento del catálogo se expresan como interfaces; el laboratorio muestra cómo probarlas con test doubles.
Preguntas de repaso
- ¿Por qué una clase puede implementar varias interfaces pero extender una sola clase? ¿Qué problema evita?
- ¿Qué gana una
Playlistque tiene unArrayList<Song>frente a una que lo extiende? ¿Qué pierde? - Muestren con un método de prueba que
Square extends Rectangleviola el principio de Liskov. ¿Qué expectativa del supertipo se rompe? - ¿Qué diferencia hay entre un stub y un spy? ¿Por qué ambos necesitan una interfaz (o una clase no
final) para existir? - Agregar un método abstracto a una interfaz rompe a todas las clases que la implementan. ¿Cómo lo evita un método
default? ¿Qué riesgo tiene?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (28)Pruebas (8)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
import java.util.Objects;
/**
* Un punto con color por composición: TIENE un Point en lugar de SER uno.
* Así cada clase compara solo objetos de su propio tipo y equals vuelve a ser
* simétrico. Quien necesite el punto lo pide con asPoint().
*/
public final class ColorPoint {
private final Point point;
private final String color;
public ColorPoint(int x, int y, String color) {
this.point = new Point(x, y);
this.color = color;
}
/** Vista del objeto como Point, para usarlo donde se espera un punto. */
public Point asPoint() {
return point;
}
public String color() {
return color;
}
@Override
public boolean equals(Object o) {
return o instanceof ColorPoint cp && point.equals(cp.point) && color.equals(cp.color);
}
@Override
public int hashCode() {
return Objects.hash(point, color);
}
@Override
public String toString() {
return point + " " + color;
}
}// El caso Point/ColorPoint de la semana 5, resuelto con composición.
public class ColorPointDemo {
public static void main(String[] args) {
Point p = new Point(1, 2);
ColorPoint red = new ColorPoint(1, 2, "rojo");
ColorPoint blue = new ColorPoint(1, 2, "azul");
IO.println("p.equals(red) = " + p.equals(red));
IO.println("red.equals(p) = " + red.equals(p));
IO.println("p.equals(red.asPoint()) = " + p.equals(red.asPoint()));
IO.println("red.equals(blue) = " + red.equals(blue));
}
}/** Notificador real del ejemplo: imprime el aviso. */
public class ConsoleNotifier implements Notifier {
@Override
public void send(String message) {
IO.println("[aviso] " + message);
}
}import java.util.Collection;
import java.util.HashSet;
/**
* Cuenta cuántos elementos se intentó agregar, por herencia. Está mal:
* HashSet.addAll llama a this.add por cada elemento, y ese add es el nuestro.
*/
@SuppressWarnings("serial") // HashSet es Serializable; aquí no serializamos
public class CountingSet extends HashSet<String> {
private int added = 0;
@Override
public boolean add(String s) {
added++;
return super.add(s);
}
@Override
public boolean addAll(Collection<? extends String> c) {
added += c.size();
return super.addAll(c); // ...que vuelve a llamar a add
}
public int added() {
return added;
}
}import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
/**
* La misma cuenta por composición: tiene un Set y le delega. No depende de
* cómo HashSet implemente addAll por dentro.
*/
public class DelegatingCountingSet {
private final Set<String> items = new HashSet<>();
private int added = 0;
public boolean add(String s) {
added++;
return items.add(s);
}
public boolean addAll(Collection<String> c) {
added += c.size();
return items.addAll(c); // items.add no es nuestro add: no cuenta doble
}
public boolean contains(String s) {
return items.contains(s);
}
public int size() {
return items.size();
}
public int added() {
return added;
}
}/**
* Copia de la jerarquía de la semana 6, ahora con una capacidad más:
* todo empleado se puede pagar.
*/
public abstract class Employee implements Payable {
private final String name;
protected Employee(String name) {
this.name = name;
}
public String name() {
return name;
}
/** Pago del mes en céntimos; cada tipo de empleado lo calcula a su manera. */
public abstract long monthlyPay();
/** Lo que se debe a un empleado es su pago del mes. */
@Override
public long amountDue() {
return monthlyPay();
}
/** Template Method: arma la boleta y deja el cálculo a la subclase. */
public String payslip() {
return name + ": " + formattedAmount();
}
@Override
public String toString() {
return getClass().getSimpleName() + "[" + name + "]";
}
}/** Stub: responde siempre el mismo stock, sin base de datos ni inventario. */
public class FixedStockLevels implements StockLevels {
private final int units;
public FixedStockLevels(int units) {
this.units = units;
}
@Override
public int unitsOf(String product) {
return units;
}
}import java.util.List;
// La superclase frágil: la subclase se rompe por un detalle interno de HashSet.
public class FragileDemo {
public static void main(String[] args) {
CountingSet inherited = new CountingSet();
inherited.addAll(List.of("arroz", "azúcar", "aceite"));
IO.println("herencia: added() = " + inherited.added());
DelegatingCountingSet composed = new DelegatingCountingSet();
composed.addAll(List.of("arroz", "azúcar", "aceite"));
IO.println("composición: added() = " + composed.added());
}
}import java.time.LocalTime;
/**
* Laboratorio, código inicial: saluda según la hora. Depende de la hora real
* (LocalTime.now()), así que una prueba no puede fijar qué saludo espera.
*/
public class Greeter {
public String greet(String name) {
int hour = LocalTime.now().getHour();
String greeting;
if (hour < 12) {
greeting = "Buenos días";
} else if (hour < 19) {
greeting = "Buenas tardes";
} else {
greeting = "Buenas noches";
}
return greeting + ", " + name;
}
}public class HourlyEmployee extends Employee {
private final long hourlyRate; // en céntimos
private final int hours;
public HourlyEmployee(String name, long hourlyRate, int hours) {
super(name);
this.hourlyRate = hourlyRate;
this.hours = hours;
}
@Override
public long monthlyPay() {
return hourlyRate * hours;
}
}import java.util.ArrayList;
/**
* La misma idea por herencia: ES un ArrayList. Hace override de add(Song) para
* cuidar la duración máxima, pero hereda decenas de métodos públicos que no la
* cuidan (add(int, Song), addAll, set...).
*/
@SuppressWarnings("serial") // ArrayList es Serializable; aquí no serializamos
public class InheritedPlaylist extends ArrayList<Song> {
private final int maxSeconds;
public InheritedPlaylist(int maxSeconds) {
this.maxSeconds = maxSeconds;
}
@Override
public boolean add(Song song) {
if (totalSeconds() + song.seconds() > maxSeconds) {
return false;
}
return super.add(song);
}
public int totalSeconds() {
int sum = 0;
for (Song s : this) {
sum += s.seconds();
}
return sum;
}
}/** Una factura de un proveedor. No tiene nada que ver con los empleados. */
public class Invoice implements Payable {
private final String number;
private final long amount; // en céntimos
public Invoice(String number, long amount) {
if (amount <= 0) {
throw new IllegalArgumentException("monto no positivo: " + amount);
}
this.number = number;
this.amount = amount;
}
public String number() {
return number;
}
@Override
public long amountDue() {
return amount;
}
@Override
public String toString() {
return "Invoice[" + number + "]";
}
}// Un cliente escrito para Rectangle, usado con un Rectangle y con un Square.
public class LiskovDemo {
/** Lo que cualquier cliente de Rectangle tiene derecho a esperar. */
static void resize(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
IO.println(r.getClass().getSimpleName() + ": área " + r.area() + " (se esperaba 20)");
}
public static void main(String[] args) {
resize(new Rectangle(2, 3));
resize(new Square(2));
}
}/** A quién avisarle. En producción, un correo; en una prueba, un test double. */
public interface Notifier {
void send(String message);
}import java.util.List;
/** Algo que se puede pagar: una factura, el sueldo de un empleado... */
public interface Payable {
/** Monto por pagar, en céntimos. */
long amountDue();
/** Monto con formato de soles. Las clases lo heredan sin escribir nada. */
default String formattedAmount() {
long cents = amountDue();
return String.format("S/ %d.%02d", cents / 100, cents % 100);
}
/** Suma lo que se debe en una lista de cosas por pagar. */
static long total(List<Payable> items) {
long sum = 0;
for (Payable p : items) {
sum += p.amountDue();
}
return sum;
}
}import java.util.ArrayList;
import java.util.List;
// Una factura y dos empleados en la misma lista: lo único que tienen en común
// es la interfaz Payable.
public class PayableDemo {
public static void main(String[] args) {
List<Payable> bills = new ArrayList<>();
bills.add(new Invoice("F001-120", 45_000));
bills.add(new SalariedEmployee("alice", 320_000));
bills.add(new HourlyEmployee("bob", 1_250, 80));
for (Payable p : bills) {
IO.println(p + " -> " + p.formattedAmount());
}
IO.println("Total: " + Payable.total(bills) + " céntimos");
}
}import java.util.ArrayList;
import java.util.List;
/**
* Lista de reproducción por composición: tiene un ArrayList y le delega el
* trabajo. Invariante: la duración total nunca pasa de maxSeconds.
*/
public class Playlist {
private final int maxSeconds;
private final ArrayList<Song> songs = new ArrayList<>();
private int totalSeconds = 0;
public Playlist(int maxSeconds) {
this.maxSeconds = maxSeconds;
}
/** Agrega la canción si cabe; devuelve si se agregó. */
public boolean add(Song song) {
if (totalSeconds + song.seconds() > maxSeconds) {
return false;
}
songs.add(song); // delegación
totalSeconds += song.seconds();
return true;
}
public int totalSeconds() {
return totalSeconds;
}
public int size() {
return songs.size(); // delegación
}
/** Copia de solo lectura: quien la recibe no puede romper el invariante. */
public List<Song> songs() {
return List.copyOf(songs);
}
}import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
// Composición frente a herencia con la misma lista de reproducción de 10 minutos.
public class PlaylistDemo {
public static void main(String[] args) {
Song a = new Song("Contigo Perú", "Trío Malecón", 215);
Song b = new Song("Valicha", "Grupo Garúa", 180);
Song c = new Song("El cóndor pasa", "Los Rieleros", 260);
// Comparable: compareTo dice cuál va primero.
IO.println("a.compareTo(b) = " + a.compareTo(b));
List<Song> sorted = new ArrayList<>(List.of(a, b, c));
Collections.sort(sorted);
IO.println("ordenadas: " + sorted);
Playlist mine = new Playlist(600);
IO.println("composición: " + mine.add(a) + " " + mine.add(b) + " " + mine.add(c)
+ " -> " + mine.totalSeconds() + " s");
InheritedPlaylist bad = new InheritedPlaylist(600);
IO.println("herencia: " + bad.add(a) + " " + bad.add(b) + " " + bad.add(c)
+ " -> " + bad.totalSeconds() + " s");
bad.add(0, c); // heredado: no pasa por el add que valida
bad.addAll(List.of(c, c)); // heredado
IO.println("después de add(0, c) y addAll: " + bad.totalSeconds() + " s de 600");
}
}/** Punto inmutable con igualdad de valor (el de la semana 5). */
public final class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int x() {
return x;
}
public int y() {
return y;
}
@Override
public boolean equals(Object o) {
return o instanceof Point p && x == p.x && y == p.y;
}
@Override
public int hashCode() {
return 31 * x + y;
}
@Override
public String toString() {
return "(" + x + ", " + y + ")";
}
}/** Rectángulo con ancho y alto que se cambian por separado. */
public class Rectangle {
private int width;
private int height;
public Rectangle(int width, int height) {
this.width = width; // campos directos: no llamar a los setters,
this.height = height; // de los que una subclase podría hacer override
}
/** Postcondición: cambia el ancho y deja el alto como estaba. */
public void setWidth(int width) {
this.width = width;
}
/** Postcondición: cambia el alto y deja el ancho como estaba. */
public void setHeight(int height) {
this.height = height;
}
public int width() {
return width;
}
public int height() {
return height;
}
public int area() {
return width * height;
}
}public class SalariedEmployee extends Employee {
private final long monthlySalary; // en céntimos
public SalariedEmployee(String name, long monthlySalary) {
super(name);
this.monthlySalary = monthlySalary;
}
@Override
public long monthlyPay() {
return monthlySalary;
}
}/** Una canción con su duración en segundos. Se ordena de la más corta a la más larga. */
public record Song(String title, String artist, int seconds) implements Comparable<Song> {
public Song {
if (seconds <= 0) {
throw new IllegalArgumentException("duración no positiva: " + seconds);
}
}
@Override
public int compareTo(Song other) {
return Integer.compare(seconds, other.seconds);
}
}import java.util.ArrayList;
import java.util.List;
/** Spy: no envía nada; anota cada mensaje para que la prueba lo revise. */
public class SpyNotifier implements Notifier {
private final List<String> sent = new ArrayList<>();
@Override
public void send(String message) {
sent.add(message);
}
public List<String> sent() {
return List.copyOf(sent);
}
}/**
* "Un cuadrado es un rectángulo" en geometría, pero no como Rectangle con
* setters independientes: para seguir siendo cuadrado, cada setter cambia los
* dos lados y rompe la postcondición del supertipo.
*/
public class Square extends Rectangle {
public Square(int side) {
super(side, side);
}
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width);
}
@Override
public void setHeight(int height) {
super.setWidth(height);
super.setHeight(height);
}
}import java.util.Stack;
// java.util.Stack extiende Vector: push, pop y peek respetan LIFO, pero hereda métodos
// que permiten a cualquier cliente saltarse ese orden.
public class StackDemo {
public static void main(String[] args) {
Stack<String> pages = new Stack<>();
pages.push("inicio");
pages.push("catálogo");
pages.push("carrito");
IO.println("stack: " + pages + ", peek = " + pages.peek());
pages.add(0, "intrusa"); // heredado de Vector: inserta en el fondo
String middle = pages.get(2); // heredado: lee el medio sin sacar nada
pages.remove(1); // heredado: borra del medio
IO.println("después de add(0, ...), get(2) y remove(1): " + pages + ", get(2) = " + middle);
}
}/** De dónde sale el stock actual de un producto. */
public interface StockLevels {
int unitsOf(String product);
}/**
* Avisa cuando un producto queda con poco stock. No sabe de dónde sale el
* stock ni cómo se envía el aviso: recibe las dos cosas por el constructor,
* como interfaces.
*/
public class StockMonitor {
private final StockLevels levels;
private final Notifier notifier;
private final int threshold;
public StockMonitor(StockLevels levels, Notifier notifier, int threshold) {
this.levels = levels;
this.notifier = notifier;
this.threshold = threshold;
}
/** Revisa un producto; avisa si le quedan menos de threshold unidades. */
public void check(String product) {
int units = levels.unitsOf(product);
if (units < threshold) {
notifier.send("Stock bajo: " + product + " (" + units + ")");
}
}
}import java.util.Stack;
/**
* Laboratorio, código inicial: historial para deshacer acciones. Hereda de
* Stack, así que cualquier cliente puede usar add(i, x), get(i), remove(i)...
* y desordenar el historial.
*/
@SuppressWarnings("serial") // Stack es Serializable; aquí no serializamos
public class UndoHistory extends Stack<String> {
/** Registra una acción hecha. */
public void record(String action) {
push(action);
}
/** Devuelve la última acción y la quita; null si no hay nada que deshacer. */
public String undo() {
return isEmpty() ? null : pop();
}
}Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import org.junit.jupiter.api.Test;
class ColorPointTest {
private final Point p = new Point(1, 2);
private final ColorPoint red = new ColorPoint(1, 2, "rojo");
@Test
void equalsIsSymmetricAgain() {
assertFalse(p.equals(red));
assertFalse(red.equals(p));
}
@Test
void asPointGivesTheUnderlyingPoint() {
assertEquals(p, red.asPoint());
}
@Test
void colorMatters() {
assertNotEquals(red, new ColorPoint(1, 2, "azul"));
assertEquals(red, new ColorPoint(1, 2, "rojo"));
assertEquals(red.hashCode(), new ColorPoint(1, 2, "rojo").hashCode());
}
}import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.List;
import org.junit.jupiter.api.Test;
class CountingSetTest {
@Test
void inheritedVersionCountsTwiceWithAddAll() {
// Documenta el error: HashSet.addAll llama al override de add.
CountingSet s = new CountingSet();
s.addAll(List.of("a", "b", "c"));
assertEquals(6, s.added());
}
@Test
void delegatingVersionCountsOnce() {
DelegatingCountingSet s = new DelegatingCountingSet();
s.addAll(List.of("a", "b", "c"));
s.add("a");
assertEquals(4, s.added());
assertEquals(3, s.size());
}
}import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
// Laboratorio: con la hora real solo se puede probar lo que no depende de ella.
// Después de extraer la interfaz, agreguen una prueba por cada saludo.
class GreeterTest {
@Test
void greetingEndsWithTheName() {
assertTrue(new Greeter().greet("alice").endsWith(", alice"));
}
}import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.List;
import org.junit.jupiter.api.Test;
class PayableTest {
@Test
void employeeOwesItsMonthlyPay() {
Payable p = new HourlyEmployee("bob", 1_250, 80);
assertEquals(100_000, p.amountDue());
}
@Test
void defaultMethodFormatsAnyPayable() {
assertEquals("S/ 450.05", new Invoice("F1", 45_005).formattedAmount());
assertEquals("S/ 3200.00", new SalariedEmployee("alice", 320_000).formattedAmount());
}
@Test
void totalAddsUnrelatedClasses() {
List<Payable> bills = List.of(new Invoice("F1", 45_000), new SalariedEmployee("alice", 320_000));
assertEquals(365_000, Payable.total(bills));
}
@Test
void invoiceRejectsNonPositiveAmount() {
assertThrows(IllegalArgumentException.class, () -> new Invoice("F1", 0));
}
}import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import org.junit.jupiter.api.Test;
class PlaylistTest {
private final Song a = new Song("A", "X", 215);
private final Song b = new Song("B", "X", 180);
private final Song c = new Song("C", "X", 260);
@Test
void rejectsSongThatDoesNotFit() {
Playlist p = new Playlist(600);
assertTrue(p.add(a));
assertTrue(p.add(b));
assertFalse(p.add(c));
assertEquals(395, p.totalSeconds());
assertEquals(2, p.size());
}
@Test
void songsReturnsAReadOnlyCopy() {
Playlist p = new Playlist(600);
p.add(a);
List<Song> view = p.songs();
assertThrows(UnsupportedOperationException.class, () -> view.add(c));
}
@Test
void inheritedPlaylistLetsInheritedMethodsBreakTheLimit() {
// Documenta el problema: los métodos heredados se saltan la validación.
InheritedPlaylist bad = new InheritedPlaylist(600);
bad.add(a);
bad.add(0, c);
bad.add(0, c);
assertTrue(bad.totalSeconds() > 600);
}
@Test
void songsCompareByDuration() {
assertTrue(b.compareTo(a) < 0);
assertEquals(0, a.compareTo(new Song("otra", "X", 215)));
}
}import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import org.junit.jupiter.api.Test;
class RectangleTest {
/** La expectativa de cualquier cliente de Rectangle. */
private static int areaAfterResize(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
return r.area();
}
@Test
void rectangleKeepsSidesIndependent() {
Rectangle r = new Rectangle(2, 3);
assertEquals(20, areaAfterResize(r));
}
@Test
void squareBreaksTheExpectation() {
// Documenta la violación de Liskov: la misma prueba, con un Square, da 16.
Rectangle s = new Square(2);
assertNotEquals(20, areaAfterResize(s));
}
}import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import org.junit.jupiter.api.Test;
// StockMonitor se prueba con dos test doubles: un stub que fija el stock y un spy
// que recuerda los avisos.
class StockMonitorTest {
@Test
void notifiesWhenStockIsBelowThreshold() {
SpyNotifier spy = new SpyNotifier();
StockMonitor monitor = new StockMonitor(new FixedStockLevels(2), spy, 5);
monitor.check("arroz");
assertEquals(List.of("Stock bajo: arroz (2)"), spy.sent());
}
@Test
void staysQuietWhenStockIsEnough() {
SpyNotifier spy = new SpyNotifier();
StockMonitor monitor = new StockMonitor(new FixedStockLevels(5), spy, 5);
monitor.check("arroz");
assertTrue(spy.sent().isEmpty());
}
}import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import org.junit.jupiter.api.Test;
// Laboratorio: estas pruebas deben seguir pasando después del cambio a composición.
class UndoHistoryTest {
@Test
void undoReturnsActionsInReverseOrder() {
UndoHistory h = new UndoHistory();
h.record("vender arroz");
h.record("reponer azúcar");
assertEquals("reponer azúcar", h.undo());
assertEquals("vender arroz", h.undo());
}
@Test
void undoOnEmptyHistoryGivesNull() {
assertNull(new UndoHistory().undo());
}
}Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 7: interfaces, composición frente a herencia, Liskov y test doubles.
# make compila todo a build/
# make test corre las demostraciones y las pruebas JUnit
# make clean borra build/
JAVAC = javac
JFLAGS = -Xlint:all -Werror
SRC = $(wildcard *.java)
DEMOS = PayableDemo PlaylistDemo FragileDemo LiskovDemo StackDemo ColorPointDemo
all: build/.stamp
build/.stamp: $(SRC)
mkdir -p build
$(JAVAC) $(JFLAGS) -cp $(JUNIT_JAR) -d build $(SRC)
touch $@
test: all
@for d in $(DEMOS); do echo "== $$d"; java -cp build $$d || exit 1; done
java -jar $(JUNIT_JAR) execute --class-path build --scan-class-path \
--disable-banner --disable-ansi-colors --details=tree
clean:
rm -rf build
.PHONY: all test clean