Является ли мое использование предикатов здесь плохой практикой или лишним?

#java #predicate #naming

Вопрос:

Я создаю карточную игру, и, возможно, я немного переутомляюсь. До того, как у меня появились методы pileHasMostSpades() и pileHasMostSevens() . Эти методы имели один и тот же код, за исключением условия if и целочисленного значения. Я смог объединить эти методы в один ( pileHasAtLeast() ), добавив предикат и целочисленный параметр.

pileHasAtLeast(count, predicate) — проверяет, есть ли по крайней мере [количество] карт, соответствующих предикату.

Кроме того, каковы были бы альтернативные или, возможно, лучшие названия для этого нового стиля метода ( pileHasAtLeast() )? Спасибо.

 public int computeScore()
{
    int score = scopas;
    
    final Predicate<Card> spades = c -> c.getSuit() == Card.Suit.SPADES;
    final Predicate<Card> sevens = c -> c.getValue() == Card.Value.SEVEN;
    
    score  = pile.size() > 20 ? 1 : 0;
    score  = pileHasAtLeast(6, spades) ? 1 : 0;
    score  = pileHasAtLeast(3, sevens) ? 1 : 0;
    score  = hasSevenOfSpades() ? 1 : 0;
    
    return score;
}

private boolean pileHasAtLeast(int count, Predicate<Card> predicate)
{
    int result = 0;
    
    for(Card card : pile)
    {
        if(predicate.test(card))
        {
            result  ;
        }
    }
    
    return result > count;
}
 

Комментарии:

1. для этого вам не обязательно нужен специальный метод, вы можете написать pile.stream().filter(c -> c.getSuit() == Card.Suit.SPADES).count() > 6

2. также ваш предикат был бы (на мой взгляд) лучше назван как isSpade , чем spades

3. и строго в отношении названия метода: это не «по крайней мере», это «больше, чем» (если только это не ошибка, и это должно быть >= вместо > )

4. Оригинальные методы, вероятно, делают код более читабельным (хотя это только мое мнение). Но вы, конечно, могли бы делегировать эти методы Predicate методу-accepting, чтобы избежать дублирования кода.

5. @njzk2 Да, это ошибка, которую я исправил сразу после публикации, спасибо!

Ответ №1:

Я провел некоторый рефакторинг вашего кода

 public int computeScore()
{
   //here we count all of the conditions that are true (won't work if you need to add different score for each condition)
   return scopas   Stream.of(pile.size() > 20,
                             pileHasAtLeast(6, c -> c.getSuit() == Card.Suit.SPADES),
                             pileHasAtLeast(3, c -> c.getValue() == Card.Value.SEVEN),
                             pileHasAtLeast(1, c -> c.getValue() == Card.Value.SEVEN) 
                                            amp;amp; c.getSuit() == Card.Suit.SPADES)
                       .filter(condition -> condition)
                       .count();
}


private boolean pileHasAtLeast(int count, Predicate<Card> predicate)
{
    return count <= pile.stream()
         .filter(predicate)
         .count();
}
 

Несколько советов:

  • Не смешивайте императивные циклы, такие как «для», с функциональным программированием. Используйте потоки всякий раз, когда у вас нет побочных эффектов.
  • Не храните предикаты, которые вы используете только один раз в переменной, вы теряете силу функционального программирования.

Комментарии:

1. Отвечая на ваш вопрос: использование предикатов-хорошая практика. В вашем случае лучше использовать их встраиваемыми.

2. Хороший рефакторинг, хотя я не уверен насчет второго совета и стиля в вашем коде. Я использовал переменные, чтобы сделать свой код более читабельным. Я смотрю на ваш код, и его трудно прочитать из-за всех вкладок и необъяснимых условий. Я хотел назвать предикаты так, чтобы они могли быть быстрее поняты читателями. Является ли плохая читаемость следствием функционального программирования или есть лучшее решение?

3. @Picarrow Я не нахожу это менее читабельным. Может быть, ты просто не привык к функциональности, я не знаю. Форматирование, однако, может быть изменено на то, что вам больше подходит.

4. Это определенно правда! Я еще не привык к функциональности. Но я не уверен, что есть лучший способ отформатировать его lol. Думаю, мне придется к этому привыкнуть.

Ответ №2:

Если у вас есть молоток, все выглядит как гвоздь.

Так обстоит дело и здесь. Предикаты хороши, но здесь, особенно в score = ... ? 1 : 0; строках, полная логика низкого уровня сжата в определенную форму.

Было бы лучше написать код, а затем снова добавить абстракции.

Я все равно не стал бы списывать предикаты. Но не на карточке, а на стопке.

Если вы хотите, чтобы ваш счет был объяснен (для новичков в игре), отдельные предикаты вместе с его счетом и пояснительным текстом "* 1 Point: " "more than 6 spades"

Заимствование замены потока @nzjk2:

 record ScoreRule(int score, Predicate<Pile>, String explanation);

ScoreRule[] scoreRules = {
    new ScoreRule(1,
        pile -> pile.size() > 20,
        "more than 20 cards"),
    new ScoreRule(1,
        pile -> pile.stream().filter(c -> c.getSuit() == Card.Suit.SPADES).count() > 6,
        "more than 6 spades"),
    new ScoreRule(...),
    ...
};
... score counting and reporting ...
 

Причина, по которой это работает лучше, заключается в том, что нужно обрабатывать кучу в целом, а поток имеет большую логику оценки и выразительные возможности запросов, такие как количество, содержит, все совпадения, ни одного совпадения.

Комментарии:

1. Интересный подход! Хотя, как бы я подсчитал счет? Выполните итерацию через ScoreRule[] и протестируйте предикат для каждого правила?

2. @Picarrow да, и снова возможен поток массива, фильтрация по истинным предикатам и суммирование результатов. Но повторение-это нормально. Мне нравится этот декларативный подход: модель логики высокого уровня.

3. Теперь это безумно мощно, я должен начать думать так же, лол! Спасибо за понимание!