简洁在什么时候不再是一种美德?


102

最近的错误修复要求我检查其他团队成员编写的代码,在这里发现了这一点(它是C#):

return (decimal)CostIn > 0 && CostOut > 0 ? (((decimal)CostOut - (decimal)CostIn) / (decimal)CostOut) * 100 : 0;

现在,有充分的理由进行所有这些强制转换,这仍然很难遵循。计算中有一个小错误,我不得不解开它以解决问题。

我从代码审查中知道了此人的编码风格,他的方法是较短的总是更好。当然,这里还有价值:我们都已经看到了不必要的复杂的条件逻辑链,这些逻辑可以与一些位置合理的运算符一起整理。但是他显然比我更擅长追随运营商,他们陷入了一个单一的问题。

当然,这最终是样式问题。但是,是否有任何关于识别代码简洁性不再有用并成为理解障碍的观点的写作或研究?

进行强制转换的原因是实体框架。数据库需要将它们存储为可空类型。小数?不等同于C#中的Decimal,需要进行强制转换。


153
当简洁胜过可读性时。
罗伯特·哈维

27
查看您的特定示例:强制转换是(1)开发人员比编译器了解更多的地方,并且需要告诉编译器一个无法推论的事实,或者(2)在“错误的地方存储一些数据” ”,表示我们需要对其执行的各种操作。两者都是可以重构的有力指标。最好的解决方案是找到一种无需强制转换即可编写代码的方法。
埃里克·利珀特

29
尤其奇怪的是,必须将CostIn强制转换为十进制以将其与零进行比较,而不是CostOut。这是为什么?到底CostIn的类型是什么,它只能通过将其转换为十进制而与零进行比较?为什么CostOut与CostIn的类型不同?
埃里克·利珀特

12
而且,这里的逻辑实际上可能是错误的。假设CostOut等于Double.Epsilon,因此大于零。但是(decimal)CostOut在那种情况下为零,我们有一个除以零的误差。第一步应该是使代码正确,但我认为并非如此。使其正确,制作测试用例,然后使其优雅。优雅的代码和简短的代码有很多共同点,但是有时简洁不是优雅的灵魂。
埃里克·利珀特

5
简洁永远是一种美德。但是我们的目标功能将简洁与其他优点结合在一起。如果在不损害其他美德的情况下能做到简短,就总是应该这样做。
所罗门诺夫(Solomonoff)的秘密,

Answers:


163

回答有关现有研究的问题

但是,是否有任何关于识别代码简洁性不再有用并成为理解障碍的观点的写作或研究?

是的,这方面已有工作。

要了解这些内容,您必须找到一种计算指标的方法,以便可以在定量的基础上进行比较(而不是像其他答案一样,仅基于智慧和直觉进行比较)。研究的一种潜在指标是

循环复杂度 ÷代码源代码行(SLOC

在您的代码示例中,此比率非常高,因为所有内容都已压缩到一行。

SATC已发现,最有效的评估是尺寸和[Cyclomatic]复杂度的组合。具有高复杂度和大尺寸的模块往往具有最低的可靠性。尺寸小,复杂度高的模块也存在可靠性风险,因为它们往往是非常简洁的代码,难以更改或修改。

链接

如果您感兴趣,这里有一些参考资料:

McCabe,T.和A. Watson(1994),软件复杂性(CrossTalk:国防软件工程杂志)。

Watson,AH和McCabe,TJ(1996)。结构化测试:使用环复杂度度量的测试方法(NIST特殊出版物500-235)。从McCabe Software网站检索:2011年5月14日:http : //www.mccabe.com/pdf/mccabe-nist235r.pdf

Rosenberg,L.,Hammer,T.,Shaw,J。(1998)。软件度量标准和可靠性(IEEE国际软件可靠性工程研讨会论文集)。从宾夕法尼亚州立大学网站检索:2011年5月14日:http : //citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.104.4041&rep=rep1&type=pdf

我的意见和解决方案

就我个人而言,我从来没有重视简洁性,只有可读性。有时简洁有助于提高可读性,有时却不能。更重要的是,您正在编写“ 真正显而易见的代码”(ROC),而不是“仅写代码”(WOC)。

只是为了好玩,这是我的写法,并请我的团队成员编写:

if ((costIn <= 0) || (costOut <= 0)) return 0;
decimal changeAmount = costOut - costIn;
decimal changePercent = changeAmount / costOut * 100;
return changePercent;

还要注意,引入工作变量具有触发定点算术而不是整数算术的令人满意的副作用,因此decimal消除了对所有这些强制类型转换的需要。


4
我非常喜欢小于零的情况的保护条款。可能值得一提:成本怎么可能小于零,那是什么特例?
user949300

22
+1。我可能会添加类似内容if ((costIn < 0) || (costOut < 0)) throw new Exception("costs must not be negative");
Doc Brown

13
@DocBrown:这是一个很好的建议,但是我会考虑是否可以通过测试来执行特殊的代码路径。如果是,则编写一个测试该代码路径的测试用例。如果否,则将整个内容更改为断言。
埃里克·利珀特

12
尽管这些都是好点,但我相信这个问题的范围是代码风格,而不是逻辑。从黑匣子的角度来看,我的代码剪裁是在功能对等方面的努力。引发异常与返回0并不相同,因此解决方案在功能上将不等效。
约翰·吴

30
“就我个人而言,我从不珍惜简洁,只重视可读性。有时简洁有助于可读性,有时却不可读。” 优秀的点。为此+1。我也喜欢您的代码解决方案。
通配符

49

简洁性可以减少周围重要事物的混乱,但是当它变得简洁时,将太多相关数据打包得过于密集以至于无法轻易跟随,那么相关数据本身就会变得混乱,您就会遇到麻烦。

在这种特殊情况下,decimal要重复进行强制转换;整体上将其重写为以下内容可能会更好:

var decIn = (decimal)CostIn;
var decOut = (decimal)CostOut;
return decIn > 0 && CostOut > 0 ? (decOut - decIn ) / decOut * 100 : 0;
//                  ^ as in the question

突然,包含逻辑的线短了很多,并且恰好位于一条水平线上,因此您无需滚动即可看到所有内容,其含义也更容易理解。


1
我可能会走得更远,然后重构((decOut - decIn ) / decOut) * 100为另一个变量。
FrustratedWithFormsDesigner

9
汇编程序更加清晰:每行仅执行一次操作。h!

2
@FrustratedWithFormsDesigner我甚至更进一步,将条件检查放在括号中。
克里斯·西里菲斯

1
@FrustratedWithFormsDesigner:将其提取到条件之前将绕过零除法检查(CostOut > 0),因此您必须将条件扩展为- if语句。这并不是说有什么问题,但它确实增加了更多的冗长性,而不仅仅是引入本地语言。
wchargin

1
老实说,如果您因为某人因无法识别简单的基本费率计算而难以阅读,那您就可以摆脱IMO的困扰,这就是他的问题,而不是我的问题。
Walfrat

7

尽管我无法对此主题进行任何特定的研究,但我建议您代码中的所有这些强制类型转换都违反了“不要自己重复”的原则。您的代码似乎想要执行的操作是将costInand 转换costOut为type Decimal,然后对此类转换的结果执行一些健全性检查,如果检查通过则对这些转换后的值执行其他操作。实际上,您的代码对未转换的值执行完整性检查之一,从而增加了costOut可能保留的值大于零,但小于Decimal可表示的最小非零值大小的一半的可能性。如果代码定义类型变量Decimal来保存转换后的值,然后对这些值进行操作,则代码将更加清晰。

它似乎奇怪的是,你会更感兴趣的比例Decimal的表示costIncostOut比实际值的比例costIncostOut,除非该代码也将使用十进制表示用于其他目的。如果代码将进一步使用这些表示形式,那么这将是创建变量以保存这些表示形式的进一步论据,而不是在整个代码中连续进行强制转换。


(十进制)强制类型转换可能是为了满足某些业务规则要求。与会计师打交道时,有时您不得不跳过愚蠢的篮球。我以为CFO会发现“使用税收舍入更正-0.01美元”这行在考虑到所要求的功能后不可避免,因此会心脏病发作。(提供:税后价格。我必须计算税前价格。没有答案的几率等于税率。)
Loren Pechtel

@LorenPechtel:鉴于类型转换的精确度Decimal取决于所讨论的值的大小,我很难想象业务规则会要求类型转换的实际行为。
超级猫

1
好点-我忘记了十进制类型的细节,因为我从来没有机会想要这样的东西。我现在认为这是对商业规则的狂热崇拜-具体来说,钱一定不能浮点。
罗兰·佩希特尔

1
@LorenPechtel:阐明我的最后一点:定点类型可以保证x + yy会溢出或产生y。该Decimal类型没有。值1.0d / 3.0的十进制小数位数比使用较大数字时可以保留的位数多。因此,相加然后减去相同的较大数字将导致精度损失。定点类型可能会因分数乘法或除法而失去精度,但不会因加,减,乘或除以余数而失去精度(例如1.00 / 7为0.14余数0.2; 1.00 div 0.15为6余数0.10)。
超级猫

1
@绿巨人:是的,当然。我正在辩论是否使用x + yy产生x,x + yx产生y或x + yxy,最后对前两个进行了密切处理。重要的是,定点类型可以保证许多操作序列永远不会导致任何程度的不可检测的舍入误差。如果代码以不同的方式将事物加在一起以验证总计是否匹配(例如,将行小计的总和与列小计的总和进行比较),则得出的结果精确相等要比“接近”得出的结果好得多。
超级猫

5

我查看该代码,然后问:“成本如何为0(或更少)?”。这表示什么特殊情况?代码应该是

bool BothCostsAreValidProducts = (CostIn > 0) && (CostOut > 0);
if (!BothCostsAreValidProducts)
  return NO_PROFIT;
else {
   // that calculation here...
}

我猜是这里的名字:变化BothCostsAreValidProductsNO_PROFIT适当的。


当然,成本可以为零(以圣诞节为例)。在负利率时代,负成本也不应该太令人惊讶,至少代码应该准备好应对(并通过抛出错误来解决)
Hagen von Eitzen

您走对了。
danny117 '17

真傻 if (CostIn <= 0 || CostOut <= 0)完全可以
Miles Rout

可读性差得多。变量名太可怕了(BothCostsAreValid会更好。这里没有关于产品的信息)。但是,即使那样也不会增加任何可读性,因为仅检查CostIn,CostOut就可以了。如果要测试的表达式的含义不明显,则可以使用一个有意义的名称来引入一个额外的变量。
gnasher729

5

当我们忘记了达到目的的手段而不是本身的美德时,简洁就不再是一种美德。我们喜欢简洁,因为它与简单性相关;我们喜欢简单,因为更简单的代码更易于理解,易于修改并且包含更少的错误。最后,我们希望代码实现这些目标:

  1. 用最少的工作量来满足业务需求

  2. 避免错误

  3. 让我们在将来做出继续实现1和2的更改

这些是目标。任何设计原则或方法(无论是KISS,YAGNI,TDD,SOLID,证明,类型系统,动态元编程等)都仅在帮助我们实现这些目标的范围内具有美德。

该生产线似乎错过了最终目标。虽然很短,但并不简单。通过多次重复相同的铸造操作,它实际上包含了不必要的冗余。重复代码会增加复杂性并增加错误的可能性。将转换与实际计算混合在一起也使代码难以遵循。

该行涉及三个方面:防护(特殊套管0),类型转换和计算。孤立地考虑每个问题都非常简单,但是由于所有问题都混合在同一个表达式中,因此很难遵循。

尚不清楚为什么CostOut第一次使用它时不会使用CostIn。可能有充分的理由,但是意图并不明确(至少并非没有上下文),这意味着维护人员会警惕更改此代码,因为可能存在一些隐藏的假设。这是对可维护性的厌恶。

由于CostIn在与0比较之前进行了强制转换,因此我假设它是一个浮点值。(如果它是一个int,则没有理由强制转换)。但是,如果CostOut是浮点数,则代码可能会隐藏一个晦涩的被零除的错误,因为浮点值可能很小但非零,但当转换为十进制时为零(至少我相信这是可能的。)

因此,问题不是简洁或缺乏,问题是重复的逻辑和关注点的混淆,导致难以维护的代码。

引入变量以保留强制转换的值可能会增加按代币数量计算的代码的大小,但会降低复杂性,分离问题并提高清晰度,这使我们更接近易于理解和维护的代码目标。


1
重要的一点是:一次转换CostIn而不是两次转换会使它变得不可读,因为读者不知道这是不是带有明显修复的细微错误,还是是否有意进行。显然,如果我不能确定说一句作者是什么意思,那么它就不可读。应该有两个强制转换,或者要有注释来解释为什么第一次使用CostIn不需要或不应该强制转换。
gnasher729

3

简洁根本不是美德。可读性是优点。

简洁可以成为实现美德的工具,或者像您的示例一样,可以成为实现完全相反的工具。通过这种方式,它几乎没有任何价值。代码应该“尽可能短”的规则也可以用“尽可能淫秽”代替-它们同样没有意义,并且如果没有更大的用途,则可能造成破坏。

此外,您发布的代码甚至都不遵循简短规则。如果用M后缀声明常量,则(decimal)可以避免大多数可怕的类型转换,因为编译器会将剩余值提升intdecimal。我相信您所描述的人只是以简洁为借口。很可能不是故意的,但仍然是。


2

在多年的经验中,我开始相信最终的简洁就是时间 -时间主导着其他一切。这包括性能时间-程序完成工作需要多长时间-以及维护时间-添加功能或修复错误需要多长时间。(如何平衡这两个代码取决于所执行代码与改进代码执行的频率-请记住,过早的优化仍然是万恶之源。)简短的代码是为了提高两者的简洁性。较短的代码通常运行速度更快,并且通常更易于理解和维护。如果两者都不做,那就是净负数。

在这里显示的情况下,我认为文本的简洁性被误认为行数的简洁性,以牺牲可读性为代价,这可能会增加维护时间。(执行转换可能还需要更长的时间,具体取决于转换的方式,但除非上一行运行了数百万次,否则可能不必担心。)在这种情况下,重复的十进制转换会降低可读性,因为很难做到这一点。看看最重要的计算是什么。我会写如下:

decimal dIn = (decimal)CostIn;
decimal dOut = (decimal)CostOut;
return dIn > 0 && CostOut > 0 ? ((dOut - dIn) / dOut) * 100 : 0;

(编辑:这是与其他答案相同的代码,所以就可以了。)

我是三元运算符的爱好者? :,所以我不予理会。


5
三元数很难阅读,特别是如果返回值中除了单个值或变量之外还存在其他表达式时。
Almo

我想知道这是否是导致否定投票的原因。除非我写的内容与梅森·惠勒(Mason Wheeler)大致相同,目前票数为10票。他也离开了三元。我不知道为什么这么多人有问题? :-我认为上面的示例足够紧凑,尤其是。与if-then-else相比。
Paul Brinkley

1
真的不确定。我没有投票给你。我不喜欢三元组,因为目前尚不清楚三元组的哪一侧:if-else读起来像英语:不能错过它的意思。
Almo

FWIW我怀疑您正在投票否决,因为这与梅森·惠勒的答案非常相似,但他的答案是第一。
Matt Thrower

1
三元运算符的死亡!!(此外,制表符,空格以及任何包围和缩进模型的死亡,除了Allman(事实上,Donald(tm)在推特上发布的消息,这将是他于20日制定的前三部法律))
Mawg

2

就像上面所有几乎所有答案一样,可读性应始终是您的主要目标。但是,我还认为,与创建变量和换行相比,格式化可能是实现此目的的更有效方法。

return ((decimal)CostIn > 0 && CostOut > 0) ?
       100 * ( (decimal)CostOut - (decimal)CostIn ) / (decimal)CostOut:
       0;

在大多数情况下,我完全同意圈复杂度的说法,但是您的函数似乎很小且很简单,可以用一个好的测试用例更好地解决。出于好奇,为什么需要强制转换为小数?


4
进行强制转换的原因是实体框架。数据库需要将它们存储为可空类型。双?在C#中不等同于Double,需要强制转换。
马特·瑟罗

2
@MattThrower你的意思是decimal吧?double!= decimal,有很大的不同。
pinkfloydx33

1
@ pinkfloydx33是的!在只有一半大脑参与的情况下在电话上打字:)
Matt Thrower

我必须向学生解释,SQL数据类型与编程语言中使用的类型奇怪地不同。不过,我无法向他们解释原因。“我不知道!” “小端!”

3
我觉得这根本不可读。
Almo

1

在我看来,这里可读性的一个大问题是完全缺乏格式。

我会这样写:

return (decimal)CostIn > 0 && CostOut > 0 
            ? (((decimal)CostOut - (decimal)CostIn) / (decimal)CostOut) * 100 
            : 0;

根据CostIn和的类型CostOut是浮点类型还是整数类型,某些强制转换也可能不是必需的。与float和不同double,整数值被隐式提升为decimal


我很遗憾看到这没有任何解释而被否决,但是在我看来,这与Backpackcodes的回答减去他的某些言论是相同,因此我认为这是合理的。
PJTraill '17

@PJTraill我一定错过了,确实几乎是一样的。但是,我强烈希望将运算符放在新的行上,这就是为什么我要保留我的版本的原因。
Felix Dombek

我同意运营商的意见,正如我在对其他答案的评论中指出的那样–我没有发现您按照我的意愿做了。
PJTraill '17年

0

可以急着编写代码,但是在我的世界中,上面的代码应该用更好的变量名编写。

如果我正确阅读了代码,则它正在尝试进行毛利润计算。

var totalSales = CostOut;
var totalCost = CostIn;
var profit = (decimal)(CostOut - CostIn);
var grossMargin = 0m; //profit expressed as percentage of totalSales

if(profit > 0)
{
    grossMargin = profit/totalSales*100
}

3
您失去了除以零将返回零。
danny117 '17

1
这就是为什么重构为简洁起见而最大化的其他人的代码很棘手的原因,以及为什么有额外的注释来解释为什么/如何工作是一件好事
Rudolf Olah

0

我假设CostIn * CostOut是整数
这就是我将其写为
M(货币)为十进制的方式

return CostIn > 0 && CostOut > 0 ? 100M * (CostOut - CostIn) / CostOut : 0M;

1
希望他们不是两个消极的想法:p
Walfrat

2
被零除的还在吗?
danny117 '17

@ danny117当简洁产生错误的答案时,它就已经走了很远。
狗仔队

不想更新问题并将其激活。100M和0M强制使用小数。我认为(CostOut-CostIn)将作为整数数学执行,然后将差值转换为十进制。
狗仔队

0

代码是为人们理解而编写的;在这种情况下,简洁性并不能带来太多好处,而且会增加维护人员的负担。为简便起见,您绝对应该通过使代码更具自记录性(更好的变量名)或添加更多注释来解释为什么如此工作来扩展此功能。

今天,当您编写代码来解决问题时,明天需求改变时,该代码可能会成为问题。始终必须考虑维护,并且提高代码的可理解性至关重要。


1
这是软件工程实践和原则发挥作用的地方。非功能性要求
hanzolo

0

精简不再是一种美德

  • 有没有事先检查为零的除法。
  • 没有检查是否为空。
  • 没有清理。
  • TRY CATCH vs引发了可以解决错误的食物链。
  • 对异步任务的完成顺序进行了假设
  • 使用延迟而不是将来重新计划的任务
  • 使用不必要的IO
  • 不使用乐观更新

没有足够长的时间时回答。

1
好的,这应该是评论,而不是答案。但是他是个新人,所以至少可以解释一下;不要只是投票而逃!欢迎登上,丹尼。我取消了
弃权票

2
好的,我已经将答案扩展到了一些更复杂的东西,这些东西我已经学会了编写简短代码的难易方式。
danny117

感谢@Mawg的欢迎,我想指出对null的检查是我在简短代码中遇到最多问题的原因。
danny117 '17

我只是通过Android进行了编辑,它没有要求提供编辑说明。我添加了乐观更新(检测到更改并发出警告)
danny117

0

如果这通过了验证单元测试,那么就可以了,如果添加了新规范,需要新测试或增强的实现,并且需要“整理”代码的简洁性,那就是问题就会出现。

显然,代码中的错误表明,Q / A还有另一个问题,那就是疏忽大意,因此,存在一个未被发现的错误的事实值得关注。

当处理非功能性需求(例如代码的“可读性”)时,它需要由开发经理定义并由主要开发人员进行管理,并得到开发人员的尊重,以确保正确的实现。

尝试确保代码的标准化实现(标准和约定),以确保“可读性”和“可维护性”的易用性。但是,如果未定义和实施这些质量属性,那么最终将得到上面的示例所示的代码。

如果您不喜欢这种代码,请尝试使团队就实施标准和约定达成一致,并将其写下来,并进行随机代码审查或抽查以确认该约定得到遵守。

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.