如何分割复杂的条件并保持短路评估?

时间:2009-12-18 15:01:44

标签: java

有时条件可能变得非常复杂,因此为了便于阅读,我通常会将它们拆分并为每个组件指定一个有意义的名称。然而,这会使短路评估失败,这可能造成问题。我想出了一个包装器的方法,但在我看来它太冗长了。

有人可以为此提出一个简洁的解决方案吗?

请参阅下面的代码,了解我的意思:

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

我意识到大多数人讨厌标签和破坏,这种风格可能太不常见,不被认为是可读的。

嵌套/拆分如果:

它允许引入有意义的名称并结合优化评估。缺点是条件语句的复杂树可以随之发生

对决

因此,为了看看我最喜欢哪种方法,我将几个建议的解决方案应用于上面给出的复杂的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;
}

嵌套/拆分if,带注释 更简洁,但仍然难以阅读和容易创建错误

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包装 请注意,方法参数必须声明为final 对于这个复杂的案例,冗长是可以防御的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();
}

我猜这真的取决于情况(一如既往)。对于非常复杂的条件,包装器方法可能是可以防御的,尽管它并不常见。

感谢您的所有投入!

编辑:事后补充。为复杂的逻辑创建一个特定的类可能更好,例如:

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 个答案:

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

答案 1 :(得分:7)

我发现linebreaks和whitespace做得非常好,实际上:

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会这样做:

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

...任何优化编译器都会崩溃。对我来说,这可能有点过于冗长。

答案 2 :(得分: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");
    }

答案 3 :(得分:2)

BooleanExpression类型似乎不足以在主类之外成为一个类,它也为你的应用程序增加了一些智力。我只想编写适当命名的私有方法来运行您想要的检查。它简单得多。

答案 4 :(得分:2)

您必须执行变量赋值 INSIDE if's。

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

转换为

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

无论如何,这通常是有益的,因为它命名了您正在测试的概念,正如您在示例代码中清楚地演示的那样。这增加了可读性。

答案 5 :(得分: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;

}

答案 6 :(得分: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执行相同操作,但使用匿名类编写效率较低且更加过分。

答案 7 :(得分:0)

将其拆分为单独的函数(以提高main()中的可读性)并添加注释(以便人们了解您要完成的任务)

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:我也喜欢Itay在ArgumentsAreValid函数中使用早期返回来提高可读性的想法。

答案 8 :(得分:0)

已有一些好的解决方案,但这是我的变化。我通常将null检查作为所有(相关)方法中最顶层的保护子句。如果我在这种情况下这样做,它只留下后续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");
        }
    }

答案 9 :(得分:0)

您的第一个解决方案在许多情况下都不起作用,包括您在上面提供的示例。如果args为null,那么

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

短路的一个优点是可以节省运行时间。另一个优点是,它允许您进行早期测试,以检查导致以后测试抛出异常的条件。

就个人而言,当测试单独简单且所有这些都变得复杂时,其中有很多,我会投票支持简单的“添加一些换行符和注释”解决方案。这比创建一堆附加功能更容易阅读。

我将事情分解为子程序的唯一一次是单个测试很复杂。如果您需要出去读取数据库或执行大量计算,那么请确保将其转换为子程序,以便顶级代码易于阅读

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

编辑: 我如何分解你给出的更复杂的例子。

当我真正得到这种复杂的条件时,我会尝试将它们分解为嵌套的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);
  }
}

这对我来说似乎是可读的,因为它可能会得到。在这个例子中,“顶级”条件显然是位置与最小值和最大值之间的关系,因此在我看来,打破这一点确实有助于澄清事物。

实际上,上述内容可能比在一行中填写所有内容更有效,因为else允许您减少比较次数。

当我把一个复杂的条件放到一个单独的行中时,我很挣扎。但即使你明白这一点,下一个人出现的可能性也不大。甚至一些相当简单的事情,比如

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

我有时会告诉自己,是的,我明白了,但其他人不会,也许更好写

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

答案 10 :(得分:-1)

第一段代码真的没什么问题;你是在思考东西,IMO。

这是另一种方式,虽然详细,但很容易理解。

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