2009-12-18 3 views
7

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

Может ли кто-нибудь придумать опрятное решение для этого?

Смотрите ниже код для примеров того, что я имею в виду:

public class BooleanEvaluator { 

    // problem: complex boolean expression, hard to read 
    public static void main1(String[] args) { 

     if (args != null && args.length == 2 && !args[0].equals(args[1])) { 
      System.out.println("Args are ok"); 
     } 
    } 

    // solution: simplified by splitting up and using meaningful names 
    // problem: no short circuit evaluation 
    public static void main2(String[] args) { 

     boolean argsNotNull = args != null; 
     boolean argsLengthOk = args.length == 2; 
     boolean argsAreNotEqual = !args[0].equals(args[1]); 

     if (argsNotNull && argsLengthOk && argsAreNotEqual) { 
      System.out.println("Args are ok"); 
     } 
    } 

    // solution: wrappers to delay the evaluation 
    // problem: verbose 
    public static void main3(final String[] args) { 

     abstract class BooleanExpression { 
      abstract boolean eval(); 
     } 

     BooleanExpression argsNotNull = new BooleanExpression() { 
      boolean eval() { 
       return args != null; 
      } 
     }; 

     BooleanExpression argsLengthIsOk = new BooleanExpression() { 
      boolean eval() { 
       return args.length == 2; 
      } 
     }; 

     BooleanExpression argsAreNotEqual = new BooleanExpression() { 
      boolean eval() { 
       return !args[0].equals(args[1]); 
      } 
     }; 

     if (argsNotNull.eval() && argsLengthIsOk.eval() && argsAreNotEqual.eval()) { 
      System.out.println("Args are ok"); 
     } 
    } 
} 

Ответ на ответы:

Спасибо за все ваши идеи! Следующие варианты были представлены до сих пор:

  • прерывистых линий и добавлять комментарии
  • оставить как есть
  • методы Экстракт
  • Ранние возвращается
  • Уплотненный/Разделить, если это

Разделите строки и добавьте комментарии:

Простое добавление строк в состояние отменяется форматированием кода в Eclipse (ctrl + shift + f). Встроенные комментарии помогают с этим, но оставляют мало места на каждой строке и могут привести к уродливой упаковке. В простых случаях этого может быть достаточно.

Оставить как:

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

private boolean targetFound(String target, List<String> items, 
     int position, int min, int max) { 

    return ((position >= min && position < max && ((position % 2 == 0 && items 
      .get(position).equals(target)) || (position % 2 == 1) 
      && position > min && items.get(position - 1).equals(target))) 
      || (position < min && items.get(0).equals(target)) || (position >= max && items 
      .get(items.size() - 1).equals(target))); 
} 

Я не рекомендовал бы оставить это как есть.

Extract методы:

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

private static boolean lengthOK(String[] args) { 
    return args.length == 2; 
} 

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

Что я пытался достичь с помощью подхода BooleanExpression, так это то, что логика остается локальной. Обратите внимание, что даже объявление BooleanExpression является локальным (я не думаю, что раньше я встречался с прецедентом для объявления локального класса!).

Ранние возвращения:

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

public static boolean areArgsOk(String[] args) { 

    check_args: { 
     if (args == null) { 
      break check_args; 
     } 
     if (args.length != 2) { 
      break check_args; 
     } 
     if (args[0].equals(args[1])) { 
      break check_args; 
     } 
     return true; 
    } 
    return false; 
} 

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

Уплотненные/разделить, если это:

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

Showdown

Так, чтобы увидеть, какой подход я utlimately пользу, я применил некоторые из предложенных решений в сложном пример targetFound, представленный выше. Вот мои результаты:

вложенные/расщепленный, если-х, с осмысленными именами очень многословные, значащие имена не реально помочь читаемости здесь

private boolean targetFound1(String target, List<String> items, 
     int position, int min, int max) { 

    boolean result; 
    boolean inWindow = position >= min && position < max; 
    if (inWindow) { 

     boolean foundInEvenPosition = position % 2 == 0 
       && items.get(position).equals(target); 
     if (foundInEvenPosition) { 
      result = true; 
     } else { 
      boolean foundInOddPosition = (position % 2 == 1) 
        && position > min 
        && items.get(position - 1).equals(target); 
      result = foundInOddPosition; 
     } 
    } else { 
     boolean beforeWindow = position < min; 
     if (beforeWindow) { 

      boolean matchesFirstItem = items.get(0).equals(target); 
      result = matchesFirstItem; 
     } else { 

      boolean afterWindow = position >= max; 
      if (afterWindow) { 

       boolean matchesLastItem = items.get(items.size() - 1) 
         .equals(target); 
       result = matchesLastItem; 
      } else { 
       result = false; 
      } 
     } 
    } 
    return result; 
} 

вложенных/разделенной, если х, с комментариями менее многословным, но все же трудно читать и легко создавать ошибки

private boolean targetFound2(String target, List<String> items, 
     int position, int min, int max) { 

    boolean result; 
    if ((position >= min && position < max)) { // in window 

     if ((position % 2 == 0 && items.get(position).equals(target))) { 
      // even position 
      result = true; 
     } else { // odd position 
      result = ((position % 2 == 1) && position > min && items.get(
        position - 1).equals(target)); 
     } 
    } else if ((position < min)) { // before window 
     result = items.get(0).equals(target); 
    } else if ((position >= max)) { // after window 
     result = items.get(items.size() - 1).equals(target); 
    } else { 
     result = false; 
    } 
    return result; 
} 

рано возвращается еще более компактной, но условное дерево остается столь же сложной

private boolean targetFound3(String target, List<String> items, 
     int position, int min, int max) { 

    if ((position >= min && position < max)) { // in window 

     if ((position % 2 == 0 && items.get(position).equals(target))) { 
      return true; // even position 
     } else { 
      return (position % 2 == 1) && position > min && items.get(
        position - 1).equals(target); // odd position 
     } 
    } else if ((position < min)) { // before window 
     return items.get(0).equals(target); 
    } else if ((position >= max)) { // after window 
     return items.get(items.size() - 1).equals(target); 
    } else { 
     return false; 
    } 
} 

Извлеченные методы приводит к абсурдным методов в классе прохождение параметр является раздражающим

private boolean targetFound4(String target, List<String> items, 
     int position, int min, int max) { 

    return (foundInWindow(target, items, position, min, max) 
      || foundBefore(target, items, position, min) || foundAfter(
      target, items, position, max)); 
} 

private boolean foundAfter(String target, List<String> items, int position, 
     int max) { 
    return (position >= max && items.get(items.size() - 1).equals(target)); 
} 

private boolean foundBefore(String target, List<String> items, 
     int position, int min) { 
    return (position < min && items.get(0).equals(target)); 
} 

private boolean foundInWindow(String target, List<String> items, 
     int position, int min, int max) { 
    return (position >= min && position < max && ((position % 2 == 0 && items 
      .get(position).equals(target)) || (position % 2 == 1) 
      && position > min && items.get(position - 1).equals(target))); 
} 

логическое выражение обертки Обратите внимание, что параметры метода должны быть объявлены окончательными для этого сложного случае подробность оборонять IMO Возможно закрытие будет сделать это легче, если они когда-либо согласятся на это (-

private boolean targetFound5(final String target, final List<String> items, 
     final int position, final int min, final int max) { 

    abstract class BooleanExpression { 
     abstract boolean eval(); 
    } 

    BooleanExpression foundInWindow = new BooleanExpression() { 

     boolean eval() { 
      return position >= min && position < max 
        && (foundAtEvenPosition() || foundAtOddPosition()); 
     } 

     private boolean foundAtEvenPosition() { 
      return position % 2 == 0 && items.get(position).equals(target); 
     } 

     private boolean foundAtOddPosition() { 
      return position % 2 == 1 && position > min 
        && items.get(position - 1).equals(target); 
     } 
    }; 

    BooleanExpression foundBefore = new BooleanExpression() { 
     boolean eval() { 
      return position < min && items.get(0).equals(target); 
     } 
    }; 

    BooleanExpression foundAfter = new BooleanExpression() { 
     boolean eval() { 
      return position >= max 
        && items.get(items.size() - 1).equals(target); 
     } 
    }; 

    return foundInWindow.eval() || foundBefore.eval() || foundAfter.eval(); 
} 

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

Спасибо за все ваши данные!

EDIT: поздняя мысль. Это может быть даже лучше создать специальный класс для сложной логики, таких как это:

import java.util.ArrayList; 
import java.util.List; 

public class IsTargetFoundExpression { 

    private final String target; 
    private final List<String> items; 
    private final int position; 
    private final int min; 
    private final int max; 

    public IsTargetFoundExpression(String target, List<String> items, int position, int min, int max) { 
     this.target = target; 
     this.items = new ArrayList(items); 
     this.position = position; 
     this.min = min; 
     this.max = max; 
    } 

    public boolean evaluate() { 
     return foundInWindow() || foundBefore() || foundAfter(); 
    } 

    private boolean foundInWindow() { 
     return position >= min && position < max && (foundAtEvenPosition() || foundAtOddPosition()); 
    } 

    private boolean foundAtEvenPosition() { 
     return position % 2 == 0 && items.get(position).equals(target); 
    } 

    private boolean foundAtOddPosition() { 
     return position % 2 == 1 && position > min && items.get(position - 1).equals(target); 
    } 

    private boolean foundBefore() { 
     return position < min && items.get(0).equals(target); 
    } 

    private boolean foundAfter() { 
     return position >= max && items.get(items.size() - 1).equals(target); 
    } 
} 

Логика достаточно, чтобы гарантировать отдельный класс комплекса (и модульное тестирование, яй!).Он сохранит код, используя эту логику, более читабельным и будет способствовать повторному использованию, если эта логика понадобится в другом месте. Я думаю, что это хороший класс, потому что он действительно имеет одну ответственность и только конечные поля.

+0

Мне нравится ваше первое решение, однако при правильном присвоении имен переменных оно может и не понадобиться. Второе решение - слишком многословная ИМО. – BacMan

ответ

6

Я считаю, переносы строк и пробелы делают очень хорошую работу, на самом деле:

public static void main1(String[] args) { 

    if (args != null 
     && args.length == 2 
     && !args[0].equals(args[1]) 
     ) { 
      System.out.println("Args are ok"); 
    } 
} 

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

Я иногда даже комментировать отдельные биты:

public static void main1(String[] args) { 

    if (args != null    // Must have args 
     && args.length == 2   // Two of them, to be precise 
     && !args[0].equals(args[1]) // And they can't be the same 
     ) { 
      System.out.println("Args are ok"); 
    } 
} 

Если вы действительно хотите назвать вещи, несколько if s сделает это:

public static void main1(String[] args) { 

    if (args != null) { 
     if (args.length == 2) { 
      if (!args[0].equals(args[1])) { 
       System.out.println("Args are ok"); 
      } 
     } 
    } 
} 

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

+0

Несколько, если это наиболее естественное и ясное решение. Он точно показывает, что происходит. Вы также можете добавлять комментарии к каждой строке. Но зачем вам оптимизировать компилятор? Нечего рушиться. – PauliL

+0

Комментарии к каждой строке имеют дополнительное преимущество форматирования Eclipse, не изменяя разрывов строк. –

+0

Глубокое гнездование в большинстве случаев считается плохой практикой. Однако я не уверен, является ли этот конкретный случай исключением. Однако он действительно улучшает читаемость. – BalusC

6

Если ваша цель - читаемость, почему бы не просто сломать строки и добавить комментарии?

if (args != null    // args not null 
     && args.length == 2   // args length is OK 
     && !args[0].equals(args[1]) // args are not equal 
    ) { 

     System.out.println("Args are ok"); 
    } 
+0

Вы изменили свою логику там ... –

+0

Мой плохой. Исправлено :-) –

+0

Я исправил оставшийся бит –

11

Вы можете использовать ранние возвращения (от метода) для достижения того же эффекта:

[Некоторые исправления применяются]

public static boolean areArgsOk(String[] args) { 
    if(args == null) 
     return false; 

    if(args.length != 2) 
     return false; 

    if(args[0].equals(args[1])) 
     return false; 

    return true; 
    } 

    public static void main2(String[] args) { 

     boolean b = areArgsOk(args); 
     if(b) 
      System.out.println("Args are ok"); 
    } 
+0

Вы забыли передать в args isArgsOk(). – BalusC

+0

Вы правы. Благодарю. Исправлена. –

+0

Внимание! У него есть && - условия. Использовать if (args == null) return false; if (args.length! = 2) возвращает false; if (args [0] .equals (args [1])) возвращает false; return true; – r3zn1k

0

Разделить его в отдельную функцию (для улучшения читаемости в основном()) и добавьте комментарий (чтобы люди понимали, что вы пытаетесь выполнить)

public static void main(String[] args) { 
    if (argumentsAreValid(args)) { 
      System.out.println("Args are ok"); 
    } 
} 


public static boolean argumentsAreValid(String[] args) { 
    // Must have 2 arguments (and the second can't be the same as the first) 
    return args == null || args.length == 2 || !args[0].equals(args[1]); 
} 

ETA: Мне также нравится идея Итай об использовании ранних возвратов в функции ArgumentsAreValid для улучшения читаемости.

+0

Спасибо за ваш ответ. Я действительно не вижу, как извлечение метода таким образом улучшает ситуацию. Добавление комментария полезно. О да, «bool» должен быть логическим, а имена методов начинаются с строчных букв в Java. –

+0

@Adriaan - исправлен код там - слишком хорошо знакомы с C# и fogetting моей Java. Лично я считаю, что его расщепление делает его более читаемым, но это, безусловно, субъективное суждение. –

-1

В первой части кода нет ничего плохого; вы слишком задумываетесь, ИМО.

Вот еще один способ, который, будучи подробным, легко понять.

static void usage() { 
    System.err.println("Usage: blah blah blah blah"); 
    System.exit(-1); 
} 

// ... 

if (args == null || args.length < 2) 
    usage(); 
if (args[0].equals(args[1])) 
    usage() 
+0

Хотя это не делает то же самое в присутствии менеджера безопасности. –

+0

Мой пример был прост для краткости. Рассмотрим действительно сложное условие. Вышеприведенное разделение в двух условных высказываниях кажется мне произвольным. –

+0

Да, конечно, это произвольно! Вы пишете код, чтобы люди читали, поэтому вы делаете суждения о том, что является самым ясным. –

2

Тип BooleanExpression не кажется достаточно полезным, чтобы быть классом самостоятельно вне вашего основного класса, и он также добавляет интеллектуальный вес вашему приложению. Я просто просто напишу частные методы, которые называются надлежащим образом для выполнения проверок, которые вы хотите. Это намного проще.

+0

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

+0

Я не согласен. Использование частных методов намного проще для понимания, чем внедрение класса в метод. Фактически, способ BooleanExpression определен, вы можете просто использовать частные методы. Не только это, но и потому, что они являются частными, их можно изменить позже. Если бы это был такой язык, как Groovy или Python, я бы сказал, передайте метод (или закрытие) для проверки валидации. Поскольку это не так, лучше всего держать вещи простыми (люди, которые должны поддержать это по линии, будут благодарны вам за это). BooleanExpression - попытка имитировать замыкания (или параметры метода). –

2

Вы должны выполнить назначение переменной INSIDE.

if (a && b && c) ... 

переводит

calculatedA = a; 
if (calculatedA) { 
    calculatedB = b; 
    if (calculatedB) { 
    calculatedC = c; 
    if (calculatedC) .... 
    } 
} 

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

1

Ваше первое решение подходит для такого рода сложностей. Если условия были более сложными, я бы написал частные методы для каждой проверки, которую вы должны выполнить.Что-то вроде:

public class DemoStackOverflow { 

    public static void main(String[] args) { 
    if (areValid(args)) { 
     System.out.println("Arguments OK"); 
    } 
    } 

    /** 
    * Validation of parameters. 
    * 
    * @param args an array with the parameters to validate. 
    * @return true if the arguments are not null, the quantity of arguments match 
    * the expected quantity and the first and second are not equal; 
    *   false, otherwise. 
    */ 
    private static boolean areValid(String[] args) { 
     return notNull(args) && lengthOK(args) && areDifferent(args); 
    } 

    private static boolean notNull(String[] args) { 
     return args != null; 
    } 

    private static boolean lengthOK(String[] args) { 
     return args.length == EXPECTED_ARGS; 
    } 

    private static boolean areDifferent(String[] args) { 
     return !args[0].equals(args[1]); 
    } 

    /** Quantity of expected arguments */ 
    private static final int EXPECTED_ARGS = 2; 

} 
+0

Это иллюстрация моего предложенного ответа. +1 –

0

Некоторые хорошие решения уже есть, но вот мои варианты. Обычно я делаю нулевую проверку как верхнюю позицию охраны во всех (соответствующих) методах. Если я сделаю это в этом случае, он оставит только проверку длины и равенства в последующем if, что уже можно считать достаточным сокращением сложности и/или улучшением удобочитаемости?

public static void main1(String[] args) { 

     if (args == null) return; 

     if (args.length == 2 && !args[0].equals(args[1])) { 
      System.out.println("Args are ok"); 
     } 
    } 
+0

Прагматичный, но недостаточный для сложного случая, я думаю. –

0

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

boolean argsNotNull = args != null; 
// argsNotNull==false, okay 
boolean argsLengthOk = args.length == 2; 
// blam! null pointer exception 

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

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

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

if (salesType=='A' && isValidCustomerForSalesTypeA(customerid)) 
etc 

Edit: Как я бы сломайте более сложный пример, который вы дали.

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

if (position < min) 
{ 
    return (items.get(0).equals(target)); 
} 
else if (position >= max) 
{ 
    return (items.get(items.size() - 1).equals(target)); 
} 
else // position >=min && < max 
{ 
    if (position % 2 == 0) 
    { 
    return items.get(position).equals(target); 
    } 
    else // position % 2 == 1 
    { 
    return position > min && items.get(position - 1).equals(target); 
    } 
} 

Это кажется мне доступным для чтения, поскольку оно, скорее всего, получит. В этом примере условие «верхнего уровня», очевидно, является отношением положения к min и max, поэтому, по-моему, это может помочь прояснить ситуацию.

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

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

return s == null? -1: s.length();

я иногда говорю себе, да, я понимаю, но другие не будут, может быть, лучше написать

if (s==null) 
    return -1; 
    else 
    return s.length(); 
+0

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

+0

@Adriaan: Хорошо. У меня создалось впечатление, что вы считаете, что это просто дело эффективности, а не «не работает». Ну, я думаю, что мой ответ правильный, даже если вы уже это знали! – Jay

+0

Итак, как бы вы представили более сложное условие условия, которое я добавил в верхней части? –

1

Этот вопрос является с 2009 года, но в будущем (Java 8), мы сможем используйте выражения Lambda, которые могут быть для этого контекста точно так же, как логические выражения, но вы можете использовать его, чтобы они оценивались только тогда, когда это необходимо.

public static void main2(String[] args) { 

    Callable<Boolean> argsNotNull =() -> args != null; 
    Callable<Boolean> argsLengthOk =() -> args.length == 2; 
    Callable<Boolean> argsAreNotEqual =() -> !args[0].equals(args[1]); 

    if (argsNotNull.call() && argsLengthOk.call() && argsAreNotEqual.call()) { 
     System.out.println("Args are ok"); 
    } 
} 

Вы можете сделать то же самое с Java 5/6, но он менее эффективен и гораздо более ulgy писать анонимные классы.

Смежные вопросы