¿Quien soy?

Agenda

  • ¿Que es FCIS?
  • Breve resumen novedades del lenguaje java
  • Ejemplos de código

Antes de empezar

¿Quien ha utilizado o conoce?

  • Ports and Adapters (Arquitectura Hexagonal)
  • Domain Driven Design (DDD)
  • Clean Architecture
  • Functional Core Imperative Shell (FCIS)

Ports And Adapters

  • Alistair Cockburn, 2005
  • Arquitectura Hexagonal

Domain Driven Design

  • Eric Evans, 2004

Clean Architecture

  • Uncle Bob, 2017

Functional Core Imperative Shell (I)

Functional Core Imperative Shell (II)

  • Tienen el mismo objetivo:
    • reducir acoplamiento.
    • maximizar cohesión.
  • Aislar el dominio de dependencias externas.

Functional Core Imperative Shell (III)

  • Todos son más o menos lo mismo.
  • Hexagonal incide en aislar el código dominio.
  • FCIS se centra en definir como se debe comportar este.

Functional Core (I)

  • Es donde reside:
    • Lógica de negocio.
    • Toma decisiones.

Functional Core (II)

  • Inmutable.
  • Sin side effects.
  • Sin exceptions.
  • Sin dependencias.

Functional Core (III)

  • Funciones puras ➡️ Determinista
  • Tests más sencillos y rápidos
  • Sin necesidad de stubs/mocks

Imperative Shell

  • Orquesta el flujo de trabajo:
    • Obtiene información del exterior.
    • Llama al código de core.
    • Gestiona los side effects.
    • Devuelve un resultado.

Encuesta

¿Qué versión de Java usas habitualmente?

  • Anterior a Java 8? 💀
  • Java 8? 💔
  • Java 11? 😠
  • Java 17? 🙁
  • Java 21? 💪
  • Java 25? 🏅

Novedades del lenguaje Java

Desde Java 11 hasta Java 25:

  • Records.
  • Sealed interfaces and classes.
  • Pattern matching.
  • Switch expressions.

Records

  • Introducido en Java 16.
  • Inmutables.
  • Constructor canónico.

Records (II)

Métodos autogenerados.

public record Movie(String title, int year, int duration) {
}
  • equals()
  • hashcode()
  • toString()

Records (III)

Constructor Canónico

public record Movie(String title, int year, int duration) {
    public Movie {
        if (title == null || title.isEmpty()) {
            throw new IllegalArgumentException();
        }
    }
}
  • Se ejecuta siempre.

Sealed interfaces

  • Introducido en Java 17.
  • Jerarquías de clases cerradas.
  • non-sealed

Sealed interfaces (II)

public sealed interface Shape {
  record Square(int side) implements Shape {}
  record Rectangle(int weight, int height) implements Shape {}
  record Circle(int radius) implements Shape {}
}

Pattern Matching for Switch

  • Introducido en Java 21.
  • Deconstructores, nos permiten acceder a los componentes internos.
  • Exhaustiveness.

Pattern Matching for Switch (II)

var area = switch (this) {
    case Square(var side) -> side * side;
    case Rectangle(var weight, var height) -> weight * height;
    case Circle(var radius) -> Math.PI * Math.pow(radius, 2);
};

Pattern Matching for Switch (III)

Null Patterns

var result = switch (obj) {
    case null      -> "null";
    case Integer i -> String.format("int %d", i);
    case Long l    -> String.format("long %d", l);
    case Double d  -> String.format("double %f", d);
    case String s  -> String.format("String %s", s);
    default        -> obj.toString();
};

Pattern matching for switch (IV)

Unnamed variables and patterns

var result = switch (obj) {
    case Integer _ -> "int";
    case Long _    -> "long";
    case Double _  -> "double";
    case String _  -> "string";
    default        -> "other";
};
  • Incluido en Java 22 / Eliminar verbosidad.

Más información

Mi charla de 2024.

Otra pequeña encuesta

¿Has usado alguna vez algún lenguaje funcional?

  • Scala 😍
  • Clojure 👽
  • Haskell 💯

Aplicación de Ejemplo

Una sencilla aplicación web para gestionar datos sobre películas 🎥

  • Crear películas.
  • Buscar películas.
  • Actualizar películas.
  • Borrar películas.

Aplicación de Ejemplo (II)

  • Java 25
  • Spring Boot 4.0
  • JSpecify + NullAway

Aplicación de Ejemplo (II)

Estructura de paquetes muy simple:

es.tonivade.movies   ➡   controller ⚙
          | 
          \-- app    ➡   use cases 👷
          | 
          \-- domain ➡   domain classes & repositories 💵
          |
          \-- infra  ➡   repository implementations 🔌

Domain (I)

public record Movie(
  Integer id,
  String title,
  int year,
  int duration,
  String director,
  List<String> cast,
  int stars,
  String category) {
}

Domain (II)

public record Movie(
  @Nullable Integer id, // mark object as nullable with jspecify  String title,
  int year,
  int duration,
  String director,
  List<String> cast,
  int stars,
  String category) {
}

Domain (III)

Constructor canónico.

public record Movie(...) {

  public Movie {
    cast = List.copyOf(cast); // create an immutable copy of cast  }
}

Domain (IV)

Hacer que estados inválidos sean irrepresentables.

public record Movie(...) {

  public Movie {
    if (title.isEmpty()) {      throw new IllegalArgumentException("title cannot be empty");    }    // ...
  }
}

Domain (V)

Withers

public record Movie(...) {
  public Movie withId(int id) {
    return new Movie(
      id, title, year, duration, director, cast, stars, category);
  }
}

Domain (VI)

Value Objects

public record Movie(
  @Nullable Integer id,
  String title,
  Year year,  Duration duration,  String director,
  Cast cast,  Stars stars,  String category) {
}

Domain (VII)

Un value object, por ejemplo Year.

public record Year(int value) {
  public Year {
    if (value < 1900 || value > 2026) {
      throw new IllegalArgumentException("invalid year: " + value);
    }
  }
}

Gestión de errores (I)

Uso de objetos resultado.

public sealed interface CreateMovieResult {

  record MovieCreated(int id) implements CreateMovieResult {}

  sealed interface ValidationError extends CreateMovieResult {
    record DuplicatedMovie(int id) implements ValidationError {}
    record InvalidDuration(int duration) implements ValidationError {}
    record InvalidYear(int year) implements ValidationError {}
    record InvalidStars(int stats) implements ValidationError {}
    // more error types
  }
}

Gestión de errores (II)

public class MovieValidator {
  public static Optional<CreateMovieResult> validate(Movie movie, Set<String> categories) {
    if (movie.title() == null || movie.title().isBlank()) {
      return Optional.of(new EmptyTitle());
    }
    if (movie.director() == null || movie.director().isBlank()) {
      return Optional.of(new EmptyDirector());
    }
    if (movie.cast() == null || movie.cast().isBlank()) {
      return Optional.of(new EmptyCast());
    }
    // move validations
    return Optional.empty();
  }
}

Gestión de errores (III)

public interface MovieRepository {
  Collection<Movie> findAll();
  Optional<Movie> findById(int id);
  CreateMovieResult create(Movie movie); // ⬅️ use of result object  void delete(int id);
  Optional<Integer> findByTitle(String title);
}

Gestión de errores (IV)

public class CreateMovie {
  public int execute(Movie movie) {
    var validate = validate(movie, categoryRepository.findCategories());
    if (validate.isPresent()) {
      throw new IllegalArgumentException("invalid movie: " + movie);
    }
    if (movieRepository.findByTitle(movie.title()).isPresent()) {
      throw new IllegalArgumentException("duplicated movie: " + movie);
    }
    var result = movieRepository.create(convert(movie));
    return switch (result) {
      case MovieCreated(var id) -> id;
      default -> throw new IllegalStateException("unexpected result: " + result);
    };
  }
}

Gestión de errores (IV)

public class CreateMovie {
  public CreateMovieResult execute(Movie movie) {
    var validate = validate(movie, categoryRepository.findCategories());    if (validate.isPresent()) {      throw new IllegalArgumentException("invalid movie: " + movie);    }    if (movieRepository.findByTitle(movie.title()).isPresent()) {
      throw new IllegalArgumentException("duplicated movie: " + movie);
    }
    var result = movieRepository.create(convert(movie));
    return switch (result) {
      case MovieCreated(var id) -> id;
      default -> throw new IllegalStateException("unexpected result: " + result);
    };
  }
}

Gestión de errores (IV)

public class CreateMovie {
  public CreateMovieResult execute(Movie movie) {
    var validate = validate(movie, categoryRepository.findCategories());
    if (validate.isPresent()) {
      throw new IllegalArgumentException("invalid movie: " + movie);
    }
    if (movieRepository.findByTitle(movie.title()).isPresent()) {      throw new IllegalArgumentException("duplicated movie: " + movie);    }    var result = movieRepository.create(convert(movie));
    return switch (result) {
      case MovieCreated(var id) -> id;
      default -> throw new IllegalStateException("unexpected result: " + result);
    };
  }
}

Gestión de errores (IV)

public class CreateMovie {
  public CreateMovieResult execute(Movie movie) {
    var validate = validate(movie, categoryRepository.findCategories());
    if (validate.isPresent()) {
      throw new IllegalArgumentException("invalid movie: " + movie);
    }
    if (movieRepository.findByTitle(movie.title()).isPresent()) {
      throw new IllegalArgumentException("duplicated movie: " + movie);
    }
    var result = movieRepository.create(convert(movie));    return switch (result) {      case MovieCreated(var id) -> id;      default -> throw new IllegalStateException("unexpected result: " + result);    };
  }
}

Gestión de errores (V)

public class CreateMovie {
  public CreateMovieResult execute(Movie movie) {
    return validate(movie, categoryRepository.findCategories())      .or(() -> movieRepository.findByTitle(title).map(DuplicatedMovie::new))      .orElseGet(() -> movieRepository.create(movie));  }
}

Gestión de errores (VI)

Uso de switch expresión en el controlador.

public class MoviesController {
  @PostMapping("/movies")
  public ResponseEntity<Integer> create(@RequestBody Movie movie) {
    var result = createMovie.execute(movie);
    return switch (result) {
      case MovieCreated(var id) -> ResponseEntity.ok(id);      case ValidationError e -> ResponseEntity.of(toProblem(e)).build();    };  }}

Gestión de errores (VII)

  static ProblemDetail toProblem(ValidationError error) {
    return switch (error) {
      case EmptyTitle() -> ProblemDetail.forStatusAndDetail(BAD_REQUEST, "title cannot be empty");
      case EmptyDirector() -> ProblemDetail.forStatusAndDetail(BAD_REQUEST, "director cannot be empty");
      case InvalidDuration(var duration) -> {
        var problem = ProblemDetail.forStatusAndDetail(BAD_REQUEST, "invalid duration: " + duration);
        problem.setProperty("duration", duration);
        yield problem;
      }
      case InvalidYear(var year) -> {
        var problem = ProblemDetail.forStatusAndDetail(BAD_REQUEST, "invalid year: " + year);
        problem.setProperty("year", year);
        yield problem;
      }
      // more error handling
    };
  }

Gestión de errores (VIII)

Uso del tipo result Result.

public sealed interface Result<F, S> {

  record Failure<F, S>(F failure) implements Result<F, S> {}
  record Success<F, S>(S success) implements Result<F, S> {}

  default <T> T fold(Function<F, T> onFailure, Function<S, T> onSuccess) {
    return switch (this) {
      case Failure<F, S>(F failure) -> onFailure.apply(failure);
      case Success<F, S>(S success) -> onSuccess.apply(success);
    };
  }
}

Gestión de errores (IX)

public class MovieValidator {
  public static Result<MovieError, Movie> validate(Movie movie, Set<String> categories) {
    if (movie.title() == null || movie.title().isBlank()) {
      return Result.failure(new EmptyTitle());
    }
    if (movie.director() == null || movie.director().isBlank()) {
      return Result.failure(new EmptyDirector());
    }
    if (movie.cast() == null || movie.cast().isEmpty()) {
      return Result.failure(new EmptyCast());
    }
    return Result.success(movie);
  }
}

Gestión de errores (X)

Podemos usar Result para combinar diferentes operaciones.

public class CreateMovie {
  public Result<MovieError, Integer> execute(Movie movie) {
    return validate(movie, categoryRepository.findCategories())
        .flatMap(this::checkDuplicatedMovie)
        .flatMap(movieRepository::create);
  }
}

Gestión de errores (XI)

public class MoviesController {
  @PostMapping("/movies")
  public ResponseEntity<Integer> create(@RequestBody Movie movie) {
    var result = create.execute(movie);
    return result.fold(
        error -> ResponseEntity.of(toProblem(error)).build(),
        ResponseEntity::ok);
  }
}

Uso de DSLs (I)

Podemos subir otro peldaño más y usar DSLs.

@Diesel(errorType = MovieError.class)
public interface MovieRepository {
  Collection<Movie> findAll();
  Result<MovieError, Movie> findById(int id);
  Result<MovieError, Integer> create(Movie movie);
  void delete(int id);
  Result<MovieError, Integer> findByTitle(String title);
}

Uso de DSLs (II)

Genera un código como a este:

public interface MovieRepositoryDsl {
  static <S extends MovieRepository, E extends MovieError> Program<S, E, Collection<Movie>> findAll() {
    return Program.effect(state -> state.findAll());
  }

  static <S extends MovieRepository, E extends MovieError> Program<S, E, Integer> create(Movie movie) {
    return Program.effectR(state -> state.create(movie).mapError(e -> (E) e));
  }
}

Uso de DSLs (III)

Y podemos usarla en el caso de uso:

public class CreateMovie {
  public static <S extends MovieRepository & CategoryRepository> Program<S, MovieError, Integer> execute(Movie movie) {
    return pipe(
        findCategories(),
        categories -> validate(movie, categories),
        CreateMovie::checkDuplicatedMovie,
        MovieRepositoryDsl::create);
  }
}

Uso de DSLs (IV)

En el controlador

public class MoviesController {
  @PostMapping("/movies")
  public ResponseEntity<Integer> create(@RequestBody Movie movie) {
    var result = CreateMovie.execute(movie).eval(new Context(movieRepository, categoryRepository));
    return result.fold(
        error -> ResponseEntity.of(toProblem(error)).build(),
        ResponseEntity::ok);
  }
}

Mas información

Mi charla de 2025.

Conclusiones

  • La programación funcional en Java es posible (hasta cierto punto)
  • Aplicar las restricciones de la programación funcional pueden mejorar la calidad del software.
    • Inmutabilidad
    • Funciones puras sin side effects
  • FCIS ayuda a definir bien los limites entre qué es core y qué no lo es.

¿Preguntas? 🤔

¡Gracias! 💖

Feedback 🔄

me@tonivade.es

Links 🔗

portada generada por IA

incluso durante la deserialización. en un POJO normal, al deserializar el objeto no se llama al constructor nunca, los atributos del objeto se rellenan desde fuera de manera insegura si tuviéramos una validación en el constructor se la saltaría

antes no era posible usarlo

La programación funcional puede parecer que te chocas con una pared pero es algo más parecido a una escalera. Subes peldaños hasta donde te encuentras más cómodo.

Modelamos Movie como un record que es inmutable.

Recomiendo usar jspecify + error_prone + null-away. Luego compartiré el proyecto con el ejemplo. El uso de @NullMarked paquete por paquete puede ser muy tedioso. Se puede configurar null-away para que a partir de un cierto paquete hacia abajo se considere como @NullMarked Nos podemos ahorrar un montón de ficheros package-info.java

Hay que tener cuidado con los records y los colecciones En java las colecciones por defecto son mutables. Y un record tiene que se inmutable. Como he dicho antes el constructor canónico se llama siempre.

El constructor canónico siempre se llama. No es necesario comprobar si title es nulo, ya que null-away nos asegura de que no puede ser nunca nulo. Esto sería en tiempo de ejecución, pero estaría genial poder este tipo de cosas en tipo de compilación.

Los records son inmutables por lo que si necesitamos cambiar algo debemos crear una nueva instancia del objeto.

Tanto objeto diferente puede penalizar el consumo de memoria. Hay que hacer un trade off si merece la pena tener código más expresivo o la performance.

aquí uso optional para indicar que si el objeto es válido se devolverá un optional vacío

Algo importante que nos dice FCIS es que dentro del dominio considerando MovieValidator dentro de core, no podemos tener dependencias. Por lo que la llamada al repositorio de categorías se hace desde el caso de uso que es la parte del shell. Otra cosa importante es que si queremos hacer una operación que requiera el uso del repositorio también es algo que debe ir en la shell. Aquí como estamos en la shell esta si que es imperativa.

aprovechando que usamos Optional, podemos simplificar el código de esta forma. De alguna manera lo funcional infecta al código que lo usa. Aunque si no nos sentimos cómodos podemos dejarlo como estaba antes.

ProblemDetail es un estándar RFC https://www.rfc-editor.org/rfc/rfc9457.html En spring desde la versión 6.0 y la 3.0 de spring-boot IMO esto es mucho mejor que los @ExceptionHandler.

Podemos sacar factor común de los objetos result. Siempre tienen un resultado de tipo success o otro failure Podemos generalizarlo. Es muy similar a Either de scala o vavr, por ejemplo.

Result tiene los métodos map y flatMap De funcionalidad similar a los que tiene Optional o Stream

aquí podemos hacer uso del método fold

con una librería con la que estoy trabajando se puede marcar cualquier tipo de interfaz. Y esta anotación se procesa y se genera un objeto MovieRepositoryDsl.

Este código es el pegamento que podemos usar para crear un DSL Program es declarativo, es realmente una estructura de datos que define un DSL para componer diferentes operaciones. Hasta que no se llama al método eval, no se ejecuta nada.

El método pipe es un operador que permite encadenar llamadas, de manera similar al uso del método flatMap. Pero he descubierto que usando pipe en lugar de flatMap, el compilador infiere los tipos muchos mejor. Encadenando flatMaps el compilador pierde el hilo y termina por no saber qué está pasando.