Как разделить сложные условия и сохранить оценку короткого замыкания? - PullRequest
9 голосов
/ 18 декабря 2009

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

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

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

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

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

Методы извлечения:

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

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

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

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

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

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

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

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

Спасибо за ваш вклад!

РЕДАКТИРОВАТЬ: запоздалая мысль. Может быть даже лучше создать специальный класс для сложной логики, такой как этот:

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

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

Ответы [ 11 ]

11 голосов
/ 18 декабря 2009

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

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

  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");
  }
7 голосов
/ 18 декабря 2009

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

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

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

6 голосов
/ 18 декабря 2009

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

    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");
    }
2 голосов
/ 18 декабря 2009

Вы должны назначить переменную INSIDE if.

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

переводится как

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

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

2 голосов
/ 18 декабря 2009

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

1 голос
/ 04 февраля 2014

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

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, но это менее эффективно и намного более неудобно писать с анонимными классами.

1 голос
/ 18 декабря 2009

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

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 голосов
/ 18 декабря 2009

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

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

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

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

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

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

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

  if (s==null)
    return -1;
  else
    return s.length();
0 голосов
/ 18 декабря 2009

Некоторые хорошие решения уже есть, но вот мой вариант. Я обычно делаю нулевую проверку как самое верхнее охранное предложение во всех (соответствующих) методах. Если я сделаю это в этом случае, он оставит только проверку длины и равенства в последующем 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 голосов
/ 18 декабря 2009

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

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

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()
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...