如何编辑一系列if-else if语句,使其遵循Bob叔叔的“清洁代码”原则?


45

我正在尝试遵循Bob叔叔的干净代码建议,尤其是要使方法简短。

我发现自己无法缩短此逻辑:

if (checkCondition()) {addAlert(1);}
else if (checkCondition2()) {addAlert(2);}
else if (checkCondition3()) {addAlert(3);}
else if (checkCondition4()) {addAlert(4);}

我无法删除其他元素,因此将整个事物分解为较小的部分,导致“其他条件”中的“其他”有助于性能-评估这些条件的成本很高,如果我可以避免评估以下条件,请导致第一个条件是的,我想避免它们。

即使从语义上来讲,从业务的角度来看,如果满足下一个条件,评估下一个条件也是没有意义的。


编辑:此问题被认为是处理if(if else)else优雅方式的可能重复。

我相信这是一个不同的问题(通过比较这些问题的答案,您也可以看到它)。


46
在此上下文中,此代码实际上有什么错误不清楚?我看不到如何缩短或简化它!评估条件的代码已经很好地构造了,作为决策结果调用的方法也是如此。您只需要看下面的一些答案,只会使代码复杂!
史蒂夫

38
这段代码没有错。它非常易读并且易于遵循。您为进一步缩小它所做的任何事情都会增加间接性并使它更难理解。
18年

20
您的代码很好。将剩余的精力投入到比尝试进一步缩短精力更有效的工作中。
罗伯特·哈维

5
如果真的只有4个条件,那很好。如果确实是12或50,那么您可能想在比这种方法更高的级别上进行重构。
JimmyJames

9
完全保留您的代码。听你父母总是告诉你的:不要相信任何叔叔在街上为孩子们提供糖果。@Harvey有趣的是,“改进”代码的各种尝试都使其变得更大,更复杂且可读性更差。
gnasher729

Answers:


81

理想情况下,我认为您应该提取逻辑以将警报代码/编号放入其自己的方法中。因此,您现有的代码会一直减少到

{
    addAlert(GetConditionCode());
}

并且您有GetConditionCode()封装了检查条件的逻辑。使用枚举可能比使用魔术数字更好。

private AlertCode GetConditionCode() {
    if (CheckCondition1()) return AlertCode.OnFire;
    if (CheckCondition2()) return AlertCode.PlagueOfBees;
    if (CheckCondition3()) return AlertCode.Godzilla;
    if (CheckCondition4()) return AlertCode.ZombieSharkNado;
    return AlertCode.None;
}

2
如果可以像您描述的那样封装(我怀疑可能不是,我认为OP为简单起见就省略了变量),它不会更改代码,这本身很好,但是会增加代码的人机工程学性和可读性+ 1
opa

17
有了这些警报代码,我感谢一次只能返回一个代码
Josh Part

12
对于switch语句的使用,这似乎也是一个完美的选择-如果可以用OP语言获得。
Frank Hopkins

4
如果可以将错误代码提取为新方法,那么将其提取为一种新方法可能只是一个好主意,以便在多种情况下有用,而不必提供有关特定情况的大量信息。实际上,有一个权衡和盈亏平衡点是值得的。但是通常您会看到验证序列特定于手头的工作,最好将它们与该工作保持在一起。在这种情况下,发明一种新的类型以告诉代码的另一部分需要做什么,这是不希望的。
PJTraill

6
重新实现的一个问题是,它使函数addAlert需要检查伪造的警报条件AlertCode.None
David Hammen '18

69

重要的度量是代码的复杂性,而不是绝对大小。假定不同的条件实际上只是单个函数调用,就像操作没有比您所显示的要复杂,我会说代码没有什么错。它已经尽可能简单了。

任何进一步“简化”的尝试的确会使事情复杂化。

当然,您可以按照其他人的建议将else关键字替换为a return,但这仅是样式问题,而不是复杂性的任何改变。


在旁边:

我的一般建议是,永远不要对任何干净的代码规则保持虔诚:如果在适当的环境中应用互联网,您在互联网上看到的大多数编码建议都是好的,但是从根本上应用相同的建议可能会赢得您的认可。国际奥委会。诀窍始终是达到平衡,使人们能够轻松地推理您的代码。

使用太大的方法,您会被搞砸。使用的功能太小,您会被拧紧。避免三元表达式,这很容易搞砸。随处使用三元表达式,这很麻烦。意识到有一些地方需要单行功能,而有地方需要50行功能(是的,它们存在!)。请意识到,有一些地方需要if()声明,并且有一些地方需要?:操作员。使用可用的全套武库,并尝试始终使用找到的最合适的工具。请记住,即使对这个建议也不要虔诚。


2
我争辩说,用else if一个内部代码替换return一个简单的if(删除else)可能会使代码更难以阅读。当代码说出时else if,我立即知道下一个块中的代码仅在上一个不执行时才会执行。没有糊涂,没有大惊小怪。如果它是普通格式,if无论是否执行前一个,它都可以执行或不执行。现在,我将不得不花费大量的精力来解析前面的代码块,以注意到它以结尾return。我宁愿花那精力去解析业务逻辑。
CVn

1
我知道,这是一件小事,但至少对我而言,它else if构成了一个语义单元。(这不一定是单个单元的编译器,不过没关系。)...; return; } if (...没有; 如果它分散在多行上,更不用说了。我实际上必须查看一下它在做什么,而不是仅通过查看关键字对就可以直接将其引入else if
CVn

@MichaelKjörlingFull Ack。我个人比较喜欢这种else if结构,尤其是因为它的链接形式是一种众所周知的模式。但是,这种形式的代码if(...) return ...;也是一种众所周知的模式,因此我不会完全谴责这一点。不过,我确实认为这是一个小问题:在两种情况下,控制流逻辑都是相同的,仔细观察一下if(...) { ...; return; }梯子将告诉我它确实等同于else if梯子。我看到一个术语的结构,推断出它的含义,我意识到它在各处都在重复,并且我知道这是怎么回事。
cmaster

从JavaScript / Node.js的来临,有些人会用使用的“腰带和背带”的代码 else if return。例如else if(...) { return alert();}
user949300 '18

1
“记住,即使对这个建议也不要信奉。” +1
像贾里德(Jared)'18

22

对于任何给定情况,这是否比普通情况“好”还存在争议。但是,如果您尝试其他方法,这是一种常用的方法。

将您的条件放入对象中并将这些对象放入列表中

foreach(var condition in Conditions.OrderBy(i=>i.OrderToRunIn))
{
    if(condition.EvaluatesToTrue())
    {
        addAlert(condition.Alert);
        break;
    }
}

如果条件上需要执行多个操作,则可以执行一些疯狂的递归

void RunConditionalAction(ConditionalActionSet conditions)
{
    foreach(var condition in conditions.OrderBy(i=>i.OrderToRunIn))
    {
        if(condition.EvaluatesToTrue())
        {
            RunConditionalAction(condition);
            break;
        }
    }
}

显然是的。仅当您有逻辑模式时,这才有效。如果尝试执行超通用递归条件操作,则对象的设置将与原始的if语句一样复杂。您将发明自己的新语言/框架。

但是你的例子确实有一个模式

此模式的常见用例是验证。代替 :

bool IsValid()
{
    if(condition1 == false)
    {
        throw new ValidationException("condition1 is wrong!");
    }
    elseif(condition2 == false)
    {
    ....

}

成为

[MustHaveCondition1]
[MustHaveCondition2]
public myObject()
{
    [MustMatchRegExCondition("xyz")]
    public string myProperty {get;set;}
    public bool IsValid()
    {
        conditions = getConditionsFromReflection()
        //loop through conditions
    }
}

27
这只会将if...else阶梯移动到Conditions列表的构造中。净增益为负,因为的构造Conditions将占用与OP代码一样多的代码,但是增加的间接性会带来可读性方面的代价。我绝对希望使用干净的编码梯子。
cmaster

3
@cmaster是的,我想我确实说过:“那么对象的设置将和原始的if语句一样复杂……
Ewan

7
这比原始的可读性差。为了弄清楚实际上正在检查什么条件,您需要深入研究代码的其他区域。它增加了不必要的间接级别,使代码更难以理解。
18年

8
将if .. else if .. else ..链转换为谓词和动作表是有意义的,但仅适用于更大的示例。该表增加了一些复杂性和间接性,因此您需要足够的条目来分摊此概念性开销。因此,对于4个谓词/动作对,请保留简单的原始代码,但是如果您有100个,则肯定要与该表一起使用。交叉点介于两者之间。@cmaster,表可以被静态初始化,因此添加谓词/动作对的增量开销仅是一行而已:很难做到更好。
Stephen C. Steel

2
可读性不是个人的。这是编程人员的责任。这是主观的。这就是为什么重要的一点,就是要来到这样的地方,听听编程人员对此有何评论。我个人认为此示例不完整。告诉我如何conditions构造... ARG!不是注释属性!为什么神?哇,我的眼睛!
candied_orange

7

考虑return;在一个条件成功之后使用,它可以为您节省所有else的时间。return addAlert(1)如果该方法具有返回值,您甚至可以直接进行操作。


3
当然,这假设在ifs 链之后没有其他事情发生。这可能是一个合理的假设,然后可能不会。
CVn

5

我见过这样的建筑有时被认为更清洁:

switch(true) {
    case cond1(): 
        statement1; break;
    case cond2():
        statement2; break;
    case cond3():
        statement3; break;
    // .. etc
}

具有适当间距的三元也可以是一个很好的选择:

cond1() ? statement1 :
cond2() ? statement2 :
cond3() ? statement3 : (null);

我猜您也可以尝试创建一个包含条件和函数对的数组,并对其进行迭代,直到满足第一个条件-如我所见,这等于Ewan的第一个答案。


1
三元是整洁的
伊万

6
@Ewan调试损坏的“深度递归三元”可能会带来不必要的痛苦。
dfri

5
它在屏幕上看起来很整洁。
伊万

嗯,什么语言允许使用带有case标签的功能?
Undercat '18

1
@undercat是有效的ECMAScript / JavaScript afaik
zworek '18

1

作为@Ewan答案的一种变体,您可以创建如下条件的(而不是“固定列表”):

abstract class Condition {
  private static final  Condition LAST = new Condition(){
     public void alertOrPropagate(DisplayInterface display){
        // do nothing;
     }
  }
  private Condition next = Last;

  public Condition setNext(Condition next){
    this.next = next;
    return this; // fluent API
  }

  public void alertOrPropagate(DisplayInterface display){
     if(isConditionMeet()){
         display.alert(getMessage());
     } else {
       next.alertOrPropagate(display);
     }
  }
  protected abstract boolean isConditionMeet();
  protected abstract String getMessage();  
}

这样,您可以按定义的顺序应用条件,并且基础架构(显示的抽象类)在满足第一个条件后将跳过其余的条件。

这是优于“固定列表”方法的地方,在平面方法中,您必须在应用条件的循环中实现“跳过”。

您只需设置条件链:

Condition c1 = new Condition1().setNext(
  new Condition2().setNext(
   new Condition3()
 )
);

并通过一个简单的调用开始评估:

c1.alertOrPropagate(display);

是的,这就是所谓的责任链模式
最大

4
我不会假装对任何人说话,但同时在问题的代码是在它的行为立即可读性和明显的,我认为这是为它做什么显而易见。
CVn

0

首先,原始代码并不糟糕。这是可以理解的,并且本质上没有任何坏处。

然后,如果您不喜欢它,请以@Ewan的想法为基础,使用列表但删除其有些不自然的foreach break模式:

public class conditions
{
    private List<Condition> cList;
    private int position;

    public Condition Head
    {
        get { return cList[position];}
    }

    public bool Next()
    {
        return (position++ < cList.Count);
    }
}


while not conditions.head.check() {
  conditions.next()
}
conditions.head.alert()

现在,以您选择的语言进行调整,使列表的每个元素成为一个对象,一个元组,无论如何,您就很好了。

编辑:看起来我想的不太清楚,所以让我进一步解释。conditions是某种有序列表;head是正在研究的当前元素-在开始时是列表的第一个元素,每次next()被调用时,它就成为下一个元素;check()alert()是OP 中的checkConditionX()addAlert(X)


1
(没有下注,但是)我无法遵循。什么是
Belle-Sophie

@Belle我编辑了答案以进一步解释。与Ewan的想法相同,但用while not代替foreach break
Nico

一个绝妙的主意的辉煌演变
Ewan

0

这个问题缺少一些细节。如果条件是:

  • 随时更改或
  • 在应用程序或系统的其他部分重复
  • 在某些情况下(例如不同的版本,测试,部署)进行了修改

或者,如果其中的内容addAlert更复杂,则使用c#可能更好的解决方案是:

//in some central spot
IEnumerable<Tuple<Func<bool>, int>> Conditions = new ... {
  Tuple.Create(CheckCondition1, 1),
  Tuple.Create(CheckCondition2, 2),
  ...
}

//at the original place
var matchingCondition = Conditions.Where(c=>c.Item1()).FirstOrDefault();
if(matchingCondition != null) 
  addAlert(matchingCondition.Item2)

元组在c#<8中不是很漂亮,但出于方便而选择。

即使上述选项均不适用,使用此方法的优点是该结构是静态类型的。例如,您不会因遗漏了而不小心搞砸了else


0

在有很多情况的情况下,降低循环复杂性的最好方法if->then statements是使用字典或列表(取决于语言)存储键值(如果语句值或的某个值),然后存储值/函数结果。

例如,代替(C#):

if (i > 10) { return "Two"; }
else if (i > 8) { return "Four" }
else if (i > 4) { return "Eight" }
return "Ten";  //etc etc say anything after 3 or 4 values

我可以简单地

var results = new Dictionary<int, string>
{
  { 10, "Two" },
  { 8, "Four"},
  { 4, "Eight"},
  { 0, "Ten"},
}

foreach(var key in results.Keys)
{
  if (i > results[key]) return results.Values[key];
}

如果您使用的是更现代的语言,则可以存储更多的逻辑,然后只需存储值(c#)。这实际上只是内联函数,但是如果逻辑上讲要内联,则也可以指向其他函数。

var results = new Dictionary<Func<int, bool>, Func<int, string>>
{
  { (i) => return i > 10; ,
    (i) => return i.ToString() },
  // etc
};

foreach(var key in results.Keys)
{ 
  if (key(i)) return results.Values[key](i);
}

0

我正在尝试遵循Bob叔叔的干净代码建议,尤其是要使方法简短。

我发现自己无法缩短此逻辑:

if (checkCondition()) {addAlert(1);}
else if (checkCondition2()) {addAlert(2);}
else if (checkCondition3()) {addAlert(3);}
else if (checkCondition4()) {addAlert(4);}

您的代码已经太短了,但是逻辑本身不应更改。乍一看,您似乎要对进行四个调用来重复自己checkCondition(),并且很明显,在仔细阅读代码后,每个调用都不相同。您应该添加正确的格式和函数名称,例如:

if (is_an_apple()) {
  addAlert(1);
}
else if (is_a_banana()) {
  addAlert(2);
}
else if (is_a_cat()) {
  addAlert(3);
}
else if (is_a_dog()) {
  addAlert(4);
}

您的代码应首先可读。阅读了鲍勃叔叔的几本书之后,我相信这就是他一直在努力传达的信息。


0

假设所有功能都在同一组件中实现,则可以使这些功能保留某些状态,以便摆脱流中的多个分支。

EG:checkCondition1()将变为evaluateCondition1(),它将检查是否满足先前的条件;如果是这样,则它将缓存一些要由检索的值getConditionNumber()

checkCondition2()将变为evaluateCondition2(),它将检查是否满足先前的条件。如果不满足先前的条件,则它将检查条件场景2,以缓存要通过检索的值getConditionNumber()。等等。

clearConditions();
evaluateCondition1();
evaluateCondition2();
evaluateCondition3();
evaluateCondition4();
if (anyCondition()) { addAlert(getConditionNumber()); }

编辑:

为了使此方法有效,这里需要对昂贵的条件进行检查。

bool evaluateCondition34() {
    if (!anyCondition() && A && B && C) {
        conditionNumber = 5693;
        return true;
    }
    return false;
}

...

bool evaluateCondition76() {
    if (!anyCondition() && !B && C && D) {
        conditionNumber = 7658;
        return true;
    }
    return false;
}

因此,如果您有太多要执行的昂贵检查,并且此代码中的内容仍然保密,则此方法有助于维护它,并在必要时可以更改检查的顺序。

clearConditions();
evaluateCondition10();
evaluateCondition9();
evaluateCondition8();
evaluateCondition7();
...
evaluateCondition34();
...
evaluateCondition76();

if (anyCondition()) { addAlert(getConditionNumber()); }

该答案仅是其他答案的替代建议,如果仅考虑4行代码,它可能不会比原始代码更好。虽然,这并不是一个可怕的方法(并且不会像其他人所说的那样使维护变得更加困难),但鉴于我提到的情况(检查太多,只有主要功能公开给公众,所有功能都是同一类的实现细节)。


我不喜欢这个建议-将测试逻辑隐藏在多个函数中。例如,如果您需要更改顺序并在#2之前执行#3,这可能会使代码难以维护。
劳伦斯'18

否。您可以检查是否评估了先前的条件anyCondition() != false
艾默生·卡多佐

1
好的,我明白你的意思了。但是,如果(说)条件2和3都等于true,则OP并不需要条件3的求值。
劳伦斯

我的意思是您可以检查anyCondition() != false功能evaluateConditionXX()。这是可以实现的。如果不希望使用内部状态的方法,我会理解,但是这种方法无效的论点是无效的。
艾默生·卡多佐

1
是的,我的反对意见是它无助地隐藏了测试逻辑,而不是说它无法正常工作。在答案(第3段)中,满足条件1的支票放在eval ... 2()内。但是,如果他在顶层切换条件1和2(由于客户需求的变化等),则您必须进入eval ... 2()来删除条件1的检查,然后进入eval。 ..1()添加对条件2的检查。可以使它起作用,但很容易导致维护问题。
劳伦斯

0

超过两个“ else”子句将迫使代码阅读者遍历整个链条,以找到感兴趣的那个。使用如下方法:void AlertUponCondition(Condition condition){switch(condition){case Condition.Con1:... break; 情况Condition.Con2:... break; 等等...}“条件”是适当的枚举。如果需要,返回布尔值或值。这样称呼:AlertOnCondition(GetCondition());

确实没有什么比这更简单的了,而且一旦超过几种情况,它就会比if-else链更快。


0

我无法代表您的特殊情况,因为代码不是特定的,但是...

对于缺乏OO模型的人来说,这样的代码通常是一种气味。您确实有四种类型的事物,每种事物都与其自己的警报器类型相关联,但是您无需识别这些实体并为每个实体创建一个类实例,而是将它们视为一件事,并在以后一次尝试弥补它时需要知道您要处理的内容才能继续。

多态可能更适合您。

对于包含长或复杂的if-then构造的长方法,请对代码多疑。您通常希望在那里有一些带有虚拟方法的类树。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.