Beaucoup d’entre nous, développeurs Java, en particulier les débutants, négligent souvent ses capacités de programmation fonctionnelles. Dans cet article, nous verrons comment enchaîner Optional
et Either
pour écrire du code concis et beau.
Pour illustrer, supposons que nous ayons une banque où un utilisateur peut avoir zéro ou plusieurs comptes. Les entités se présentent comme ci-dessous :
record User(
int id,
String name
) {}
record Account(
int id,
User user // a user can have zero or more account
) {}
Pour récupérer les entités, les référentiels se présentent comme ci-dessous :
interface UserRepository {
Optional<User> findById(int userId);
}
interface AccountRepository {
Optional<Account> findAnAccountOfUser(User user); // given a user, find its any account if it has one
}
Maintenant, préparez-vous pour quelques missions !
Première affectation
Codons une méthode Optional<Account> findAnAccountByUserId(Integer userId)
qui serait:
- Donné un
userId
renvoie n’importe quel compte de l’utilisateur, s’il y en a un - Si soit il n’y a pas d’utilisateur avec l’identifiant donnéou il y a pas de compte de l’utilisateurretourne un vide
Optional
Une solution novice pourrait être la suivante :
public Optional<Account> findAccountByUserId(int userId) {
Optional<User> possibleUser = userRepository.findById(userId);
if (possibleUser.isEmpty())
return Optional.empty();
var user = possibleUser.orElseThrow();
return accountRepository.findAnAccountOfUser(user);
}
Mais, alors le map
méthode de Optional
frappe notre esprit! Au lieu de vérifier possibleUser.isEmpty()
on pourrait juste map
l’utilisateur, s’il est présent, à un compte :
public Optional<Account> findAccountByUserId(int userId) {
return userRepository
.findById(userId)
.map(accountRepository::findAnAccountOfUser);
}
Nous nous retrouvons avec une erreur de compilation car accountRepository.findAnAccountOfUser(user)
renvoie un Optional<Account>
tandis que le map
méthode ci-dessus nécessite un Account
. Pour ce cas d’utilisation précis, Optional
fournit un flatMap
méthode, qui aplatit imbriqué Optional
s. Alors, changer map
pour flatMap
travaillerait.
public Optional<Account> findAccountByUserId(int userId) {
return userRepository
.findById(userId)
.flatMap(accountRepository::findAnAccountOfUser);
}
Cool! Préparez-vous pour une mission plus complexe.
Deuxième affectation
Lorsqu’un user
/account
n’est pas trouvé, au lieu de retourner un vide optional
que diriez-vous d’indiquer exactement ce qui n’a pas été trouvé: utilisateur ou compte ?
Nous pourrions aborder ce problème de plusieurs façons :
Lancer des exceptions
Nous pourrions définir des exceptions personnalisées, à savoir. UserNotFoundException
et AccountNotFoundException
et jetez ceux-ci :
public Account findAccountByUserIdX(int userId) {
var user = userRepository.findById(userId).orElseThrow(UserNotFoundException::new);
return accountRepository.findAnAccountOfUser(user).orElseThrow(AccountNotFoundException::new);
}
Cependant, l’utilisation d’exceptions pour les cas attendus est considérée comme un anti-modèle: Googling vous permettra d’obtenir de nombreux articles sur le sujet. Alors évitons ça.
Utiliser une interface de résultat
Une autre approche serait de retourner une coutume Result
objet au lieu de retourner Optional
; c’est à dire. Result findAnAccountByUserId(Integer userId)
. Le résultat serait une interface qui serait implémentée par des classes d’erreurs personnalisées, ainsi que Account
et User
.
Utilisez l’un ou l’autre
Une troisième approche, que je trouve plus simple, consiste à retourner un Either
au lieu de Optional
. Alors qu’un Optional
détient zéro ou un valeur, une Either
détient l’un des deux valeurs. Il est généralement utilisé pour contenir soit un erreur ou un succès valeur.
Contrairement à Optional
vous n’obtenez pas de Java Either
mise en œuvre hors de la boîte. Il y a pas mal de bibliothèques. Je préfère utiliser jbock-java/soit parce que c’est léger et simple.
Alors, définissons d’abord le error
interface et classe :
interface Error {}
record UserNotFound() implements Error {}
record AccountNotFound() implements Error {}
Essayons maintenant de coder :
public Either<Error, Account> findAccountByUserId(int userId) {
...
}
Avez-vous remarqué ci-dessus que nous avons utilisé Error
comme le gauche paramètre générique, alors que Account
comme le droite un? Ce n’était pas accidentel : la convention lors de l’utilisation Either
est-ce le gauche est utilisé pour les erreurs tandis que le le droit est utilisé pour le succès valeurs.
Either
a une API fonctionnelle similaire comme Optional
. Par exemple, nous avons map
et flatMap
pour cartographier les valeurs de réussite, alors que nous avons mapLeft
et flatMapLeft
pour cartographier les erreurs. Nous avons aussi des méthodes utilitaires comme Either.left(value)
et Either.right(value)
créer Either
objets. Jetez un œil à son API. Il possède de nombreuses fonctionnalités intéressantes pour la programmation fonctionnelle.
Ainsi, poursuivant notre chemin, nous pourrions d’abord créer un either
avoir le user
ou error
comme ci-dessous :
public Either<Error, Account> findAccountByUserId(int userId) {
var eitherErrorOrUser = userRepository
.findById(userId)
.map(Either::<Error, User>right)
.orElse(Either.left(new UserNotFound()))
...
}
Les lignes 4 et 5 ci-dessus convertissent un Optional<User>
pour Either<UserNotFound, User>
. Parce que convertir un Optional
à un Either
serait un cas d’utilisation courant, codons une méthode utilitaire pour cela :
public class EitherUtils {
public static <L, R> Either<L, R> of(Optional<R> possibleValue, Supplier<L> errorSupplier) {
return possibleValue.map(Either::<L, R>right).orElseGet(() -> Either.<L, R>left(errorSupplier.get()));
}
}
Il faut le optional
Et un errorSupplier
. Le errorSupplier
est utilisé pour composer l’erreur si le optional
est vide.
En l’utilisant, notre code ressemble maintenant à ceci :
public Either<Error, Account> findAccountByUserId(int userId) {
var eitherErrorOrUser = EitherUtils
.<Error, User>of(userRepository.findById(userId), UserNotFound::new)
...
}
Ensuite, comme ci-dessus, eitherErrorOrUser
pourrait être mappé pour un compte de la même manière. La solution complète ressemblerait alors à ceci :
public Either<Error, Account> findAccountByUserId(int userId) {
return EitherUtils
.<Error, User>of(userRepository.findById(userId), UserNotFound::new)
.flatMap(user -> of(accountRepository.findAnAccountOfUser(user), AccountNotFound::new));
}
Ça a l’air mignon, n’est-ce pas ?
Résumé
Envisagez d’utiliser les capacités de programmation fonctionnelle de Java dans la mesure du possible et rendez votre code mignon et concis !