parameter - java function example lambda
Usa Java lambda en lugar de ''if else'' (5)
Con Java 8, tengo este código:
if(element.exist()){
// Do something
}
Quiero convertir al estilo lambda,
element.ifExist(el -> {
// Do something
});
con un método ifExist
como este:
public void ifExist(Consumer<Element> consumer) {
if (exist()) {
consumer.accept(this);
}
}
Pero ahora tengo otros casos a los que llamar:
element.ifExist(el -> {
// Do something
}).ifNotExist(el -> {
// Do something
});
Puedo escribir un ifNotExist
similar, y quiero que se ifNotExist
mutuamente (si la condición exist
es verdadera, no hay necesidad de verificar ifNotExist
, porque a veces, el método exist () requiere mucha carga de trabajo), pero siempre tengo para comprobar dos veces. ¿Cómo puedo evitar eso?
Tal vez la palabra "existe" hace que alguien mal entienda mi idea. Puedes imaginar que también necesito algunos métodos:
ifVisible()
ifEmpty()
ifHasAttribute()
Mucha gente dijo que esto es una mala idea, pero:
En Java 8 podemos usar lambda forEach en lugar de un bucle for
tradicional. En la programación for
y if
hay dos controles básicos de flujo. Si podemos usar lambda para un bucle for
, ¿por qué usar lambda para una mala idea?
for (Element element : list) {
element.doSomething();
}
list.forEach(Element::doSomething);
En Java 8, hay Optional
con ifPresent, similar a mi idea de ifExist:
Optional<Elem> element = ...
element.ifPresent(el -> System.out.println("Present " + el);
Y sobre el mantenimiento y la legibilidad del código, ¿qué opinas si tengo el siguiente código con muchas cláusulas simples de repetición?
if (e0.exist()) {
e0.actionA();
} else {
e0.actionB();
}
if (e1.exist()) {
e0.actionC();
}
if (e2.exist()) {
e2.actionD();
}
if (e3.exist()) {
e3.actionB();
}
Comparar con:
e0.ifExist(Element::actionA).ifNotExist(Element::actionB);
e1.ifExist(Element::actionC);
e2.ifExist(Element::actionD);
e3.ifExist(Element::actionB);
¿Cual es mejor? Y, ay, ¿te das cuenta de que en el código tradicional de la cláusula if
, hay un error en:
if (e1.exist()) {
e0.actionC(); // Actually e1
}
Creo que si usamos lambda, podemos evitar este error!
El problema
(1) Parece que se mezclan diferentes aspectos: control de flujo y lógica de dominio .
element.ifExist(() -> { ... }).otherElementMethod();
^ ^
control flow method business logic method
(2) No está claro cómo deben comportarse los métodos después de un método de flujo de control (como ifExist
, ifNotExist
). ¿Se deben ejecutar siempre o se deben llamar solo bajo la condición (similar a ifExist
)?
(3) El nombre ifExist
implica una operación de terminal, por lo que no hay nada que devolver, void
. Un buen ejemplo es void ifPresent(Consumer)
de Optional
.
La solución
Escribiría una clase completamente separada que sería independiente de cualquier clase concreta y cualquier condición específica.
La interfaz es simple y consta de dos métodos de flujo de control sin contexto: ifTrue
y ifFalse
.
Puede haber algunas formas de crear un objeto Condition
. Escribí un método de fábrica estático para su instancia (por ejemplo, element
) y condición (por ejemplo, Element::exist
).
public class Condition<E> {
private final Predicate<E> condition;
private final E operand;
private Boolean result;
private Condition(E operand, Predicate<E> condition) {
this.condition = condition;
this.operand = operand;
}
public static <E> Condition<E> of(E element, Predicate<E> condition) {
return new Condition<>(element, condition);
}
public Condition<E> ifTrue(Consumer<E> consumer) {
if (result == null)
result = condition.test(operand);
if (result)
consumer.accept(operand);
return this;
}
public Condition<E> ifFalse(Consumer<E> consumer) {
if (result == null)
result = condition.test(operand);
if (!result)
consumer.accept(operand);
return this;
}
public E getOperand() {
return operand;
}
}
Además, podemos integrar la Condition
en el Element
:
class Element {
...
public Condition<Element> formCondition(Predicate<Element> condition) {
return Condition.of(this, condition);
}
}
El patrón que estoy promoviendo es:
- trabajar con un
Element
; - obtener una
Condition
; - controlar el flujo por la
Condition
; - cambiar de nuevo a la
Element
; - Continúa trabajando con el
Element
.
El resultado
Obtención de una Condition
por Condition.of
.
Element element = new Element();
Condition.of(element, Element::exist)
.ifTrue(e -> { ... })
.ifFalse(e -> { ... })
.getOperand()
.otherElementMethod();
Obtención de una Condition
por Element#formCondition
:
Element element = new Element();
element.formCondition(Element::exist)
.ifTrue(e -> { ... })
.ifFalse(e -> { ... })
.getOperand()
.otherElementMethod();
Actualización 1:
Para otros métodos de prueba, la idea sigue siendo la misma.
Element element = new Element();
element.formCondition(Element::isVisible);
element.formCondition(Element::isEmpty);
element.formCondition(e -> e.hasAttribute(ATTRIBUTE));
Actualización 2:
Es una buena razón para repensar el diseño del código. Ninguno de los 2 fragmentos es grande.
Imagina que necesitas actionC
dentro de e0.exist()
. ¿Cómo se cambiaría el método de referencia Element::actionA
?
Se convertiría de nuevo en un lambda:
e0.ifExist(e -> { e.actionA(); e.actionC(); });
a menos que envuelva actionA
y actionC
en un solo método (lo que suena horrible):
e0.ifExist(Element::actionAAndC);
La lambda ahora es incluso menos "legible" que el if
.
e0.ifExist(e -> {
e0.actionA();
e0.actionC();
});
¿Pero cuánto esfuerzo haríamos para hacer eso? ¿Y cuánto esfuerzo pondremos en mantenerlo todo?
if(e0.exist()) {
e0.actionA();
e0.actionC();
}
Como algunas sugerencias, tengo una idea de almacenar en caché el valor booleano:
public class Element {
private Boolean exist;
public void ifExist(Consumer<Element> consumer) {
if (exist == null) {
exist = exist();
}
if (exist) {
consumer.accept(this);
}
}
public void ifNotExist(Consumer<Element> consumer) {
if (exist == null) {
exist = exist();
}
if (!exist) {
consumer.accept(this);
}
}
}
Puedes usar un solo método que tome dos consumidores:
public void ifExistOrElse(Consumer<Element> ifExist, Consumer<Element> orElse) {
if (exist()) {
ifExist.accept(this);
} else {
orElse.accept(this);
}
}
Entonces llámalo con:
element.ifExistOrElse(
el -> {
// Do something
},
el -> {
// Do something else
});
Se llama una ''interfaz fluida'' . Simplemente cambia el tipo de return this;
y return this;
para permitirte encadenar los métodos:
public MyClass ifExist(Consumer<Element> consumer) {
if (exist()) {
consumer.accept(this);
}
return this;
}
public MyClass ifNotExist(Consumer<Element> consumer) {
if (!exist()) {
consumer.accept(this);
}
return this;
}
Podría obtener un poco más sofisticado y devolver un tipo intermedio:
interface Else<T>
{
public void otherwise(Consumer<T> consumer); // ''else'' is a keyword
}
class DefaultElse<T> implements Else<T>
{
private final T item;
DefaultElse(final T item) { this.item = item; }
public void otherwise(Consumer<T> consumer)
{
consumer.accept(item);
}
}
class NoopElse<T> implements Else<T>
{
public void otherwise(Consumer<T> consumer) { }
}
public Else<MyClass> ifExist(Consumer<Element> consumer) {
if (exist()) {
consumer.accept(this);
return new NoopElse<>();
}
return new DefaultElse<>(this);
}
Uso de la muestra:
element.ifExist(el -> {
//do something
})
.otherwise(el -> {
//do something else
});
Ya que casi pero no coincide realmente con Opcional , tal vez podría reconsiderar la lógica:
Java 8 tiene una expresividad limitada:
Optional<Elem> element = ...
element.ifPresent(el -> System.out.println("Present " + el);
System.out.println(element.orElse(DEFAULT_ELEM));
Aquí el map
podría restringir la vista en el elemento:
element.map(el -> el.mySpecialView()).ifPresent(System.out::println);
Java 9:
element.ifPresentOrElse(el -> System.out.println("Present " + el,
() -> System.out.println("Not present"));
En general las dos ramas son asimétricas.