Una sola razón para cambiar por módulo — cohesión y coste del cambio
Un módulo (clase, componente o función) debe tener una, y solo una, razón para cambiar: un único actor o eje de negocio/técnico que pueda forzar su modificación. Si dos preocupaciones distintas provocan cambios en el mismo código, el módulo viola el SRP.
Robert C. Martin lo formula así para acotar el blast radius de un requisito nuevo: cuando el equipo de facturación pide un cambio y el equipo de marketing otro, no quieres que ambos toquen la misma clase por accidente.
«Responsabilidad» aquí no es «una sola función pública», sino un solo eje de variación. Orquestar un caso de uso puede ser una responsabilidad; persistir otra; notificar al cliente otra. Mezclarlas en un solo servicio barre el suelo con cambios en cadena cada vez que cambia el proveedor de correo o el esquema de la tabla.
Servicio de pedidos que también envía el correo y escribe métricas: tres razones para cambiar (dominio, canal de notificación, observabilidad).
public class OrderService { private final OrderRepository orders; public OrderService(OrderRepository orders) { this.orders = orders; } public void placeOrder(OrderId id, Money total) { var order = new Order(id, total, Instant.now()); orders.save(order); // persistencia SmtpEmailClient.sendMime( // infraestructura de correo (detalle) order.customerEmail(), buildOrderConfirmationMime(order)); Files.writeString( // otro eje (formato/ruta de logs) Path.of("/var/log/orders.log"), order.id() + "," + total + "\n", StandardOpenOption.APPEND); } }
El caso de uso orquesta; persistencia, notificación y auditoría viven detrás de abstracciones pequeñas (cada una evoluciona por su cuenta).
public final class PlaceOrder { private final OrderRepository orders; private final OrderNotifier notifier; private final OrderAudit audit; public PlaceOrder(OrderRepository orders, OrderNotifier notifier, OrderAudit audit) { this.orders = orders; this.notifier = notifier; this.audit = audit; } public void execute(OrderId id, Money total) { var order = new Order(id, total, Instant.now()); orders.save(order); notifier.orderConfirmed(order); audit.recordPlaced(order); } }
OrderNotifier puede ser correo, SMS o webhook sin tocar PlaceOrder. El log estructurado puede pasar a OpenTelemetry cambiando solo OrderAudit.