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

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

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

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

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 items, int position, int min, int max) { return ((position >= min && position  min && items.get(position - 1).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; } 

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

如果是嵌套/拆分:

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

摊牌

因此,为了看看我最喜欢哪种方法,我将几个建议的解决方案应用于上面提到的复杂的targetFound示例。 这是我的结果:

如果是嵌套/拆分,有意义的名称非常详细,有意义的名称在这里并没有真正帮助可读性

 private boolean targetFound1(String target, List items, int position, int min, int max) { boolean result; boolean inWindow = position >= min && position  min && items.get(position - 1).equals(target); result = foundInOddPosition; } } else { boolean beforeWindow = 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 items, int position, int min, int max) { boolean result; if ((position >= min && position  min && items.get( position - 1).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 items, int position, int min, int max) { if ((position >= min && position  min && items.get( position - 1).equals(target); // odd position } } else if ((position = max)) { // after window return items.get(items.size() - 1).equals(target); } else { return false; } } 

提取的方法导致类中的无意义方法参数传递很烦人

 private boolean targetFound4(String target, List 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 items, int position, int max) { return (position >= max && items.get(items.size() - 1).equals(target)); } private boolean foundBefore(String target, List items, int position, int min) { return (position < min && items.get(0).equals(target)); } private boolean foundInWindow(String target, List items, int position, int min, int max) { return (position >= min && position  min && items.get(position - 1).equals(target))); } 

重新审视了BooleanExpression包装器,注意方法参数必须在这个复杂的情况下被声明为final,详细程度是可以防御的IMO也许闭包会使这更容易,如果他们就此达成一致( –

 private boolean targetFound5(final String target, final List 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  min && items.get(position - 1).equals(target); } }; BooleanExpression foundBefore = 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 items; private final int position; private final int min; private final int max; public IsTargetFoundExpression(String target, List 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  min && items.get(position - 1).equals(target); } private boolean foundBefore() { return position = max && items.get(items.size() - 1).equals(target); } } 

逻辑很复杂,足以保证一个单独的类(和unit testing,耶!)。 它将使使用此逻辑的代码更易读,并在其他地方需要此逻辑时促进重用。 我认为这是一个很好的课程,因为它确实只有一个责任,只有最后的字段。

我发现换行和空白做得非常好,实际上:

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

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

您可以使用早期返回(从方法)来实现相同的效果:

[应用了一些修复]

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

如果您的目标是可读性,为什么不简单地划分界限并添加评论?

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

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

你必须在if中进行变量赋值。

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

翻译成

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

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

您的第一个解决方案是有利于这种复杂性。 如果条件更复杂,我会为您需要运行的每个检查编写私有方法。 就像是:

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

这个问题是从2009年开始的,但在将来(Java 8),我们将能够像布尔表达式一样使用可能用于此上下文的Lambda表达式,但您可以使用它,以便仅在需要时对它们进行评估。

 public static void main2(String[] args) { Callable argsNotNull = () -> args != null; Callable argsLengthOk = () -> args.length == 2; Callable argsAreNotEqual = () -> !args[0].equals(args[1]); if (argsNotNull.call() && argsLengthOk.call() && argsAreNotEqual.call()) { System.out.println("Args are ok"); } } 

您可以使用java 5/6执行相同操作,但使用匿名类编写效率较低且更加过分。

已有一些好的解决方案,但这是我的变化。 我通常将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"); } } 

将其拆分为单独的函数(以提高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函数中使用早期返回来提高可读性的想法。

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

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

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

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

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

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

第一段代码真的没什么问题; 你是在思考东西,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()