这是一种常见的情况,有许多常见的处理方法。这是我尝试得出的标准答案。如果我错过了任何事情,请发表评论,我将保持最新状态。
这是一支箭
您正在讨论的被称为箭头反模式。之所以称为箭头,是因为嵌套的ifs链形成了代码块,这些代码块向右扩展,然后向左扩展,形成一个可视的箭头,“指向”代码编辑器窗格的右侧。
用后卫拉平箭
这里讨论了一些避免“箭”的常见方法。最常见的方法是使用保护模式,其中代码首先处理异常流,然后处理基本流,例如
if (ok)
{
DoSomething();
}
else
{
_log.Error("oops");
return;
}
...你会用...
if (!ok)
{
_log.Error("oops");
return;
}
DoSomething(); //notice how this is already farther to the left than the example above
如果有很长的警卫队,这会使代码变得更加平坦,因为所有警卫队都一直出现在左侧,而您的if则没有嵌套。此外,您还可以将逻辑条件与其相关的错误进行直观地配对,从而更容易知道发生了什么:
箭头:
ok = DoSomething1();
if (ok)
{
ok = DoSomething2();
if (ok)
{
ok = DoSomething3();
if (!ok)
{
_log.Error("oops"); //Tip of the Arrow
return;
}
}
else
{
_log.Error("oops");
return;
}
}
else
{
_log.Error("oops");
return;
}
守卫:
ok = DoSomething1();
if (!ok)
{
_log.Error("oops");
return;
}
ok = DoSomething2();
if (!ok)
{
_log.Error("oops");
return;
}
ok = DoSomething3();
if (!ok)
{
_log.Error("oops");
return;
}
ok = DoSomething4();
if (!ok)
{
_log.Error("oops");
return;
}
这在客观上和数量上都更易于阅读,因为
- 给定逻辑块的{和}字符靠得更近
- 理解特定线条所需的精神背景量较小
- 与if条件相关联的整个逻辑更可能在一页上
- 大大减少了编码人员滚动页面/眼睛轨迹的需要
如何在末尾添加通用代码
保护模式的问题在于它依赖于所谓的“机会主义回报”或“机会主义退出”。换句话说,它打破了每个函数都应该恰好具有一个退出点的模式。这是一个问题,有两个原因:
- 它以某种错误的方式给人们带来了麻烦,例如,学会使用Pascal进行编码的人们已经知道一个函数=一个出口点。
- 它没有提供无论退出什么都会在退出时执行的代码部分,而这是当前的主题。
下面,我通过使用语言功能或完全避免了此问题,提供了一些解决此限制的选项。
选项1。您不能这样做:使用 finally
不幸的是,作为c ++开发人员,您不能这样做。但这是包含finally关键字的语言的第一答案,因为这正是它的用途。
try
{
if (!ok)
{
_log.Error("oops");
return;
}
DoSomething(); //notice how this is already farther to the left than the example above
}
finally
{
DoSomethingNoMatterWhat();
}
选项2.避免出现此问题:重组功能
您可以通过将代码分成两个函数来避免此问题。该解决方案的优势是可以使用任何语言,此外,它还可以降低循环复杂性,这是降低缺陷率并提高任何自动化单元测试特异性的行之有效的方法。
这是一个例子:
void OuterFunction()
{
DoSomethingIfPossible();
DoSomethingNoMatterWhat();
}
void DoSomethingIfPossible()
{
if (!ok)
{
_log.Error("Oops");
return;
}
DoSomething();
}
选项3.语言技巧:使用伪造的循环
我看到的另一个常见技巧是使用while(true)和break,如其他答案所示。
while(true)
{
if (!ok) break;
DoSomething();
break; //important
}
DoSomethingNoMatterWhat();
尽管这不像使用那样“诚实” goto
,但它在重构时不容易被弄乱,因为它清楚地标记了逻辑范围的边界。剪切并粘贴您的标签或goto
语句的幼稚编码器可能会导致严重问题!(坦率地说,这种模式现在是如此普遍,我认为它清楚地传达了意图,因此根本不是“不诚实的”)。
此选项还有其他变体。例如,可以使用switch
代替while
。任何带有break
关键字的语言构造都可能有效。
选项4.利用对象生命周期
另一种方法是利用对象生命周期。使用上下文对象携带您的参数(我们的朴素示例可能缺少的内容)并在完成后将其处理。
class MyContext
{
~MyContext()
{
DoSomethingNoMatterWhat();
}
}
void MainMethod()
{
MyContext myContext;
ok = DoSomething(myContext);
if (!ok)
{
_log.Error("Oops");
return;
}
ok = DoSomethingElse(myContext);
if (!ok)
{
_log.Error("Oops");
return;
}
ok = DoSomethingMore(myContext);
if (!ok)
{
_log.Error("Oops");
}
//DoSomethingNoMatterWhat will be called when myContext goes out of scope
}
注意:请确保您了解所选语言的对象生命周期。您需要某种确定性的垃圾回收才能正常工作,即,您必须知道何时调用析构函数。在某些语言中,您将需要使用Dispose
而不是析构函数。
选项4.1。利用对象生命周期(包装模式)
如果您打算使用面向对象的方法,那么最好也做对了。此选项使用一个类来“包装”需要清除的资源及其其他操作。
class MyWrapper
{
bool DoSomething() {...};
bool DoSomethingElse() {...}
void ~MyWapper()
{
DoSomethingNoMatterWhat();
}
}
void MainMethod()
{
bool ok = myWrapper.DoSomething();
if (!ok)
_log.Error("Oops");
return;
}
ok = myWrapper.DoSomethingElse();
if (!ok)
_log.Error("Oops");
return;
}
}
//DoSomethingNoMatterWhat will be called when myWrapper is destroyed
同样,请确保您了解对象的生命周期。
选项5.语言技巧:使用短路评估
另一种技术是利用短路评估。
if (DoSomething1() && DoSomething2() && DoSomething3())
{
DoSomething4();
}
DoSomethingNoMatterWhat();
该解决方案利用了&&运算符的工作方式。当&&的左侧评估为false时,将永远不会评估右侧。
当需要紧凑代码并且代码不太可能需要太多维护时(例如,您正在实现一种众所周知的算法),此技巧最有用。对于更通用的编码,此代码的结构太脆弱了;即使对逻辑进行很小的更改也可能会触发完全重写。