大布尔表达式比分解成谓词方法的同一个表达式更具可读性吗?[关闭]


63

什么更容易理解,一个大的布尔语句(相当复杂),或同一语句分解为谓词方法(很多额外的代码需要阅读)?

选项1,大布尔表达式:

    private static bool ContextMatchesProp(CurrentSearchContext context, TValToMatch propVal)
    {

        return propVal.PropertyId == context.Definition.Id
            && !repo.ParentId.HasValue || repo.ParentId == propVal.ParentId
            && ((propVal.SecondaryFilter.HasValue && context.SecondaryFilter.HasValue && propVal.SecondaryFilter.Value == context.SecondaryFilter) || (!context.SecondaryFilter.HasValue && !propVal.SecondaryFilter.HasValue));
    }

选项2,条件分为谓词方法:

    private static bool ContextMatchesProp(CurrentSearchContext context, TValToMatch propVal)
    {
        return MatchesDefinitionId(context, propVal)
            && MatchesParentId(propVal)
            && (MatchedSecondaryFilter(context, propVal) || HasNoSecondaryFilter(context, propVal));
    }

    private static bool HasNoSecondaryFilter(CurrentSearchContext context, TValToMatch propVal)
    {
        return (!context.No.HasValue && !propVal.SecondaryFilter.HasValue);
    }

    private static bool MatchedSecondaryFilter(CurrentSearchContext context, TValToMatch propVal)
    {
        return (propVal.SecondaryFilter.HasValue && context.No.HasValue && propVal.SecondaryFilter.Value == context.No);
    }

    private bool MatchesParentId(TValToMatch propVal)
    {
        return (!repo.ParentId.HasValue || repo.ParentId == propVal.ParentId);
    }

    private static bool MatchesDefinitionId(CurrentSearchContext context, TValToMatch propVal)
    {
        return propVal.PropertyId == context.Definition.Id;
    }

我更喜欢第二种方法,因为我将方法名称视为注释,但是我知道这是有问题的,因为您必须阅读所有方法以了解代码的作用,因此它抽象了代码的意图。


13
选项2与Martin Fowler在其重构书中建议的类似。再加上您的方法名称是所有随机表达式的意图,方法的内容只是随时间变化的实现细节。
程序员

2
真的是一样的表情吗?“或”的优先级低于“与”的优先级。无论如何,第二个告诉您意图,另一个(第一个)是技术性的。
包装者

3
@thepacker说什么。第一种方法导致您犯了一个错误的事实是一个很好的线索,即第一种方法对于目标受众的非常重要的领域而言不容易理解。你自己!
史蒂夫·杰索普

3
选项3:我都不喜欢任何一个。第二个是冗长的,第一个不等于第二个。括号会有所帮助。
大卫·哈曼

3
这可能很花哨,但是在任何一个代码块中都没有任何 if语句。您的问题是关于布尔表达式的
凯尔·斯特兰德

Answers:


88

什么更容易理解

后一种方法。它不仅易于理解,而且易于编写,测试,重构和扩展。每个要求的条件都可以通过自己的方式安全地解耦和处理。

这是有问题的,因为您必须阅读所有方法来理解代码

方法的名称正确没有问题。实际上,由于方法名称将描述条件的意图,因此更容易理解。
对于围观者而言,if MatchesDefinitionId()if (propVal.PropertyId == context.Definition.Id)

[个人而言,第一种方法使我的眼睛发痛。


12
如果方法名称很好,那么它也更容易理解。
2016年

并且,请使它们(方法名称)有意义且简短。20多个字符方法名称令我眼花eyes乱。MatchesDefinitionId()是临界点。
Mindwin'3

2
@Mindwin如果要在使方法名称“短”和使其有意义之间做出选择,我会说每次都使用后者。简短是好的,但不能以可读性为代价。
Ajedi32 '16

@ Ajedi32不必写一篇关于该方法对方法名称的作用的文章,也不必具有语法上合理的方法名称。如果能使缩写标准(在整个工作组或整个组织中)保持清晰,那么缩写名称和可读性就不会成为问题。
Mindwin'3

使用齐普夫定律:使事物更加冗长,以阻止其使用。
hoosierEE's

44

如果这是使用这些谓词函数的唯一位置,则也可以改用局部bool变量:

private static bool ContextMatchesProp(CurrentSearchContext context, TValToMatch propVal)
{
    bool matchesDefinitionId = (propVal.PropertyId == context.Definition.Id);
    bool matchesParentId = (!repo.ParentId.HasValue || repo.ParentId == propVal.ParentId);
    bool matchesSecondaryFilter = (propVal.SecondaryFilter.HasValue && context.No.HasValue && propVal.SecondaryFilter.Value == context.No);
    bool hasNoSecondaryFilter = (!context.No.HasValue && !propVal.SecondaryFilter.HasValue);

    return matchesDefinitionId
        && matchesParentId
        && matchesSecondaryFilter || hasNoSecondaryFilter;
}

这些也可以进一步细分并重新排序以使其更具可读性,例如

bool hasSecondaryFilter = propVal.SecondaryFilter.HasValue;

然后替换的所有实例propVal.SecondaryFilter.HasValue。然后立即出现的一件事是,hasNoSecondaryFilter在否定的HasValue属性上使用逻辑AND ,而matchesSecondaryFilter在未否定的属性上使用逻辑AND- HasValue因此并非完全相反。


3
这个解决方案非常好,我当然已经写了很多类似的代码。这是非常可读的。与我发布的解决方案相比,缺点是速度。使用这种方法,无论如何都可以执行一堆条件测试。在我的解决方案中,可以根据处理的值显着减少操作。
BuvinJ

5
@BuvinJ像这里所示的测试应该相当便宜,因此除非我知道某些条件很昂贵,或者除非这是对性能非常敏感的代码,否则我会选择可读性更高的版本。
2013年

1
@svick毫无疑问,大多数情况下这不太可能引起性能问题。但是,如果您可以减少操作而不损失可读性,那为什么不这样做呢?我不认为这比我的解决方案更具可读性。它确实为测试提供了自我记录的“名称”-很好...我认为这取决于具体的用例以及测试本身的可理解性。
BuvinJ

添加评论也可以提高可读性...
BuvinJ

@BuvinJ我对此解决方案的真正喜欢是,通过忽略除最后一行之外的所有内容,我可以快速了解它的作用。我确实认为这更具可读性。
2013年

42

通常,后者是优选的。

它使呼叫站点更加可重用。它支持DRY(意味着您可以在更改标准时更改的位置更少,并且可以更可靠地进行更改)。通常,这些子条件是可以在其他地方独立重用的东西,使您能够做到这一点。

哦,这使这些东西更容易进行单元测试,使您确信自己已正确完成。


1
是的,尽管您的答案还应该解决固定使用的问题repo,这似乎是一个静态字段/属性,即全局变量。静态方法应该是确定性的,不能使用全局变量。
David Arno

3
@DavidArno-虽然不是很好,但似乎与当前问题相切。如果没有更多的代码,则设计可以像这样半有效地运行是有道理的。
Telastyn

1
是的,没关系回购。我不得不稍微混淆代码,不想在互联网上按原样共享客户端代码:)
willem

23

如果介于这两个选择之间,则后者更好。这些不是唯一的选择!如何将单个功能分解为多个ifs?测试退出函数的方式以避免额外的测试,在单线测试中大致模拟“短路”。

这更容易阅读(您可能需要仔细检查示例的逻辑,但这一概念成立):

private static bool ContextMatchesProp(CurrentSearchContext context, TValToMatch propVal)
{
    if( propVal.PropertyId != context.Definition.Id ) return false;

    if( repo.ParentId.HasValue || repo.ParentId != propVal.ParentId ) return false;

    if( propVal.SecondaryFilter.HasValue && 
        context.SecondaryFilter.HasValue && 
        propVal.SecondaryFilter.Value == context.SecondaryFilter ) return true;

    if( !context.SecondaryFilter.HasValue && 
        !propVal.SecondaryFilter.HasValue) return true;

    return false;   
}

3
为什么在发布此消息后的几秒钟之内就对此表示反对?下投时请添加评论!该答案的操作速度一样快,而且更易于阅读。所以有什么问题?
BuvinJ

2
@BuvinJ:绝对没有错。与原始代码相同,不同之处在于您不必用十几个括号和一条延伸到屏幕末尾的单行进行战斗。我可以从上到下阅读该代码,并立即理解它。WTF计数= 0
gnasher729

1
返回而不是在函数末尾使IMO的代码可读性降低,而不是可读性更高。我更喜欢单出口。在此链接上,有一些很好的论证。stackoverflow.com/questions/36707/...
布拉德·托马斯

5
@布拉德·托马斯我不同意这个出口点。它通常导致深层嵌套括号。返回结束了路径,所以对我来说更容易阅读。
Borjab

1
@BradThomas我完全同意Borjab。实际上,避免深层嵌套是为什么我经常使用这种样式而不是破坏长条件语句的原因。我经常发现自己在编写带有大量嵌套的代码。然后,我开始寻找几乎不超过一个或两个嵌套的方法,结果,我的代码变得更易于阅读和维护。如果您找到退出功能的方法,请尽快这样做!如果您找到避免深层嵌套和长条件的方法,请这样做!
BuvinJ

10

我更喜欢选项2,但建议进行一次结构性更改。将条件最后一行的两个检查合并到一个调用中。

private static bool ContextMatchesProp(CurrentSearchContext context, TValToMatch propVal)
{
    return MatchesDefinitionId(context, propVal)
        && MatchesParentId(propVal)
        && MatchesSecondaryFilterIfPresent(context, propVal);
}

private static bool MatchesSecondaryFilterIfPresent(CurrentSearchContext context, 
                                                    TValToMatch propVal)
{
    return MatchedSecondaryFilter(context, propVal) 
               || HasNoSecondaryFilter(context, propVal);
}

我建议这样做的原因是,这两个检查是单个功能单元,并且在条件中嵌套括号很容易出错:从最初编写代码的角度和阅读代码的人员的角度来看。如果表达式的子元素不遵循相同的模式,则尤其如此。

我不确定这MatchesSecondaryFilterIfPresent()是组合的最佳名称吗?但是没有比这更好的了。


很好,尝试解释方法内部正在做的事情实际上比重组调用好。
卡拉

2

尽管在C#中,代码不是非常面向对象的。它使用的是静态方法,看起来像静态字段(例如repo)。通常认为,静态变量使您的代码难以重构且难以测试,同时又阻碍了可重用性,并且提出了一个问题:与诸如面向对象的构造相比,此类静态用法的可读性和可维护性较低。

您应该将此代码转换为更面向对象的形式。当您这样做时,您会发现在适当的地方放置了用于比较对象,字段等的代码。您然后可能会要求对象进行自我比较,这将使if语句大为简化。简单的比较请求(例如if ( a.compareTo (b) ) { },可能包括所有字段比较。)

C#具有一组丰富的接口和系统实用程序,用于对对象及其字段进行比较。除了显而易见的.Equals方法,对于初学者来说,看看IEqualityComparerIEquatable和公用事业一样System.Collections.Generic.EqualityComparer.Default


0

后者绝对是首选,我以第一种方式看过案例,并且几乎总是无法阅读。我犯了第一种方法的错误,并被要求将其更改为谓词方法。


0

我想说的是,如果您添加一些空格以提高可读性和一些注释来帮助读者了解较为晦涩的部分,则两者大致相同。

切记:好的注释可以告诉读者在编写代码时的想法

通过进行如我建议的更改,我可能会采用前一种方法,因为它不太混乱和分散。子例程调用就像脚注:它们提供有用的信息,但会干扰阅读流程。如果谓词更复杂,那么我会将它们分解为单独的方法,以便可以将其包含的概念构建为可理解的块。


值得+1。尽管不是基于其他答案的流行观点,但还是值得深思的。谢谢:)
威廉

1
@willem不,它不值得+1。两种方法不一样。多余的注释是愚蠢且不必要的。
2016年

2
好的代码绝不依赖于可理解的注释。实际上,注释是代码可能造成的最糟糕的混乱。该代码应该说明一切。另外,OP希望评估的两种方法永远不会“大致相同”,无论一个空白添加了多少。
Wonderbell

具有有意义的函数名称比阅读注释要好。如《清洁代码》一书中所述,注释是无法表达抛出代码的。当函数可以更清楚地说明它时,为什么要解释您在做什么呢。
Borjab

0

好吧,如果您想重用某些部分,最好将它们分离成单独的正确命名的函数。
即使您可能永远不会重用它们,这样做也可以使您更好地构造条件,并给它们加上描述其含义的标签。

现在,让我们看一下您的第一个选择,并承认,缩进和换行既不是所有有用的东西,也不是条件构造的那么好:

private static bool ContextMatchesProp(CurrentSearchContext context, TValToMatch propVal) {
    return propVal.PropertyId == context.Definition.Id && !repo.ParentId.HasValue
        || repo.ParentId == propVal.ParentId
        && propVal.SecondaryFilter.HasValue == context.SecondaryFilter.HasValue
        && (!propVal.SecondaryFilter.HasValue || propVal.SecondaryFilter.Value == context.SecondaryFilter.Value);
}

0

第一个绝对可怕。您一直在使用|| 在同一行上有两件事;这可能是代码中的错误,也可能是混淆代码的意图。

    return (   (   propVal.PropertyId == context.Definition.Id
                && !repo.ParentId.HasValue)
            || (   repo.ParentId == propVal.ParentId
                && (   (   propVal.SecondaryFilter.HasValue
                        && context.SecondaryFilter.HasValue 
                        && propVal.SecondaryFilter.Value == context.SecondaryFilter)
                    || (   !context.SecondaryFilter.HasValue
                        && !propVal.SecondaryFilter.HasValue))));

这至少是经过适当格式化的一半(如果格式化很复杂,那是因为if条件很复杂),并且您至少有机会弄清楚其中是否有任何废话。与您格式化的垃圾相比,还有其他更好的选择。但是您似乎只能做极端:要么完全弄乱if语句,要么四个完全没有意义的方法。

注意(cond1 && cond2)|| (!cond1 && cond3)可以写成

cond1 ? cond2 : cond3

这样可以减少混乱。我会写

if (propVal.PropertyId == context.Definition.Id && !repo.ParentId.HasValue) {
    return true;
} else if (repo.ParentId != propVal.ParentId) {
    return false;
} else if (propVal.SecondaryFilter.HasValue) {
    return (   context.SecondaryFilter.HasValue
            && propVal.SecondaryFilter.Value == context.SecondaryFilter); 
} else {
    return !context.SecondaryFilter.HasValue;
}

-4

我不喜欢这些解决方案,它们既难以推理,也难以阅读。仅为较小的方法而提取较小的方法并不总是可以解决问题。

理想情况下,我认为您应该在元编程上比较属性,因此您不必定义新方法,也不必每次都想比较一组新属性时是否分支。

我不确定c#,但是在javascript中,这种方法会更好,并且至少可以替换MatchesDefinitionId和MatchesParentId

function compareContextProp(obj, property, value){
  if(obj[property])
    return obj[property] == value
  return false
}

1
在C#中实现类似的东西应该不是问题。
史努比

我没有看到compareContextProp(propVal, "PropertyId", context.Definition.Id)〜5的布尔组合比OP的〜5比较形式的布尔组合更容易阅读propVal.PropertyId == context.Definition.Id。它要长得多,并增加了一层,却没有真正隐藏呼叫站点的任何复杂性。(如果有关系,我并不会
投票
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.