Используйте Java lambda вместо 'if else'

С Java 8 у меня есть этот код:

if(element.exist()){
    // Do something
}

Я хочу преобразовать в лямбда-стиль,

element.ifExist(el -> {
    // Do something
});

с помощью метода ifExist:

public void ifExist(Consumer<Element> consumer) {
    if (exist()) {
        consumer.accept(this);
    }
}

Но теперь у меня еще есть случаи, чтобы позвонить:

element.ifExist(el -> {
    // Do something
}).ifNotExist(el -> {
    // Do something
});

Я могу написать аналогичный ifNotExist, и я хочу, чтобы они были взаимоисключающими (если exist условие истинно, нет необходимости проверять ifNotExist, потому что иногда метод exist() требует столько рабочей нагрузки для проверки), но у меня всегда есть проверить два раза. Как я могу избежать этого?

Возможно, слово "существует" заставляет кого-то неправильно понять мою идею. Вы можете себе представить, что мне также нужны некоторые методы:

 ifVisible()
 ifEmpty()
 ifHasAttribute()

Многие говорили, что это плохая идея, но:

В Java 8 можно использовать лямбда Foreach вместо традиционной for цикла. В программировании for и if есть два основных элемента управления потоком. Если мы можем использовать лямбда для for цикла, почему используется лямбда для if плохая идея?

for (Element element : list) {
    element.doSomething();
}

list.forEach(Element::doSomething);

В Java 8 есть Optional с ifPresent, аналогичная моей идее ifExist:

Optional<Elem> element = ...
element.ifPresent(el -> System.out.println("Present " + el);

И о поддержке коды и читабельности, что вы думаете, если у меня есть следующий код со многими повторять простой, if условие?

    if (e0.exist()) {
        e0.actionA();
    } else {
        e0.actionB();
    }

    if (e1.exist()) {
        e0.actionC();
    }

    if (e2.exist()) {
        e2.actionD();
    }

    if (e3.exist()) {
        e3.actionB();
    }

Сравнить с:

    e0.ifExist(Element::actionA).ifNotExist(Element::actionB);
    e1.ifExist(Element::actionC);
    e2.ifExist(Element::actionD);
    e3.ifExist(Element::actionB);

Что лучше? И, oops, вы заметили, что в традиционном коде кода if есть ошибка в:

if (e1.exist()) {
    e0.actionC(); // Actually e1
}

Я думаю, что если мы используем лямбда, мы можем избежать этой ошибки!

Ответ 1

Поскольку это почти, но не совсем соответствует Дополнительно, возможно, вы можете пересмотреть логику:

Java 8 имеет ограниченную выразительность:

Optional<Elem> element = ...
element.ifPresent(el -> System.out.println("Present " + el);
System.out.println(element.orElse(DEFAULT_ELEM));

Здесь map может ограничить представление элемента:

element.map(el -> el.mySpecialView()).ifPresent(System.out::println);

Java 9:

element.ifPresentOrElse(el -> System.out.println("Present " + el,
                        () -> System.out.println("Not present"));

В общем случае две ветки асимметричны.

Ответ 2

Он назвал "свободный интерфейс". Просто измените тип return this; и return this; чтобы вы могли объединить методы:

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;
}

Вы могли бы немного поучаствовать и вернуться к промежуточному типу:

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);
}

Использование образца:

element.ifExist(el -> {
    //do something
})
.otherwise(el -> {
    //do something else
});

Ответ 3

Вы можете использовать один метод, который принимает двух потребителей:

public void ifExistOrElse(Consumer<Element> ifExist, Consumer<Element> orElse) {
    if (exist()) {
        ifExist.accept(this);
    } else {
        orElse.accept(this);
    }
}

Затем назовите его:

element.ifExistOrElse(
  el -> {
    // Do something
  },
  el -> {
    // Do something else
  });

Ответ 4

Эта проблема

(1) Кажется, вы смешиваете разные аспекты - контроль потока и логику домена.

element.ifExist(() -> { ... }).otherElementMethod();
          ^                      ^
        control flow method     business logic method

(2) Неясно, как должны вести себя методы после метода потока управления (например, ifExist, ifNotExist). Должны ли они всегда выполняться или называться только при условии (аналогично ifExist)?

(3) Имя ifExist подразумевает операцию терминала, поэтому возвращать нечего - void. Хорошим примером является void ifPresent(Consumer) из Optional.

Решение

Я бы написал полностью разделенный класс, который был бы независим от любого конкретного класса и любого конкретного условия.

Интерфейс прост и состоит из двух бесконтактных методов управления потоком - ifTrue и ifFalse.

Существует несколько способов создания объекта Condition. Я написал статический заводский метод для вашего экземпляра (например, element) и условия (например, 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;
    }

}

Более того, мы можем интегрировать Condition в Element:

class Element {

    ...

    public Condition<Element> formCondition(Predicate<Element> condition) {
        return Condition.of(this, condition);
    }

}

Модель, которую я продвигаю, такова:

  • работа с Element;
  • получить Condition;
  • контролировать поток по Condition;
  • вернитесь к Element;
  • продолжайте работать с Element.

Результат

Получение Condition по Condition.of:

Element element = new Element();

Condition.of(element, Element::exist)
             .ifTrue(e -> { ... })
             .ifFalse(e -> { ... })
         .getOperand()
             .otherElementMethod();

Получение Condition по Element#formCondition:

Element element = new Element();

element.formCondition(Element::exist)
           .ifTrue(e -> { ... })
           .ifFalse(e -> { ... })
       .getOperand()
           .otherElementMethod();

Обновление 1:

Для других методов тестирования идея остается прежней.

Element element = new Element();

element.formCondition(Element::isVisible);
element.formCondition(Element::isEmpty);
element.formCondition(e -> e.hasAttribute(ATTRIBUTE));

Обновление 2:

Это хорошая причина переосмыслить дизайн кода. Ни один из 2 фрагментов не велик.

Представьте, что вам нужно actionC внутри e0.exist(). Как будет изменен метод ссылки Element::actionA?

Он будет возвращен в лямбду:

e0.ifExist(e -> { e.actionA(); e.actionC(); });

если вы не actionA и actionC в один метод (что звучит ужасно):

e0.ifExist(Element::actionAAndC);

Лямбда теперь даже меньше, читабельная тогда, if была.

e0.ifExist(e -> {
    e0.actionA();
    e0.actionC();
});

Но сколько усилий мы сделаем для этого? И сколько усилий мы будем поддерживать в этом?

if(e0.exist()) {
    e0.actionA();
    e0.actionC();
}

Ответ 5

Если вы выполняете простую проверку объекта, а затем выполняете некоторые операторы, основанные на условии, тогда одним из подходов было бы иметь карту с предикатом в качестве ключа и желаемое выражение в качестве значения, например.

Map<Predicate<Integer>,Supplier<String>> ruleMap = new HashMap<Predicate<Integer>,Supplier<String>>(){{
put((i)-> i<10,()->"Less than 10!");
put((i)-> i<100,()->"Less than 100!");
put((i)-> i<1000,()->"Less than 1000!");
}};

Позднее мы могли бы передать следующую карту, чтобы получить значение, когда предикат возвращает true, что может заменить весь код if/else

        ruleMap.keySet()
               .stream()
               .filter((keyCondition)->keyCondition.test(numItems,version))
               .findFirst()
               .ifPresent((e)-> System.out.print(ruleMap.get(e).get()));

Поскольку мы используем findFirst(), это эквивалентно if/else if/else if......

Ответ 6

Как некоторые предложения, у меня есть идея кэшировать логическое значение:

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);
        }
    }
}