---
name: java-patrones
description: >
  Patrones de diseño idiomáticos en Java moderno: records, sealed interfaces,
  Optional correcto, switch expressions, method references y Comparator.
  Cargar cuando se diseñe arquitectura Java 17+ o se refactorice código legacy.
version: "1.0.0"
herramientasPermitidas: [Read, Glob, Grep]
exclusiones:
  - "No cargar para implementar controllers, services o entidades Spring Boot — para implementación concreta cargar `java-experto`."
  - "No cargar para escribir tests JUnit o de integración — para testing cargar `java-testing`."
  - "No cargar para errores de compilación Java — para build errors cargar `build-errors-java`."
  - "No cargar para patrones de diseño genéricos (GoF) sin contexto Java — este skill cubre idiomas específicos de Java 17+ como sealed classes y pattern matching."
evolvable: true  # default para skill estandar
---
# Java Patrones — Diseño Idiomático Java Moderno

Cubre los patrones que produce código Java moderno legible y mantenible:
value objects con records, dominios cerrados con sealed, Optional correcto,
switch expressions, JPMS básico y composición funcional.

## Cuándo cargar este skill

Invoca `Skill("java-patrones")` cuando:

- Se diseñen value objects, command objects o result types en Java
- Se refactorice código con instanceof encadenados o switch con strings
- Se diseñe la arquitectura de un módulo nuevo en Java 17+
- Se revise código legacy que use patrones obsoletos
- Se implementen Comparators, collectors o pipelines funcionales

## Cuándo NO cargar

- La pregunta es sobre implementar controllers, services o entidades JPA — para implementación concreta cargar `java-experto`.
- La tarea es escribir tests JUnit, MockMvc o de integración — cargar `java-testing`.
- Los errores son de compilación Java — cargar `build-errors-java`.
- La discusión es sobre patrones GoF genéricos (Singleton, Observer) sin contexto Java moderno — este skill cubre idiomas específicos de Java 17+ como records, sealed e instanceof patterns.

## Conceptos clave

### Value Objects con Records

Un record es un transportador de datos inmutable con igualdad por valor.
Ideal para DTOs, value objects de dominio y parámetros de comando.
Genera automáticamente: constructor canónico, accessors, equals, hashCode, toString.

### Dominios cerrados con Sealed Interfaces

`sealed interface` + `permits` garantiza en tiempo de compilación que el
conjunto de implementaciones es conocido y finito. El compilador puede verificar
exhaustividad en switch expressions.

### Pattern Matching

`instanceof` con binding variable y switch con patterns eliminan casts manuales
y reducen la verbosidad de la navegación de tipos.

## Reglas obligatorias

### Records para value objects — no POJOs mutables

Value objects que representan conceptos del dominio (Money, Email, OrderId)
deben ser records inmutables. Los POJOs mutables con setters son frágiles en
contextos concurrentes y dificultan el razonamiento sobre el estado.

### Optional solo como tipo de retorno — nunca como parámetro

`Optional` como parámetro de método fuerza al llamador a envolver valores en
Optional innecesariamente y no aporta claridad. Como tipo de retorno comunica
explícitamente que el valor puede no estar presente.

```java
// MAL — Optional como parametro
void buscar(Optional<String> filtro) { ... }

// BIEN — Optional como retorno
Optional<Usuario> buscarPorEmail(String email) { ... }
```

### Switch expression sobre switch statement para asignacion

El switch expression es exhaustivo (el compilador obliga a cubrir todos los
casos) y retorna un valor directamente, eliminando variables mutables temporales.

### Comparator.comparing para ordenamiento — no implementar Comparable en DTOs

Los DTOs y records no deben implementar `Comparable` porque el criterio de
ordenamiento depende del contexto de uso, no del tipo. Usar `Comparator.comparing`
en el sitio de uso.

## Patrones recomendados

### Record con validación en constructor canónico

```java
public record Email(String valor) {
    public Email {
        Objects.requireNonNull(valor, "email no puede ser null");
        if (!valor.contains("@")) {
            throw new IllegalArgumentException("Email invalido: " + valor);
        }
        valor = valor.toLowerCase().trim(); // normalizacion en constructor canonico
    }
}
```

### Sealed interface como result type

```java
public sealed interface ResultadoValidacion
    permits ResultadoValidacion.Valido, ResultadoValidacion.Invalido {

    record Valido() implements ResultadoValidacion {}
    record Invalido(List<String> errores) implements ResultadoValidacion {
        public Invalido {
            errores = List.copyOf(errores); // copia defensiva
        }
    }

    static ResultadoValidacion valido() { return new Valido(); }
    static ResultadoValidacion invalido(String... errores) {
        return new Invalido(List.of(errores));
    }
}

// Uso
ResultadoValidacion resultado = validar(pedido);
String mensaje = switch (resultado) {
    case ResultadoValidacion.Valido v -> "OK";
    case ResultadoValidacion.Invalido i ->
        "Errores: " + String.join(", ", i.errores());
};
```

### Pattern matching con instanceof

```java
// Antes de Java 16 (verboso)
if (objeto instanceof String) {
    String s = (String) objeto;
    return s.toUpperCase();
}

// Java 16+ con binding variable
if (objeto instanceof String s) {
    return s.toUpperCase();
}

// Java 21+ con switch pattern matching
return switch (objeto) {
    case String s when s.isBlank() -> "VACIO";
    case String s -> s.toUpperCase();
    case Integer i -> String.valueOf(i);
    case null -> "NULL";
    default -> objeto.toString();
};
```

### Comparator composition

```java
// Ordenar por apellido, luego nombre, luego edad descendente
Comparator<Usuario> ordenamiento = Comparator
    .comparing(Usuario::apellido)
    .thenComparing(Usuario::nombre)
    .thenComparing(Comparator.comparingInt(Usuario::edad).reversed());

usuarios.sort(ordenamiento);
```

### Optional correcto

```java
// Cadena de Optional sin null checks
return repo.findByEmail(email)                    // Optional<Usuario>
    .filter(u -> u.estatus() == Estatus.ACTIVO)   // solo activos
    .map(u -> new UsuarioResponse(u.id(), u.nombre()))  // mapear a DTO
    .orElseThrow(() -> new NotFoundException("Usuario no encontrado o inactivo"));
```

### Method references en lugar de lambdas redundantes

```java
// MAL — lambda que solo delega
usuarios.stream().map(u -> u.getNombre()).collect(toList());
lista.forEach(item -> System.out.println(item));

// BIEN — method reference directo
usuarios.stream().map(Usuario::getNombre).collect(toList());
lista.forEach(System.out::println);
```

### Text blocks para strings multilínea

```java
// MAL — concatenacion fragil
String sql = "SELECT u.id, u.nombre " +
             "FROM usuarios u " +
             "WHERE u.activo = true " +
             "ORDER BY u.nombre";

// BIEN — text block (Java 15+)
String sql = """
    SELECT u.id, u.nombre
    FROM usuarios u
    WHERE u.activo = true
    ORDER BY u.nombre
    """;
```

## Anti-patrones conocidos

### Stringly-typed — NUNCA usar String para conceptos de dominio

```java
// MAL — cualquier String pasa sin verificacion
void transferir(String cuentaOrigen, String cuentaDestino, double monto) { ... }

// BIEN — tipos que documentan y verifican en compilacion
void transferir(CuentaId origen, CuentaId destino, Dinero monto) { ... }

public record CuentaId(String valor) {
    public CuentaId { Objects.requireNonNull(valor); }
}
```

### null en colecciones o retornos de lista

```java
// MAL — NullPointerException silencioso en el llamador
public List<Pedido> buscarActivos() {
    if (cache.isEmpty()) return null; // obliga al llamador a chequear null
}

// BIEN — coleccion vacia explicita
public List<Pedido> buscarActivos() {
    if (cache.isEmpty()) return List.of();
}
```

### instanceof encadenados en lugar de sealed + switch

```java
// MAL — fragil y no exhaustivo
if (evento instanceof EventoPedidoCreado) { ... }
else if (evento instanceof EventoPedidoCancelado) { ... }
// si se agrega un tercer tipo, el compilador no avisa

// BIEN — sealed garantiza exhaustividad
sealed interface EventoPedido permits EventoPedidoCreado, EventoPedidoCancelado {}
// switch expression obliga a cubrir todos los casos
```

## Checklist de verificación

- [ ] Value objects usan record, no POJO mutable
- [ ] Optional solo como tipo de retorno, nunca como parámetro
- [ ] Dominios cerrados usan sealed interface con switch exhaustivo
- [ ] No hay instanceof encadenados: se usan patterns o polimorfismo
- [ ] Ordenamientos usan Comparator.comparing, no Comparable en DTOs
- [ ] Sin null como retorno de colecciones: usar List.of() o Collections.emptyList()
- [ ] Text blocks para strings multilínea de más de 2 líneas

## Gotchas / Errores comunes no obvios

**Sealed interface con `permits` en módulo diferente al subtype requiere el mismo paquete**: las clases/interfaces que implementan un sealed interface deben estar en el mismo paquete o módulo que el sealed type. Si se intenta implementar en un paquete diferente del mismo proyecto, el compilador lanza error aunque el paquete sea accesible. Fix: colocar las implementaciones permitidas en el mismo paquete que el sealed interface, o usar JPMS para permitir acceso explícito.

**`record` con campo de tipo mutable (List, Map) no es immutable de verdad**: `record Pedido(List<Item> items)` compila pero `pedido.items().add(new Item(...))` muta el estado interno. La naturaleza final de los campos del record solo impide reasignación de la referencia, no mutación del objeto apuntado. Fix: usar `List.copyOf(items)` en el canonical constructor o retornar `Collections.unmodifiableList` en el accessor.

**`switch` expression exhaustivo con `default` no detecta casos olvidados en sealed types**: si se agrega un nuevo subtipo a una sealed interface, un `switch` con `default ->` compila sin error pero el nuevo caso cae al default silenciosamente. Fix: usar `switch` sin `default` en dominios sellados — el compilador fuerza entonces la adición del caso nuevo como error de compilación (solo funciona con sealed types).

**`Optional` como campo de entidad JPA causa serialización inconsistente**: persistir `Optional<String>` como campo de una entidad JPA causa problemas con proveedores de persistencia que no reconocen `Optional` como tipo persistible. Fix: nunca usar `Optional` como tipo de campo de entidad; usar `String` (nullable) en la entidad y envolver con `Optional` solo en el valor de retorno del repositorio o service.

---
*Skill creado con swl:crear-skill el 2026-03-31. Versión 1.0.0.*
