在什么情况下更少的代码不是更好?

我最近在工作中重构了一些代码,我认为我做得很好。我将980行代码丢弃到450,并减少了一半的类。

当向同事们展示时,有些人不同意这是一个改进。

他们说 - “更少的代码行不一定更好”

我可以看到,可能存在极端情况,人们会写出很长的行和/或将所有内容放在一个方法中以节省几行,但这不是我所做的。在我看来,代码结构合理,易于理解/维护,因为它的大小只有一半。

我很难理解为什么有人会想要完成工作所需的双倍代码,我想知道是否有人和我的同事感觉一样,并且可以做出一些好的案例来减少更多的代码?

54
@jamesqf我明白了你的意思,我不清楚。通常当我实施数学算法时,我添加了一个纸张链接,该链接使用相同的变量名称或将方程转换放在注释中。我希望在这样的智能代码中至少有2-3行评论描述它是牛顿方法以及如何实现智能。
额外 作者 svec,
考虑快速逆平方根算法。使用适当的变量命名实现完整的牛顿方法将更清晰,更容易阅读,即使它可能包含更多代码行。 (请注意,在这种特殊情况下,使用智能代码可以通过perf关注来证明)。
额外 作者 svec,
@Bergi那不是真的:你所描述的是代码复杂性,而不是 size 。另外,研究表明,行数和错误的可能性之间存在相关性。使用较少的行来完成相同的任务与较低的错误机会相关。
额外 作者 Mike McAllister,
它的unrdbl。
额外 作者 vesan,
虽然我是DRY代码的非常坚定的信徒,但我并不认为它与代码大小/可读性有任何预定义的关系,我认为不重复的代码可以写得很好或很差。另一方面,“Terse”代码很可能写得不好。我所说的是,鉴于您的代码不会重复,较长的解决方案通常会比较短的解决方案更好,而最短的解决方案很少会是最好的。
额外 作者 Aesin,
@nocomprende我应该说我来自可读性是最重要因素的背景。 APL,当我至少在80年代了解它时,被认为是一种只写的语言......我喜欢这个想法并且玩了一下它,但它针对的是一种不同于我工作过的环境。在。
额外 作者 Aesin,
你写的问题绝对过于宽泛。建议您写一篇关于您所做的具体更改的新文章。
额外 作者 jmibanez,
@BillK不是APL的粉丝,嗯?
额外 作者 user251748,
“少代码”和“少代码行”之间存在显着差异。
额外 作者 Ben Aaronson,
额外 作者 Robert Grant,
请你的同事提出建设性的批评:如果他们认为这种重构不是一种改进,那么为什么呢?
额外 作者 Delores J. Linscott,
“更少的代码行不一定更好” - 这样的普遍性并不意味着什么。为什么他们认为,在这种特殊情况下,更少的线路更好?
额外 作者 sonstone,
在codereview中有一些人认为代码行是所有问题,我发现他们的代码很难遵循。
额外 作者 Flamewires,
有人必须这样做:这可以通过一些代码扩展来开始。
额外 作者 Rick,
我最喜欢的例子可以在这里找到。 joelonsoftware.com/2001/12/11/back-to-basics 。 TL; DR,简单的实现可能是CPU密集型的。一些额外的代码可以帮助阻止雇用Schlemial Painters。
额外 作者 Fahim,
@PiersyP是的,这正是我想要暗示的:不同的受众认为不同的“密度”是最优的 - 而且大多数客观的代码指标都过于简单而忽视了这一点。
额外 作者 Bergi,
代码大小是在您需要阅读和理解代码时测量的,而不是行数或字符数。
额外 作者 Bergi,
@AndresF。是的,复杂性在我所说的“大小”中起着重要作用,但与大多数复杂度指标不同,我还会考虑格式和其他因素,如评论质量。无论如何,我认为我们可以同意普通文件大小在确定“代码越少越好”方面毫无用处。
额外 作者 Bergi,
我完全同意你的所作所为。 Robert C. Martin也会同意你的观点。
额外 作者 Billal Begueradj,
在高尔夫球场中,代码越少越好
额外 作者 Eugen Hruska,
和其他人一样,我认为最重要的因素是可读性和性能,而不是代码大小
额外 作者 Eugen Hruska,
@Philipp有许多代码涉及许多类之间分散的一堆进程。我的重构基本上是聚类相关的代码。在这个过程中,很多代码碎片化的类都消失了。例如,其接口和实现允许一个类接收来自另一个类的事件的通知。当您合并这些类时,该过程将消失,因为该过程变为2行,一行计算值,另一行将值作为参数传递。
额外 作者 F.leon,
@Bergi我喜欢这个主意但不幸的是,这种方法完全是主观的
额外 作者 F.leon,
为了进一步扩大人们所说的话并不总是更好,我会给你一个例子; RegEx,它们是意图的最大提炼版本,但该死的,它们很难阅读。
额外 作者 glts,
@Maciej Piechotka:为什么你认为变量(除了'三个'荒谬之外)没有正确命名?这是数学。数学通常使用这样的变量名称编写,并且早在计算机代码中实现之前就已存在。将变量更改为过于冗长的变量,并且混淆了a)具有数学背景的任何人,或者b)试图将其与数学论文或文本中的原始方法相关联。对于OP:考虑你的同事的评论只是酸葡萄的可能性: read.gov/aesop/ 005.html
额外 作者 Ruks,
@Maciej Piechotka:同意。有时我甚至会在评论中包含相关方程的LaTeX代码。
额外 作者 Ruks,
有一个完整的堆栈交换站点专门回答您的问题: codegolf.stackexchange.com 。 :)
额外 作者 jacktang1996,
“课程数减半” - 这是有趣的部分。你删除了哪些课程?
额外 作者 Philipp,

10 答案

瘦弱的人不一定比超重的人更健康。

一个980行儿童故事比450行物理论文更容易阅读。

有许多属性决定了代码的质量。 有些是简单的计算,如 Cyclomatic ComplexityHalstead复杂性。 其他更松散的定义,例如凝聚力,可读性,可理解性,可扩展性,稳健性,正确性,自我记录,清洁度,可测试性等等。

例如,当你减少代码的总长度时,你可能会引入额外的无根据的复杂性,并使代码更加神秘。

将一长段代码拆分成微小的方法可能有害,因为它可能是有益的

请您的同事向您提供具体的反馈,告诉他们为什么他们认为您的重构工作会产生不良后果。

122
额外
特别是在可读性方面+1。如果它有助于可读性,我很乐意将数字线加倍。如果您的同事无法轻易理解您的隐秘密集代码您没有改进的东西...那么说你可能只是通过如此短暂的显着简化使它们看起来很糟糕
额外 作者 Tim Mahy,
干得好,伙计们 - 你已经确定某处有一个“正确”的重量(具体数量可能会有所不同)。甚至@Neil的原始帖子都说“超重”而不是“更重的人”,这是因为有一个甜蜜点,就像编程一样。添加超出“正确大小”的代码只是混乱,删除该点下面的行只是为了简洁而牺牲了理解。知道那一点到底是什么......这是困难的一点。
额外 作者 vincent low,
@ray我同意美丽是旁观者的眼睛。显然,构成“平衡”代码的是完全主观的。我只想强调至少尝试平衡的重要性,而不是认为冗长等同于“更好”。
额外 作者 Justin Poliey,
@Neil你一般都是正确的,但你提到的那种难以捉摸的“平衡”是一种神话,客观地说话。每个人对“良好平衡”的含义都有不同的看法。显然,OP认为他做了一些好事,他的同事没有,但我确信他们都认为自己在编写代码时有“正确的平衡”。
额外 作者 Vineel Adusumilli,
@nocomprende:当然,尽管我认为我们的“减持”门槛需要修改。
额外 作者 DCookie,
@ M.A.Hanin这是事实。 “在一分钟内,有时间,对于愿景和修改,一分钟都会逆转。” (重构需要更长的时间,但莎士比亚没有写过这个)
额外 作者 user251748,
@AC请记住,最好的代码是不存在的代码。如果你没有它,那就做吧。
额外 作者 user251748,
@PiersyP我不认为重复数据删除代码不好只是因为它对圈复杂度没有显着影响。我认为这根本就不是重构。重构与“编辑现有代码”不同。重构是重复分解过程,意思是:(重新)将问题分解成部分。我读过Martin Fowler的书“重构”,其中充满了诸如“提取方法”之类的技术 - 这些技术对于重构过程非常有用。但仅仅应用技术并不意味着你实际上正在改变这些因素。
额外 作者 anil,
@PiersyP我没有参考书籍或文章,这是我在课程中教授的指南(IDesign的建筑师大师班)。
额外 作者 anil,
@PiersyP也,我不是说你的代码比它更糟或更好。作为一个局外人我无法分辨。也可能是你的同事过于保守,害怕你的改变只是因为他们没有做出审查和验证所需的努力。这就是为什么我建议你要求他们提供额外的反馈。
额外 作者 anil,
@PiersyP只是一个FYI,我被教导的关于良好重构的指导之一是我们应该看到圈复杂度降低到它最初的平方根
额外 作者 anil,
仅仅因为没有必要并不意味着它没有价值。
额外 作者 Chris Wohlert,
@ M.A Hanin你能否提供关于重构指南的参考,关于将圈复杂度降低到平方根?
额外 作者 F.leon,
在这种情况下,圈复杂度随着代码的减少而减半。
额外 作者 F.leon,
@ M.A.Hanin很有意思,我一直认为重新分解是在不改变功能的情况下改变代码结构。
额外 作者 F.leon,
@ M.A.Hanin我越想到这种平方根减少的圈复杂度是好的重新分解,它就越少有意义。例如,简单地删除重复重构,在几个位置提取方法,如果它正在做的是删除重复,则圈复杂度将基本不变。由于程序中的代码路径大部分都保持不变。我认为你很难说这不是一个好的重新分解因为圈复杂度不是它之前价值的根源。
额外 作者 F.leon,

有趣的是,一位同事和我目前处于一个重构的中间,它将增加类和函数的数量略少于两倍,尽管代码行将保持不变。所以我碰巧有一个好榜样。

在我们的例子中,我们有一层抽象,真的应该是两层。一切都挤进了ui层。通过将其分成两层,一切都变得更具凝聚力,并且测试和维护各个部分变得更加简单。

代码的 size 不是困扰你的同事,而是其他东西。如果他们无法清楚地表达出来,那么请尝试自己查看代码,就像从未见过旧的实现一样,并根据自己的优点进行评估,而不是仅仅进行比较。有时候,当我做一个很长的重构时,我会忽视原来的目标并把事情做得太过分。采取一个关键的“大局观”外观并将其重新放回正轨,也许是在一对程序员的帮助下,他的建议是你重视的。

35
额外
是的,肯定将UI与其他东西分开,这总是值得的。关于忽视原始目标的观点,我有点同意,但你也可以重新设计一些更好的东西,或者在更好的方式上。就像关于进化的旧观点(“有什么好处是一个翼的一部分?”)如果你从来没有花时间去改进它们,事情就不会改善。直到路上,你并不总是知道你要去哪里。我同意试图找出同事为什么感到不安,但也许这确实是“他们的问题”,而不是你的问题。
额外 作者 user251748,

我想起了阿尔伯特·爱因斯坦的一句话:

让一切尽可能简单,但并不简单。

当你过度修剪时,它会使代码更难以阅读。由于“容易/难以阅读”可能是一个非常主观的术语,我将完全解释我的意思:衡量熟练的开发人员在确定“这段代码有什么作用?”时所具有的难度。只需查看来源,无需专业工具的帮助。

像Java和Pascal这样的语言因其冗长而臭名昭着。人们常常指出某些语法元素,并嘲弄地说“他们只是为了让编译器的工作变得更容易”。除了“正义”部分之外,这或多或少都是正确的。信息越明确,代码就越容易阅读和理解,不仅是编译器,还有人类。

如果我说 var x = 2 + 2; ,很明显 x 应该是一个整数。但是,如果我说 var foo = value.Response; ,那么 foo 代表什么或者它的属性和功能是多么不清楚。即使编译器可以很容易地推断它,它也会给一个人带来更多的认知努力。

请记住,必须编写程序供人们阅读,并且只有机器才能执行。 (具有讽刺意味的是,这句话来自一本专门讨论极难阅读的语言的教科书!)删除冗余的东西是一个好主意,但不要带走使你的同胞更容易接受的代码弄清楚发生了什么,即使对于正在编写的程序来说并不是绝对必要的。

18
额外
var 示例不是一个特别好的简化示例,因为大多数时候阅读和理解代码涉及在某个抽象级别找出行为,因此通常了解特定变量的实际类型不会改变任何东西(它只能帮助你理解较低的抽象)。一个更好的例子是将多行简单代码压缩成单个卷积语句 - 例如 if((x = Foo())!=(y = Bar())&& CheckResult(x,y))需要时间来理解,并且知道 xy 丝毫没有帮助。
额外 作者 Ben Cottrell,

更长的代码可能更容易阅读。它通常是相反的,但有很多例外 - 其中一些在其他答案中列出。

但让我们从不同的角度看。我们假设新代码将被大多数技术熟练的程序员视为优秀,他们在不了解公司文化,代码库或路线图的情况下看到2段代码。即使这样,有很多理由反对新代码。为简洁起见,我将称之为“人们批评新代码” Pecritenc

  • 稳定性。如果已知旧代码是稳定的,则新代码的稳定性是未知的。在使用新代码之前,仍然需要对其进行测试。如果由于某种原因无法进行适当的测试,则更改是一个相当大的问题。即使可以进行测试,Pecritenc也可能认为努力不值得(代价)改进代码。
  • 性能/缩放。旧代码可能会更好地扩展,Pecritenc认为性能将成为一个问题,因为客户端和功能很快就会堆积起来。
  • 扩展。旧代码可能允许轻松引入Pecritenc假设即将添加的一些功能*。
  • 熟悉。旧代码可能重用了公司代码库中其他5个地方使用的模式。与此同时,新代码使用了一种奇特的模式,此时公司只有一半听说过。
  • 猪上的口红。 Pecritenc可能认为旧代码和新代码都是垃圾或无关紧要,因此它们之间的任何比较都毫无意义。
  • 傲慢。 Pecritenc可能是该代码的原始作者,并不喜欢人们对其代码进行大量更改。他甚至可能认为改善是一种轻微的侮辱,因为他们暗示他应该做得更好。
15
额外
此外,有问题的代码可能不是关键代码,因此被认为是浪费工程资源来清理它。
额外 作者 Ayyash,
'Pecritenc'的+1,以及在预制之前应该预先考虑的合理反对意见的非常好的总结。
额外 作者 user251748,
@MilindR可能是一种偏见,偏好,或者个人偏好?或者,也许根本没有理由,共同因素的宇宙汇合,混淆阴谋的条件。不知道,真的。你呢?
额外 作者 user251748,
@nocomprende我首先把它读作“先前重新考虑”,“以前的重构”和“以前合理”:-)
额外 作者 Milind R,
@nocomprende你使用preasonable,preconsidered和prefactoring的任何理由?可能与Pecritenc类似的方法?
额外 作者 Milind R,
并且+1表示“可扩展性” - 我认为原始代码可能具有旨在用于未来项目的函数或类,因此抽象可能看似多余或不必要,但仅限于单个程序的上下文中。
额外 作者 krcko,

Computational performance. When optimizing pipe-lining or running parts of you code in parallel it might be beneficial to, for example not loop from 1 to 400, but from 1 to 50 and put 8 instances of similar code in each loop. I am not assuming this was the case in your situation, but it is an example where more lines is better (performance-wise).

2
额外
一个好的编译器应该比一般的程序员更了解如何为特定的计算机体系结构展开循环,但一般的观点是有效的。我曾经从Cray高性能库中查看了矩阵乘法例程的源代码。矩阵乘法是三个嵌套循环,总共约6行代码,对吧?错了 - 该库例程运行到大约1100行代码,加上类似数量的注释行解释为什么它这么长!
额外 作者 picciopiccio,
@alephzero哇,我很想看到那个代码,它一定只是Cray Cray。
额外 作者 user251748,
@alephzero,好的编译器可以做很多事,但遗憾的是不是一切。好的一面是那些让编程变得有趣的东西!
额外 作者 Jack Riminton,
@alephzero实际上,良好的矩阵乘法代码不仅可以减少一点时间(即将其减去常数因子),它使用具有不同渐近复杂度的完全不同的算法,例如: Strassen算法大致为O(n ^ 2.8)而不是O(n ^ 3)。
额外 作者 John,

这完全取决于。我一直在研究一个不允许布尔变量作为函数参数的项目,而是需要为每个选项提供专用的 enum

所以,

enum OPTION1 { OPTION1_OFF, OPTION1_ON };
enum OPTION2 { OPTION2_OFF, OPTION2_ON };

void doSomething(OPTION1, OPTION2);

比起来更冗长

void doSomething(bool, bool);

然而,

doSomething(OPTION1_ON, OPTION2_OFF);

比可读性更强

doSomething(true, false);

编译器应为两者生成相同的代码,因此使用较短的表单无法获得任何内容。

1
额外

什么样的代码更好可能取决于程序员的专业知识以及他们使用的工具。例如,这就是为什么通常被认为编写得不好的代码在某些情况下可能比充分利用继承的编写良好的面向对象代码更有效:

(1)一些程序员对面向对象编程没有直观的把握。如果你对软件项目的比喻是电路,那么你会期望大量的代码重复。您希望在许多类中看到或多或少相同的方法。它们会让你有宾至如归的感觉。一个项目,你必须在父类或甚至祖父母的类中查找方法,看看发生了什么可能会感到敌意。您不想了解父类的工作原理,然后了解当前类的不同之处。您希望直接了解当前类的工作方式,并且您发现信息分散在多个文件中这一事实令人困惑。

此外,当您只想修复特定类中的特定问题时,您可能不想考虑是直接在基类中修复问题还是覆盖当前感兴趣类中的方法。 (如果没有继承,你就不必采取有意识的决定。默认情况下只是忽略类似类中的类似问题,直到它们被报告为错误。)这最后一个方面实际上并不是一个有效的参数,尽管它可能解释了一些反对。

(2)一些程序员经常使用调试器。虽然总的来说我自己坚定地站在代码继承和防止重复的一边,但我分享了在调试面向对象代码时我在(1)中描述的一些挫败感。当你执行代码时,它有时会在(祖先)类之间跳转,即使它停留在同一个对象中。此外,当在编写良好的代码中设置断点时,更有可能在没有帮助的情况下触发,因此您可能不得不花费精力使其成为条件(在可行的情况下),或者甚至在相关触发之前手动连续多次。

1
额外
“祖父母班”!山楂!请注意亚当和夏娃的课程。 (当然是神级)在那之前,它没有形式,也没有。
额外 作者 user251748,

计算机代码需要做很多事情。不做这些事情的“极简主义”代码不是好代码。

例如,计算机程序应该涵盖所有可能的(或者至少是所有可能的情况)。如果一段代码只覆盖一个“基本案例”并忽略其他代码,那么它就不是好代码,即使它很简短。

计算机代码应该是“可扩展的”。一个神秘的代码可能只适用于一个专门的应用程序,而一个更长但更开放的程序可能会更容易添加新的应用程序。

计算机代码应该清楚。正如另一位回答者所展示的那样,核心编码器有可能产生一种“算法”类型的功能来完成这项工作。但在普通程序员明白之前,必须将单行分为五个不同的“句子”。

0
额外
责任在旁观者的眼中。
额外 作者 user251748,
  • When less code doesn't do the same job as more code. Refactoring for simplicity is good, but you must take care not to oversimplify the problem space that this solution meets. 980 lines of code might handle more corner cases than 450.
  • When less code doesn't fail as gracefully as more code. I've seen a couple "ref***toring" jobs done on code to remove "unnecessary" try-catch and other error-case handling. The inevitable result was instead of showing a dialog box with a nice message about the error and what the user could do, the app crashed or YSODed.
  • When less code is less maintainable/extensible than more code. Refactoring for conciseness of code often removes "unnecessary" code constructs in the interest of LoC. Trouble is, those code constructs, like parallel interface declarations, extracted methods/subclasses etc are necessary should this code ever need to do more than it currently does, or do it differently. In the extreme, certain solutions custom-tailored to the specific problem may not work at all if the problem definition changes just a little bit.

    One example; you have a list of integers. Each of these integers has a duplicate value in the list, except for one. Your algorithm must find that unpaired value. The general-case solution is to compare every number against every other number until you find a number that has no dupe in the list, which is an N^2-time operation. You could also build a histogram using a hashtable, but that's very space-inefficient. However, you can make it linear-time and constant-space by using a bitwise XOR operation; XOR every integer against a running "total" (starting with zero), and at the end, the running sum will be the value of your unpaired integer. Very elegant. Until the requirements change, and more than one number in the list could be unpaired, or the integers include zero. Now your program either returns garbage or ambiguous results (if it returns zero, does that mean all elements are paired, or that the unpaired element is zero?). Such is the problem of "clever" implementations in real-world programming.

  • When less code is less self-documenting than more code. Being able to read the code itself and determine what it's doing is critical to team development. Giving a brain-f*** algorithm you wrote that performs beautifully to a junior developer and asking him to tweak it to modify the output slightly is not going to get you very far. Plenty of senior devs would have trouble with that situation as well. Being able to understand at any given time what the code is doing, and what could go wrong with it, is key to a working team development environment (and even solo; I guarantee you that the flash of genius you had when you wrote a 5-line method to cure cancer is going to be long gone when you come back to that function looking to make it cure Parkinson's too.)
0
额外

我认为凝聚力可能是一个问题。

例如,在Web应用程序中,让我们假设您有一个管理页面,您可以在其中索引所有产品,这与您在主页情况下使用的代码(索引)基本相同,只是为产品编制索引。

如果您决定将所有内容分开以便保持干爽和流畅,那么您必须添加很多条件,如果用户浏览是否是管理员,并且使用不必要的东西使代码混乱,这将使得它非常难以理解设计师!

所以在这样的情况下,即使代码几乎相同,只是因为它可以扩展到其他东西并且用例可能会稍微改变,通过添加条件和ifs来追踪每个代码是不好的。 因此,一个好的策略是放弃DRY概念并将代码分解为可维护的部分。

0
额外