ifpresent - optional list java 8
¿Cómo usar Java 8 Optionals, realizando una acción si los tres están presentes? (10)
Bueno, tomé el mismo enfoque de Federico para llamar solo al DB cuando era necesario, también es bastante detallado, pero vago . También simplifiqué esto un poco. Teniendo en cuenta que tiene estos 3 métodos:
public static Optional<String> firstCall() {
System.out.println("first call");
return Optional.of("first");
}
public static Optional<String> secondCall() {
System.out.println("second call");
return Optional.empty();
}
public static Optional<String> thirdCall() {
System.out.println("third call");
return Optional.empty();
}
Lo he implementado así:
firstCall()
.flatMap(x -> secondCall().map(y -> Stream.of(x, y))
.flatMap(z -> thirdCall().map(n -> Stream.concat(z, Stream.of(n)))))
.ifPresent(st -> System.out.println(st.collect(Collectors.joining("|"))));
Tengo un código (simplificado) que usa Java Optionals:
Optional<User> maybeTarget = userRepository.findById(id1);
Optional<String> maybeSourceName = userRepository.findById(id2).map(User::getName);
Optional<String> maybeEventName = eventRepository.findById(id3).map(Event::getName);
maybeTarget.ifPresent(target -> {
maybeSourceName.ifPresent(sourceName -> {
maybeEventName.ifPresent(eventName -> {
sendInvite(target.getEmail(), String.format("Hi %s, $s has invited you to $s", target.getName(), sourceName, meetingName));
}
}
}
No hace falta decir que esto se ve y se siente mal.
Pero no puedo pensar en otra forma de hacer esto de una manera menos anidada y más legible.
Pensé en transmitir los 3 opcionales, pero descarté la idea de hacer un
.filter(Optional::isPresent)
luego un
.map(Optional::get)
siente aún peor.
Entonces, ¿hay una forma mejor y más ''Java 8'' o ''Alfabetizada opcional'' de tratar esta situación (esencialmente, todas las Opcionales múltiples necesarias para calcular una operación final )?
Creo que deberías considerar tomar otro enfoque.
Comenzaría por no emitir las tres llamadas al DB al principio. En cambio, emitiría la primera consulta y solo si el resultado está presente, emitiría la segunda. Luego aplicaría el mismo razonamiento con respecto a la tercera consulta y, finalmente, si el último resultado también está presente, enviaría la invitación. Esto evitaría llamadas innecesarias a la base de datos cuando alguno de los dos primeros resultados no está presente.
Para hacer que el código sea más legible, comprobable y mantenible, también extraería cada llamada de DB a su propio método privado, encadenándolas con
Optional.ifPresent
:
public void sendInvite(Long targetId, Long sourceId, Long meetingId) {
userRepository.findById(targetId)
.ifPresent(target -> sendInvite(target, sourceId, meetingId));
}
private void sendInvite(User target, Long sourceId, Long meetingId) {
userRepository.findById(sourceId)
.map(User::getName)
.ifPresent(sourceName -> sendInvite(target, sourceName, meetingId));
}
private void sendInvite(User target, String sourceName, Long meetingId) {
eventRepository.findById(meetingId)
.map(Event::getName)
.ifPresent(meetingName -> sendInvite(target, sourceName, meetingName));
}
private void sendInvite(User target, String sourceName, String meetingName) {
String contents = String.format(
"Hi %s, $s has invited you to $s",
target.getName(),
sourceName,
meetingName);
sendInvite(target.getEmail(), contents);
}
Creo que transmitir los tres s
Optional
es una exageración, ¿por qué no lo simple?
if (maybeTarget.isPresent() && maybeSourceName.isPresent() && maybeEventName.isPresent()) {
...
}
En mi opinión, esto establece la lógica condicional más claramente en comparación con el uso de la API de flujo.
Dado que el código original se está ejecutando por sus efectos secundarios (enviando un correo electrónico) y no extrayendo o generando un valor, las llamadas anidadas
ifPresent
parecen apropiadas.
El código original no parece tan malo, y de hecho parece bastante mejor que algunas de las respuestas que se han propuesto.
Sin embargo, la declaración lambdas y las variables locales de tipo
Optional
parecen agregar una buena cantidad de desorden.
Primero, me tomaré la libertad de modificar el código original envolviéndolo en un método, dando a los parámetros nombres agradables y creando algunos nombres de tipo. No tengo idea si el código real es así, pero esto realmente no debería sorprender a nadie.
// original version, slightly modified
void inviteById(UserId targetId, UserId sourceId, EventId eventId) {
Optional<User> maybeTarget = userRepository.findById(targetId);
Optional<String> maybeSourceName = userRepository.findById(sourceId).map(User::getName);
Optional<String> maybeEventName = eventRepository.findById(eventId).map(Event::getName);
maybeTarget.ifPresent(target -> {
maybeSourceName.ifPresent(sourceName -> {
maybeEventName.ifPresent(eventName -> {
sendInvite(target.getEmail(), String.format("Hi %s, %s has invited you to %s",
target.getName(), sourceName, eventName));
});
});
});
}
Jugué con diferentes refactorizaciones, y descubrí que extraer la declaración interna lambda en su propio método tiene más sentido para mí. Dados los usuarios de origen y destino y un evento, sin material opcional, envía un correo al respecto. Este es el cálculo que debe realizarse después de que se hayan abordado todas las cosas opcionales. También he movido la extracción de datos (correo electrónico, nombre) aquí en lugar de mezclarlo con el procesamiento Opcional en la capa externa. Nuevamente, esto tiene sentido para mí: enviar correo desde el origen al destino sobre el evento .
void setupInvite(User target, User source, Event event) {
sendInvite(target.getEmail(), String.format("Hi %s, %s has invited you to %s",
target.getName(), source.getName(), event.getName()));
}
Ahora, tratemos con las cosas opcionales.
Como dije anteriormente,
ifPresent
es el camino a seguir aquí, ya que queremos hacer algo con efectos secundarios.
También proporciona una forma de "extraer" el valor de un Opcional y vincularlo a un nombre, pero solo dentro del contexto de una expresión lambda.
Dado que queremos hacer esto para tres Opcionales diferentes, se requiere anidar.
El anidamiento permite que los nombres de las lambdas externas sean capturados por las lambdas internas.
Esto nos permite vincular nombres a valores extraídos de los opcionales, pero solo si están presentes.
Esto realmente no se puede hacer con una cadena lineal, ya que sería necesaria una estructura de datos intermedia como una tupla para generar los resultados parciales.
Finalmente, en la lambda más interna, llamamos al método auxiliar definido anteriormente.
void inviteById(UserId targetId, UserId sourceID, EventId eventId) {
userRepository.findById(targetId).ifPresent(
target -> userRepository.findById(sourceID).ifPresent(
source -> eventRepository.findById(eventId).ifPresent(
event -> setupInvite(target, source, event))));
}
Tenga en cuenta que he incluido los opcionales en lugar de mantenerlos en variables locales.
Esto revela la estructura de anidación un poco mejor.
También proporciona un "cortocircuito" de la operación si una de las búsquedas no encuentra nada, ya que
ifPresent
simplemente no hace nada en un Opcional vacío.
Sin embargo, todavía es un poco denso para mí.
Creo que la razón es que este código todavía depende de algunos repositorios externos en los que realizar las búsquedas.
Es un poco incómodo mezclar esto con el procesamiento opcional.
Una posibilidad es simplemente extraer las búsquedas en sus propios métodos
findUser
y
findEvent
.
Estos son bastante obvios, así que no los escribiré.
Pero si esto se hiciera, el resultado sería:
void inviteById(UserId targetId, UserId sourceID, EventId eventId) {
findUser(targetId).ifPresent(
target -> findUser(sourceID).ifPresent(
source -> findEvent(eventId).ifPresent(
event -> setupInvite(target, source, event))));
}
Básicamente, esto no es tan diferente del código original. Es subjetivo, pero creo que prefiero esto al código original. Tiene la misma estructura bastante simple, aunque anidada en lugar de la típica cadena lineal de procesamiento opcional. Lo que es diferente es que las búsquedas se realizan condicionalmente dentro del procesamiento Opcional, en lugar de realizarse por adelantado, almacenarse en variables locales y luego solo se realiza una extracción condicional de los valores Opcionales. Además, he separado la manipulación de datos (extracción de correo electrónico y nombre, envío de mensajes) en un método separado. Esto evita mezclar la manipulación de datos con el procesamiento opcional, que creo que tiende a confundir las cosas si estamos tratando con múltiples instancias opcionales.
El primer enfoque no es perfecto (no admite la pereza; las 3 llamadas a la base de datos se activarán de todos modos):
Optional<User> target = userRepository.findById(id1);
Optional<String> sourceName = userRepository.findById(id2).map(User::getName);
Optional<String> eventName = eventRepository.findById(id3).map(Event::getName);
if (Stream.of(target, sourceName, eventName).anyMatch(obj -> !obj.isPresent())) {
return;
}
sendInvite(target.get(), sourceName.get(), eventName.get());
El siguiente ejemplo es un poco detallado, pero admite pereza y legibilidad:
private void sendIfValid() {
Optional<User> target = userRepository.findById(id1);
if (!target.isPresent()) {
return;
}
Optional<String> sourceName = userRepository.findById(id2).map(User::getName);
if (!sourceName.isPresent()) {
return;
}
Optional<String> eventName = eventRepository.findById(id3).map(Event::getName);
if (!eventName.isPresent()) {
return;
}
sendInvite(target.get(), sourceName.get(), eventName.get());
}
private void sendInvite(User target, String sourceName, String eventName) {
// ...
}
Puede usar lo siguiente si desea apegarse a
Optional
y no comprometerse a consumir el valor de inmediato.
Utiliza
Triple<L, M, R>
de Apache Commons:
/**
* Returns an optional contained a triple if all arguments are present,
* otherwise an absent optional
*/
public static <L, M, R> Optional<Triple<L, M, R>> product(Optional<L> left,
Optional<M> middle, Optional<R> right) {
return left.flatMap(l -> middle.flatMap(m -> right.map(r -> Triple.of(l, m, r))));
}
// Used as
product(maybeTarget, maybeSourceName, maybeEventName).ifPresent(this::sendInvite);
Uno podría imaginar un enfoque similar para dos o múltiples
Optional
, aunque desafortunadamente Java no tiene un tipo de tupla general (todavía).
Qué tal algo como esto
if(Stream.of(maybeTarget, maybeSourceName,
maybeEventName).allMatch(Optional::isPresent))
{
sendinvite(....)// do get on all optionals.
}
Una vez dicho esto.
Si su lógica para buscar en la base de datos es solo enviar correo, entonces tal
maybeTarget.ifPresent()
es falso, entonces no tiene sentido buscar los otros dos valores, ¿no?
Me temo, esta lógica solo se puede lograr a través de declaraciones tradicionales si no.
Si trata
Optional
solo como un marcador para los valores de retorno del método, el código se vuelve muy simple:
User target = userRepository.findById(id1).orElse(null);
User source = userRepository.findById(id2).orElse(null);
Event event = eventRepository.findById(id3).orElse(null);
if (target != null && source != null && event != null) {
String message = String.format("Hi %s, %s has invited you to %s",
target.getName(), source.getName(), event.getName());
sendInvite(target.getEmail(), message);
}
El punto de
Optional
no es que deba usarlo en todas partes.
En cambio, sirve como marcador de los valores de retorno del método para informar a la persona que llama que verifique la ausencia.
En este caso, el
orElse(null)
se encarga de esto, y el código de llamada es plenamente consciente de la posible nulidad.
Usando una función auxiliar, las cosas al menos quedan un poco anidadas:
@FunctionalInterface
interface TriConsumer<T, U, S> {
void accept(T t, U u, S s);
}
public static <T, U, S> void allOf(Optional<T> o1, Optional<U> o2, Optional<S> o3,
TriConsumer<T, U, S> consumer) {
o1.ifPresent(t -> o2.ifPresent(u -> o3.ifPresent(s -> consumer.accept(t, u, s))));
}
allOf(maybeTarget, maybeSourceName, maybeEventName,
(target, sourceName, eventName) -> {
/// ...
});
La desventaja obvia es que necesitaría una sobrecarga de la función auxiliar por cada número diferente de s
Optional
return userRepository.findById(id)
.flatMap(target -> userRepository.findById(id2)
.map(User::getName)
.flatMap(sourceName -> eventRepository.findById(id3)
.map(Event::getName)
.map(eventName-> createInvite(target, sourceName, eventName))))
En primer lugar, devuelve un Opcional también. Es mejor tener primero un método que cree una invitación, al que pueda llamar y luego enviar si no está vacío.
Entre otras cosas, es más fácil de probar. Con flatMap también obtienes el beneficio de la pereza, ya que si el primer resultado está vacío, no se evaluará nada más.
Cuando desee usar múltiples opciones, siempre debe usar una combinación de map y flatMap.
Tampoco estoy usando target.getEmail () y target.getName (), esos deben extraerse de forma segura en el método createInvite, ya que no sé si pueden ser nulos o no.