嵌套if语句的替代方案是什么?

3

我有编程经验,但对软件开发的经验不多。我正在为我所在的公司编写一款软件,并且我已经开始挑战自己的代码可读性。

我想知道这是否是嵌套if语句的“有效”替代方案,或者是否有更好的解决方法。

假设我有以下方法:

public void someMethod()
{
    if (some condition)
    {
        if (some condition 2)
        {
            if (some condition 3)
            {
                // ...etc all the way until:
                doSomething();
            }
            else
            {
                System.err.println("Specific Condition 3 Error");
            }
        }
        else
        {
            System.err.println("Specific Condition 2 Error");
        }
    }
    else
    {
        System.err.println("Specific Condition 1 Error");
    }
}

现在,我首先要指出的是,在这种情况下,将条件组合起来(使用&&)是不可能的,因为每个条件都有一个独特的错误需要报告,如果我将它们组合起来,我就无法做到这一点(或者我可以吗?)。在任何人对我尖叫“SWITCH语句!”之前,我应该指出的第二件事是,并非所有这些条件都可以通过switch语句处理;有些是特定于对象的方法调用,有些是整数比较等等。
话虽如此,以下是否是使上述代码更易读的有效方法,或者是否有更好的方法呢?
public void someMethod()
{
    if (!some condition)
    {
        System.err.println("Specific Condition 1 Error");
        return;
    }

    if (!some condition 2)
    {
        System.err.println("Specific Condition 2 Error");
        return;
    }

    if (!some condition 3)
    {
        System.err.println("Specific Condition 3 Error");
        return;
    }

    doSomething();
}

基本上,我们不是在else块中检查条件并报告错误,而是检查条件的反义词,并在其为真时返回。结果应该是相同的,但是否有更好的处理方法?
8个回答

2

我会把它写成

if (failing condition) {
    System.err.println("Specific Condition 1 Error");
} else {
    somethingExpensiveCondition2and3Dependon();
    if (failing condition 2)
        System.err.println("Specific Condition 2 Error");
    else if (failing condition 3)
        System.err.println("Specific Condition 3 Error");
    else
        doSomething();
}

如果我没错的话,如果我想在条件检查之间执行代码,这种方法就不起作用了,对吧?除此之外,这是一个不错的做法。 - Rsaesha
我怀疑您想在这些检查之间放置的代码可以放在它们之前。 - Peter Lawrey
它可能会,但在某些情况下,代码执行需要很长时间。出于效率的考虑,如果条件1无论如何都会失败,我不想执行许多取决于条件2和3的代码。 - Rsaesha
我已经添加了一个处理此问题的示例。 - Peter Lawrey
嗯,我明白了。但是如果条件3依赖于条件2没有的东西,我们又回到了最初的状态(嵌套的if语句!)。我喜欢你的解决方案,在大多数情况下它是最好的选择,但不幸的是我的情况并不适用。 - Rsaesha
然后你回到了你刚才提到的最后一个例子。 - Peter Lawrey

2

是的,在两种情况下你的代码都存在条件复杂性味道(代码味道)。

Java 是一种面向对象编程语言,因此你的代码应该根据面向对象设计原则进行重构,类似于以下示例:

for (Condition cond : conditions) {
    if (cond.happens(params))
         cond.getHandler().handle(params);
}

应该将条件列表注入到此类中,这样当添加或删除新条件时,该类不会更改。(开闭原则


2

你的第二个方法相当不错。如果你想要更加繁琐一点的做法,可以将你的条件移到Callable对象中。每个对象也可以提供处理错误的方法。这样,你就可以写出任意长的测试系列而不会牺牲功能。

class Test {
    private final Callable<Boolean> test;
    private final Runnable errorHandler;

    public Test(Callable<Boolean> test, Runnable handler) {
        this.test = test;
        errorHandler = handler;
    }

    public boolean runTest() {
        if (test.call()) {
            return true;
        }
        errorHandler.run();
        return false;
    }
}

您可以按以下方式组织您的代码:
ArrayList<Test> tests;

public void someMethod() {
    for (Test test : tests) {
        if (!test.runTest()) {
            return;
        }
    }
    doSomething();
}

编辑

这是上文的更通用版本。它可以处理几乎任何此类型的情况。

public class Condition {
    private final Callable<Boolean> test;
    private final Runnable passHandler;
    private final Runnable failHandler;

    public Condition(Callable<Boolean> test,
            Runnable passHandler, Runnable failHandler)
    {
        this.test = test;
        this.passHandler = passHandler;
        this.failHandler = failHandler;
    }

    public boolean check() {
        if (test.call()) {
            if (passHandler != null) {
                passHandler.run();
            }
            return true;
        }
        if (errorHandler != null) {
            errorHandler.run();
        }
        return false;
    }
}

public class ConditionalAction {
    private final ArrayList<Condition> conditions;
    private final Runnable action;

    public ConditionalAction(ArrayList<Condition> conditions,
            Runnable action)
    {
        this.conditions = conditions;
        this.action = action;
    }

    public boolean attemptAction() {
    for (Condition condition : conditions) {
        if (!condition.check()) {
            return false;
        }
    }
    action.run();
    return true;
    }
}

人们可能会想要添加一些通用数据,以便传递信息或收集结果。但我建议在实现条件和操作的对象内部实现此数据共享,并保留此结构。


那么如果我有几种不同类型的条件(因此需要不同的runTest()方法),我只需为每种类型创建一个继承Test类的类?这非常有启发性,谢谢! - Rsaesha
@Rsaesha - 这是一种方法,虽然runTest()非常通用。另一种方法可能是进一步参数化Test类--例如,除了错误处理程序之外,还有一个通过处理程序。(为了对称,我可能会将errorHandler重命名为failHandler。)这样,您可以将每个测试的通过与一个动作关联起来。 - Ted Hopp

2
如果我特别追求准确性,我会使用类似这样的东西。
boolean c1, c2, c3;

public void someMethod() {
  boolean ok = true;
  String err = "";

  if (ok && !(ok &= c1)) {
    err = "Specific Condition 1 Error";
  }

  if (ok && !(ok &= c2)) {
    err = "Specific Condition 2 Error";
  }

  if (ok && !(ok &= c3)) {
    err = "Specific Condition 3 Error";
  }

  if ( ok ) {
    doSomething();
  } else {
    System.out.print(err);
  }
}

现在您是单一退出并且扁平化的。

补充

如果 &= 对您来说很困难,请使用类似以下的内容:

  if (ok && !c3) {
    err = "Specific Condition 3 Error";
    ok = false;
  }

这些条件很棒。我想我以前从未使用过 &= 操作符! - Rsaesha
1
我记得在C语言中,你可以这样写 if (!(ok &= cond)) { 或者类似的方式,这样更简洁,但是我的C语言现在有点生疏了。 - OldCurmudgeon
3
单一出口和扁平化,但您已经将显而易见的东西转化为需要人们再三思考的内容。 - yshavit
@yshavit ...直到你意识到它在做什么,那么它就像任何范式一样简单明显。 它消除了箭头味道,而不引入多个退出的味道。 它还有助于调试-在每个“err =”行上设置断点并永远留在那里。 - OldCurmudgeon
@保罗 确实,一年后再次查看这段代码时,你需要记住这个奇怪的模式。或者,有人遇到你的代码并需要弄清楚它。如果每个人都这样做,那就没什么问题了;但这不是很符合Java编码习惯,尽管一个好的Java开发者可以弄清楚你在做什么,但是我的观点是,即使像嵌套if这样基本的东西也会让人想了解一段时间才能理解代码库。 - yshavit
@OldCurmudgeon 小而优雅。这是可以改善编码风格的小范式转变之一。我喜欢它。 - Fritz

1

对于这种情况,这已经是最干净的了,因为您既有自定义条件,又有每个条件的自定义响应。


1

你实际上正在调用doSomething()方法之前验证一些条件。我建议将验证过程提取到一个单独的方法中。

public void someMethod() {
  if (isValid()) {
    doSomething();
  }
}

private boolean isValid() {
  if (!condition1) {
    System.err.println("Specific Condition 1 Error");
    return false;
  }
  if (!condition2) {
    System.err.println("Specific Condition 2 Error");
    return false;
  }
  if (!condition3) {
    System.err.println("Specific Condition 3 Error");
    return false;
  }
  return true;
}

0

不,这就是你在Java中得到的。如果你有太多这样的东西,那么可能意味着你应该进行一些重构,甚至重新考虑你的算法——尝试简化它可能是值得的,否则几个月后你会回到代码中,想知道为什么 a + b + c + d = e 但是 a + b' + c + d = zebra


0
你有第二个选择,这个更易读。虽然通常不建议使用多个返回语句,但将它们全部放在代码开头是清晰的(因为它们并不散布在整个方法中)。而嵌套的if语句则很难跟踪和理解。

我的一些方法可能会在各个地方返回值。如果是这种情况,你认为第二个选项仍然更易读吗? - Rsaesha
@Bombe 在你的代码中,你只需要一个单一的退出点。如果你想了解更多细节,请阅读Bob Martin的《Clean Code》。 - avgvstvs
@avgvstvs,“单一出口”这个概念从一开始就被误解了,因为它从来不是关于从哪里退出,而是关于退出到哪里。因此,为了满足一个从未意味着适用的概念而添加不必要的复杂性……嗯……好吧,我会让你决定,亲爱的读者。 :) - Bombe
@ArnonRotem-Gal-Oz 当你看到“return”时,有什么难以理解的呢?在这种情况下,发生了什么是非常清楚的...除非你通常反向跟踪代码。 - Bombe
@Bombe 我认为添加一个变量并仅返回一次并不比多次返回更复杂。除非你的方法如此简单,以至于你可以使用三元运算符,否则复杂度是相同的... public boolean someMethod(){ int rval = false; if(someCondition){ rval = true; }else{ rval = false; } return rval; } public boolean someMethod(){ if(someCondition){ return true; }else{ return false; } } 一样简单。 - avgvstvs
显示剩余9条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接