Luis Tavarez · Noticias

Java: POO y SOLID con un checkout que permite cambiar políticas

Separa reglas, contratos y persistencia. Sustituye un descuento sin modificar el caso de uso y comprueba el contrato con Java 17.

Luis Tavarez ·

Un cambio de negocio pequeño

Un checkout empieza con una regla de descuento. Si cada promoción obliga a editar una clase que también valida y guarda órdenes, el cambio afecta responsabilidades distintas. El objetivo de este ejercicio es poder intercambiar esa regla sin copiar el flujo de compra.

La decisión de diseño

Order conserva los datos y sus invariantes. Discount representa una política; OrderWriter es un puerto de escritura. Checkout recibe ambos por constructor. InMemoryOrders es un adaptador para el ejercicio, no una base de datos.

La separación ilustra responsabilidad única, extensión mediante políticas e inversión de dependencias. El contrato de Discount exige un resultado entre cero y el total original. La sustitución solo es válida si cada implementación respeta ese contrato; implementar una interfaz por sí solo no demuestra Liskov. No hace falta crear una interfaz para cada clase.

Qué ejecutar y observar

El ejemplo comprueba una compra sin descuento, una promoción del diez por ciento y una política inválida. Esta última se rechaza antes de guardar. Usa centavos enteros para este caso; el redondeo comercial, los impuestos y distintas monedas requieren reglas explícitas.

Cuándo adaptar la propuesta

El puerto permite añadir un adaptador SQL sin cambiar el checkout, pero todavía debes decidir los límites transaccionales. Guardar y cobrar externamente no se vuelven atómicos por usar inyección de dependencias. No hay cobros reales ni persistencia en este ejemplo.

Java · Laboratorio para desarrolladores

Arquitectura, código y práctica

LT Architecture StudioDiagrama creado con nuestra herramienta. Explóralo y adáptalo a tu proyecto.
Java: POO y SOLID con un checkout que permite cambiar políticas

Ejecutar el ejemplo

JDK 17 o superior. Descarga Main.java y ejecuta el comando en esa carpeta.

java Main.java

Salida esperada: OK: policies replaced; invalid order not saved

Código del ejemplo
java
import java.util.ArrayList;
import java.util.List;

public class Main {
    record Order(String id, long totalCents) {
        Order {
            if (id == null || id.isBlank() || totalCents < 0)
                throw new IllegalArgumentException("Invalid order");
        }
    }
    interface OrderWriter { void save(Order order); }
    interface Discount { long apply(long cents); }
    static final class InMemoryOrders implements OrderWriter {
        final List<Order> values = new ArrayList<>();
        public void save(Order order) { values.add(order); }
    }
    static final class Checkout {
        private final OrderWriter orders;
        private final Discount discount;
        Checkout(OrderWriter orders, Discount discount) {
            this.orders = orders;
            this.discount = discount;
        }
        Order place(String id, long cents) {
            if (cents < 0) throw new IllegalArgumentException("Negative total");
            long discounted = discount.apply(cents);
            if (discounted < 0 || discounted > cents)
                throw new IllegalArgumentException("Discount violated its contract");
            Order order = new Order(id, discounted);
            orders.save(order);
            return order;
        }
    }
    public static void main(String[] args) {
        InMemoryOrders orders = new InMemoryOrders();
        Checkout standard = new Checkout(orders, cents -> cents);
        Checkout promotion = new Checkout(orders, cents -> cents - cents / 10);
        if (standard.place("a", 1000).totalCents() != 1000) throw new AssertionError();
        if (promotion.place("b", 1000).totalCents() != 900) throw new AssertionError();
        try {
            new Checkout(orders, cents -> -1).place("invalid", 1000);
            throw new AssertionError("Invalid discount was accepted");
        } catch (IllegalArgumentException expected) { /* Contract enforced. */ }
        if (orders.values.size() != 2) throw new AssertionError();
        System.out.println("OK: policies replaced; invalid order not saved");
    }
}

Ejercicio para continuar

Sustituye InMemoryOrders por un adaptador que registre las llamadas. Añade una prueba de contrato para una política que devuelve un total mayor al original.