您如何通过遵循干净的代码实践来证明编写更多代码是合理的?


106

主持人笔记
这个问题已经有十七个答案。在发布新答案之前,请阅读现有答案,并确保您的观点尚未得到适当覆盖。

我一直遵循罗伯特·马丁(Robert Martin)的“清洁代码”书中推荐的一些做法,尤其是那些适用于我使用的软件类型的做法以及对我有意义的做法(我不遵循它作为教条) 。

但是,我注意到的一个副作用是,我编写的“干净”代码比没有遵循某些实践的代码要多。导致这种情况的具体做法是:

  • 封装条件

所以代替

if(contact.email != null && contact.emails.contains('@')

我可以写一个像这样的小方法

private Boolean isEmailValid(String email){...}
  • 用另一个私有方法替换内联注释,以便方法名称描述自身,而不是在其顶部添加内联注释
  • 一堂课只有一个改变的理由

还有其他一些。关键是,可能是30行的方法最终成为一个类,这是因为微小的方法可以替换注释并封装条件等。当您意识到自己有这么多的方法时,它对将所有功能都放在一个类中,实际上它本来应该是一种方法。

我知道任何极端的做法都是有害的。

我正在寻找答案的具体问题是:

这是编写干净代码的可接受的副产品吗?如果是这样,我可以使用哪些论据来证明已编写了更多LOC这一事实呢?

该组织并不特别在意更多的LOC,但是更多的LOC可能会导致非常大的类(同样,为了便于阅读,可以用长方法代替一堆只使用一次的辅助函数)。

当您看到一个足够大的课程时,它给人的印象是该课程很忙,并且其职责已经结束。因此,您最终可能会创建更多的类来实现其他功能。结果就是很多类,都借助于许多小的辅助方法来“做一件事”。

这是特定的关注点……这些类可以是一个单一的类,仍然可以实现“一件事”,而无需许多小方法的帮助。它可能是单个类,可能带有3或4个方法以及一些注释。


98
如果您的组织仅将 LOC用作代码库的指标,那么证明干净的代码是毫无希望的。
Kilian Foth

24
如果将可维护性作为您的目标,则LOC 并不是判断的最佳指标-它是其中之一,但要考虑的不仅仅是简单地将其简化。
Zibbobz

29
这不是一个答案,而是要指出的一点:关于编写代码的整个子社区,请使用尽可能少的行/符号。codegolf.stackexchange.com人们可以争辩说,那里的大多数答案并不像它们可能的那样可读。
Antitheos

14
了解每一个最佳实践背后的原因,而不仅仅是规则自己。遵守规则是没有理由的。每个规则都有其自身的原因。
盖尔曼

9
顺便说一句,并使用您的示例,有时将事情推到方法上会让您想到:“也许有一个库函数可以做到这一点”。例如,要验证电子邮件地址,可以创建一个System.Net.Mail.MailAddress来为您验证它。然后,您可以(希望)信任该库的作者以使其正确无误。这意味着您的代码库将具有更多的抽象,并减小大小。
Gregory Currie

Answers:


130

...我们是一个很小的团队,支持相对较大且未记录的代码库(我们继承了),因此一些开发人员/经理认为编写更少的代码来完成工作具有价值,因此我们需要维护的代码更少

这些人正确地识别出一些东西:他们希望代码更易于维护。但是,他们出了错的地方是假设代码越少,维护起来就越容易。

要使代码易于维护,就必须易于更改。到目前为止,获得易于更改的代码的最简单方法是对其进行全套自动化测试,如果您的更改不成功,该测试将失败。测试是代码,因此编写这些测试将扩大您的代码库。那是一件好事。

其次,为了弄清楚需要更改的内容,您的代码既要易于阅读又要易于推理。非常简洁的代码,只是为了减少行数而缩小了大小,很难读取。显然有一个折衷的选择,因为更长的代码将需要更长的时间才能读取。但是,如果要更快地理解它,那是值得的。如果没有提供这种好处,那么冗长就不再是一种好处。但是,如果更长的代码提高了可读性,那么这又是一件好事。


27
“到目前为止,获得易于更改的代码的最简单方法是对其进行全套自动化测试,如果您所做的更改是一个重大突破,它将失败。” 这是不正确的。对于每次行为更改,测试都需要额外的工作,因为测试也需要更改,这是设计使然,许多人认为使更改更安全,但也必然使更改更难。
杰克·艾德利

63
可以,但是与那些您无法诊断和修复测试所阻止的错误的时间相比,维持这些测试所花费的时间相形见war 。
MetaFight

29
@JackAidley,必须随代码一起更改测试可能会带来更多工作,但前提是只有忽略了未经测试的代码更改会带来的难以发现的错误,并且这些错误通常要等到发货后才能找到。后者仅提供较少工作的错觉。
David Arno

31
@JackAidley,我完全不同意你的看法。测试使代码更易于更改。我将承认,设计太糟糕的代码过于紧密地耦合在一起,因此很难与测试紧密耦合,这很难改变,但是结构良好,经过良好测试的代码在我的经验中很容易改变。
David Arno

22
@JackAidley您可以进行很多重构,而无需更改任何API或接口。这意味着您可以在修改代码时发疯,而不必在单元测试或功能测试中更改任何一行。也就是说,如果您的测试没有测试特定的实现。
Eric Duminil

155

是的,这是可以接受的副产品,其理由是它现在已经结构化,因此您不必在大多数时间都阅读大多数代码。而不是每次更改时都读取30行功能,而是读取5行功能以获取总体流程,如果更改涉及该区域,则可能需要几个辅助功能。如果调用了新的“额外”类,EmailValidator并且您知道问题不在于电子邮件验证,则可以完全跳过阅读。

重用较小的部分也更容易,这往往会减少整个程序的行数。An EmailValidator可以在各处使用。某些执行电子邮件验证但与数据库访问代码捆绑在一起的代码行无法重用。

然后考虑如果需要更改电子邮件验证规则,该怎么做-您希望:一个已知位置;或很多位置,可能缺少一些?


10
更好的答案就是疲倦的“单元测试解决了您所有的问题”
Dirk Boer

13
这个答案的关键点是Bob叔叔和他的朋友们似乎总是很想念-仅在不必阅读所有小的方法来弄清楚代码在做什么的情况下,重构为小的方法才有用。创建一个单独的类来验证电子邮件地址是明智的。将代码iterations < _maxIterations拖入称为的方法ShouldContinueToIterate愚蠢的
BJ Myers

4
@DavidArno:“要有用”!=“解决了所有问题”
Christian Hackl

2
@DavidArno:当有人抱怨暗示单元测试“解决了您的所有问题”时,他们显然意味着暗示单元测试解决了或至少有助于解决软件工程中几乎所有问题的人。我认为没有人会指责有人建议通过单元测试来结束战争,贫穷和疾病。另一种表达方式是,(正确地)批评(不仅对这个问题,而且对整个SE)在许多答案中对单元测试的极端高估。
Christian Hackl

2
@DavidArno,您好,我的评论显然是夸张的,不是稻草人;)对我来说,这是这样的:我问如何固定汽车,宗教人士过来告诉我,我应该过少犯罪的生活。从理论上讲,有一些值得讨论的东西,但这并不能真正帮助我在修理汽车方面做得更好。
Dirk Boer

34

比尔·盖茨的著名说法是:“用代码行来衡量编程进度就像用重量来衡量飞机制造进度。”

我谦卑地同意这一观点。这并不是说一个程序应该争取更多或更少的代码行,但这并不是最终创建一个正常运行的程序至关重要。它有助于记住,最终添加额外的代码行的原因是,从理论上讲,这种方式更具可读性。

对于某个更改是否或多或少可读性可能存在分歧,但是我认为您对程序进行更改不会错,因为您认为这样做可以提高可读性。例如,制作一个isEmailValid可以被认为是多余的和不必要的,尤其是在定义它的类仅一次调用它的情况下。但是,我宁愿看到isEmailValid一个条件,而不是一连串的ANDed条件,因为我必须确定每个条件检查的内容以及为什么要检查它。

遇到麻烦的地方是当您创建一种isEmailValid具有副作用或检查电子邮件以外的内容的方法时,因为这比简单地全部写出来要。更糟糕的是,它会误导我,因此我可能会错过一个错误。

尽管显然您在这种情况下不这样做,所以我鼓励您继续这样做。您应该经常问自己,如果进行更改,是否更容易阅读,如果是这种情况,那就去做吧!


1
飞机重量是一个重要指标。在设计过程中,预期重量会受到密切监控。不是进步的标志,而是制约。监控代码行表明,越多越好,而在飞机设计中,重量越轻越好。因此,我认为盖茨先生可以为他的观点选择一个更好的例证。
乔斯

21
与特定OP团队合作的@jos,看来“更好”的LOC更少。比尔·盖茨(Bill Gates)提出的观点是,LOC 进度没有任何有意义的联系,就像飞机的重量与进度没有任何意义一样。在建的飞机可能相对较快地达到了其最终重量的95%,但是这只是一个空壳,没有控制系统,还没有完成95%。在软件中也是如此,如果程序有10万行代码,并不意味着每1000行提供1%的功能。
Mindor先生

7
进度监视是一项艰巨的任务,不是吗?可怜的经理。
乔斯

@jos:在代码中,如果其他所有条件都相同,则最好为同一功能使用更少的行。
RemcoGerlich

@jos请仔细阅读。盖茨没有透露重量是否对飞机本身很重要。他说,重量是衡量飞机制造进度的可怕指标。毕竟,通过这种方法,一旦将整个船体抛在了地上,就可以基本完成了,因为这大概占整个飞机重量的9%。
Voo

23

因此,一些开发人员/经理认为编写更少的代码来完成工作具有价值,因此我们需要维护的代码更少

这是对实际目标视而不见的问题。

重要的是减少开发时间。那是按时间(或等效工作量)而不是代码行来衡量的。
这就像在说汽车制造商应该用更少的螺丝钉来制造汽车,因为每个螺丝钉的插入时间都不为零。虽然这是正确的做法,但汽车的市场价值并不取决于它的螺丝钉数量。或没有。首先,汽车必须具有高性能,安全性和易于维护的特点。

剩下的答案是干净代码如何导致时间节省的示例。


记录中

使用没有日志记录的应用程序(A)。现在创建应用程序B,它与应用程序A相同,但带有日志记录。B总是会有更多的代码行,因此您需要编写更多的代码。

但是,很多时间会投入到调查问题和错误,并找出问题出在哪里。

对于应用程序A,开发人员将被困在阅读代码的过程中,并且不得不不断重现问题并逐步遍历代码以查找问题的根源。这意味着开发人员必须在每个使用的层中从执行开始到结束进行测试,并且需要观察每个使用的逻辑。
也许他很幸运能够立即找到它,但是答案可能会在他认为要寻找的最后位置。

对于应用程序B,假设日志记录完美,开发人员将观察日志,可以立即识别出故障组件,并且现在知道在哪里查找。

这可以节省几分钟,几小时或几天。取决于代码库的大小和复杂性。


回归分析

以完全不适合DRY的应用程序A为例。
以应用程序B为例,它是DRY,但由于附加的抽象,最终需要更多的行。

提出了更改请求,这要求更改逻辑。

对于应用程序B,开发人员根据更改请求更改(唯一,共享)逻辑。

对于应用程序A,开发人员必须在他记得正在使用该逻辑的地方更改此逻辑的所有实例。

  • 如果他设法记住所有实例,则仍然必须多次实施相同的更改。
  • 如果他不能记住所有实例,那么您现在正在处理与自己矛盾的不一致的代码库。如果开发人员忘记了很少使用的代码,则该错误可能直到很长一段时间才对最终用户显而易见。那时,最终用户是否会确定问题的根源?即使是这样,开发人员也可能不记得所做的更改,因此必须找出如何更改此被遗忘的逻辑。也许那时开发人员甚至不在公司工作,然后其他人现在必须从头开始解决所有问题。

这会导致大量的时间浪费。不仅在开发中,而且还在寻找和发现错误中。应用程序可能会以开发人员无法轻易理解的方式开始出现异常行为。这将导致冗长的调试会话。


开发人员互换性

开发人员A创建的应用程序A。代码虽然不干净也不可读,但是它的工作原理就像一个魅力,已经在生产中运行。毫不奇怪,也没有文档。

由于假期,开发商A缺席一个月。紧急更改请求已提交。迫不及待要等三个星期,开发人员A才能返回。

开发人员B必须执行此更改。他现在需要读取整个代码库,明白了一切是如何工作的,为什么它的工作原理,以及它试图完成的任务。这需要很长时间,但可以说他可以在三周后完成。

同时,应用程序B(由开发人员B创建)处于紧急状态。开发人员B被占用,但开发人员C可用,即使他不知道代码库也是如此。我们做什么?

  • 如果我们让B在A上工作,而让C在B上工作,那么我们有两个开发人员不知道他们在做什么,那么工作就无法达到最佳效果。
  • 如果我们将B从A上拉开,然后让他去做B,现在我们将C放在A上,那么开发人员B的所有工作(或其中很大一部分)都可能最终被丢弃。这可能浪费了数天/数周的精力。

开发人员A休假回来,发现B不理解该代码,因此实施得很糟糕。这不是B的错,因为他使用了所有可用的资源,所以源代码只是不够可读。现在A是否必须花时间修复代码的可读性?


所有这些问题,甚至更多,最终都浪费了时间。是的,在短期内,干净的代码现在需要付出更多的努力,但是当需要解决不可避免的错误/更改时,它将最终在将来获得回报。

管理层需要了解,现在一个短任务将在将来为您节省一些长任务。不计划就是计划失败。

如果是这样,我可以使用哪些论据来证明已编写了更多LOC这一事实呢?

我的解释是询问管理人员他们更喜欢什么:一个可以在三个月内开发的带有100KLOC代码库的应用程序,或者可以在六个月内开发的一个50KLOC代码库的应用程序。

他们显然会选择较短的开发时间,因为管理层并不关心KLOC。专注于KLOC的经理在进行微观管理的同时,不了解他们要管理的内容。


23

我认为您在应用“干净的代码”实践时应该非常小心,以防它们导致更大的整体复杂性。过早的重构是许多不好的事情的根源。

提取条件的功能,带来更简单的代码在距提取条件的点,但会导致更多的整体复杂性,因为你现在有一个函数,它是从程序中的多个点可见。在现在可以看到此新功能的所有其他功能上,您增加了一点复杂性负担。

我并不是说您不应该提取条件,只是在需要时应该仔细考虑。

  • 如果要专门测试电子邮件验证逻辑。然后,您需要将该逻辑提取到一个单独的函数中-甚至可能是类。
  • 如果在代码的多个位置使用了相同的逻辑,那么显然您必须将其提取到单个函数中。不要重复自己!
  • 如果逻辑显然是单独的责任,例如,电子邮件验证发生在排序算法的中间。电子邮件验证将独立于排序算法而更改,因此它们应位于单独的类中。

在所有上述情况中,这是提取之外的原因,而仅仅是“干净的代码”。此外,您可能甚至不会怀疑它是否正确。

我会说,如果有疑问,请始终选择最简单,最直接的代码。


7
我必须同意,在维护和代码审查方面,将每个条件转换为一种验证方法都会带来更多不必要的复杂性。现在,您必须在代码中来回切换,以确保您的条件方法正确。当您在相同条件下具有不同条件时会发生什么?现在,您可能会遇到带有几种小的方法的命名梦small,这些方法只会被调用一次,并且看起来几乎相同。
pboss3010

7
最好的答案就在这里。特别是(在第三段中)观察到的复杂性不仅仅是整个代码整体的属性,而是同时存在于多个抽象级别上并且有所不同的某种东西。
Christian Hackl

2
我认为,表达这种情况的一种方法是,通常,只有在该条件有一个有意义的,不混淆的名称时,应提取条件。这是必要条件,但不是充分条件。
JimmyJames

Re “ ...,因为您现在有了一个可以从程序中更多点看到的函数”:在Pascal中,可以有局部函数- “ ...每个过程或函数可以具有自己的goto标签,常量声明。 ,类型,变量以及其他过程和函数,...”
Peter Mortensen

2
@PeterMortensen:在C#和JavaScript中也可以。太好了!但是重点仍然是,与内联代码片段相比,在更大的范围内可以看到一个函数,甚至是一个局部函数。
雅克B

9

我要指出的是,这本质上没有错:

if(contact.email != null && contact.email.contains('@')

至少假设它已使用过一次。

我可能很容易遇到这个问题:

private Boolean isEmailValid(String email){
   return email != null && email.contains('@');
}

我要注意的几件事:

  1. 为什么是私人的?它看起来像一个潜在有用的存根。作为私有方法有用吗?没有机会被更广泛地使用吗?
  2. 我不会亲自给方法IsValidEmail命名,可能不会包含ContainsAtSignLooksVaguelyLikeEmailAddress,因为它几乎不进行任何实际验证,这可能很好,也许不是预期的结果。
  3. 它被多次使用了吗?

如果只使用一次,就很容易解析,而且花费不到一行,我会第二次猜测这个决定。如果不是团队中的特殊问题,那可能不是我要讲的。

另一方面,我已经看到方法做了这样的事情:

if (contact.email != null && contact.email.contains('@')) { ... }
else if (contact.email != null && contact.email.contains('@') && contact.email.contains("@mydomain.com")) { //headquarters email }
else if (contact.email != null && contact.email.contains('@') && (contact.email.contains("@news.mydomain.com") || contact.email.contains("@design.mydomain.com") ) { //internal contract teams }

该示例显然不是DRY。

甚至就是最后一条语句都可以给出另一个示例:

if (contact.email != null && contact.email.contains('@') && (contact.email.contains("@news.mydomain.com") || contact.email.contains("@design.mydomain.com") )

目标应该是使代码更具可读性:

if (LooksSortaLikeAnEmail(contact.Email)) { ... }
else if (LooksLikeFromHeadquarters(contact.Email)) { ... }
else if (LooksLikeInternalEmail(contact.Email)) { ... }

另一种情况:

您可能有类似以下方法:

public void SaveContact(Contact contact){
   if (contact.email != null && contact.email.contains('@'))
   {
       contacts.Add(contact);
       contacts.Save();
   }
}

如果这符合您的业务逻辑并且不被重用,则这里没有问题。

但是当有人问“为什么保存'@',因为那是不对的!”时,然后您决定添加某种形式的实际验证,然后将其提取!

当您还需要考虑总统的第二个电子邮件帐户Pr3 $ sid3nt @ h0m3!@ mydomain.com并决定全力以赴并尝试支持RFC 2822 时,您会感到很高兴。

关于可读性:

// If there is an email property and it contains an @ sign then process
if (contact.email != null && contact.email.contains('@'))

如果您的代码如此清晰,则无需在此处添加注释。实际上,您不需要注释来说明代码大部分时间在做什么,而是在说什么原因

// The UI passes '@' by default, the DBA's made this column non-nullable but 
// marketing is currently more concerned with other fields and '@' default is OK
if (contact.email != null && contact.email.contains('@'))

在我看来,无论是if语句上方的注释还是小方法内的注释,都是腐的。我甚至可能认为与内部的另一种方法好评论有用相反,因为现在你将不得不转到另一种方法来看看如何以及为什么它做它做什么。

总结:不要测量这些东西;重点关注文本的构建原则(DRY,SOLID,KISS)。

// A valid class that does nothing
public class Nothing 
{

}

3
Whether the comments above an if statement or inside a tiny method is to me, pedantic.这是一个“稻草打断了骆驼的背”的问题。没错,这件事并不难读。但如果你有其中有几十这些小评估的方法,较大(如大进口),具有这些封装在可读的方法名称(IsUserActiveGetAverageIncomeMustBeDeleted,...)将成为阅读的代码明显的改善。该示例的问题在于,它仅观察到一根稻草,而不观察到破坏骆驼后背的整个捆。
平坦的

@Flater,我希望这是读者从中获得的精神。
AthomSfere

1
这种“封装”是一种反模式,答案实际上证明了这一点。我们回来阅读代码是出于调试和扩展代码的目的。在两种情况下,了解代码的实际作用都是至关重要的。代码块的启动if (contact.email != null && contact.email.contains('@'))存在错误。如果if为false,则else if行都可以为true。这在LooksSortaLikeAnEmail块中根本看不到。包含一行代码的函数并不比注释解释一行的工作原理好多少。
夸克

1
充其量,间接的另一层模糊了实际的机制,使调试更加困难。最糟糕的是,函数名已变成谎言,就像注释变成谎言一样-内容已更新,但名称未更新。一般而言,这并不是针对封装的攻击,但是这种特殊用法是“企业”软件工程的一个伟大的现代问题的征兆-抽象的层和层将相关的逻辑掩盖起来。
夸克

@quirk我认为您同意我的总体观点?有了胶水,您将面临一个完全不同的问题。在查看新的团队代码时,我实际上使用了代码映射。我见过的一些大型方法即使在mvc模式级别也调用了一系列大型方法,这是非常可怕的。
AthomSfere

6

清洁代码是一本非常好的书,非常值得一读,但是它并不是此类事情的最终权威。

将代码分解为逻辑函数通常是一个好主意,但是很少有程序员像Martin那样去做-在某些时候,将所有内容转换为函数的收益都会减少,而当所有代码都很小时,很难遵循件。

当不值得创建一个新函数时,一个选择就是简单地使用一个中间变量:

boolean isEmailValid = (contact.email != null && contact.emails.contains('@');

if (isEmailValid) {
...

这有助于使代码易于遵循,而不必在文件中四处跳动。

另一个问题是,“ 清洁代码”现在已经很老了。许多软件工程朝功能编程的方向发展,而Martin则竭尽全力为事物添加状态并创建对象。我怀疑如果他今天写这本书,他会写完全不同的书。


有些人担心条件附近的多余代码行(我一点也不在意),但也许可以在您的回答中解决。
Peter Mortensen

5

考虑到您当前具有的“有效的电子邮件”条件将接受非常无效的电子邮件地址“ @”,我认为您有充分的理由抽象出EmailValidator类。更好的是,使用一个经过良好测试的良好库来验证电子邮件地址。

代码行作为度量标准是没有意义的。软件工程中的重要问题不是:

  • 您的代码太多了吗?
  • 您的代码太少了吗?

重要的问题是:

  • 整个应用程序设计正确吗?
  • 代码是否正确实施?
  • 代码可以维护吗?
  • 代码可以测试吗?
  • 代码是否经过充分测试?

除了Code Golf以外,出于任何目的编写代码时,我从未考虑过LoC。我问自己“我能不能更简洁地写这个?”,但出于可读性,可维护性和效率的目的,而不仅仅是长度。

当然,也许我可以使用一连串的布尔运算而不是实用程序方法,但是应该吗?

您的问题实际上使我回想起我写的一些长布尔值,并意识到我可能应该写一个或多个实用方法。


3

一方面,它们是正确的-更少的代码更好。引用盖特的另一个答案,我更喜欢:

“如果调试是消除软件错误的过程,那么编程必须是将其引入的过程。” – Edsger Dijkstra

“调试时,新手会插入纠正代码;专家删除有缺陷的代码。” –理查德·帕蒂斯(Richard Pattis)

最便宜,最快和最可靠的组件是那些不存在的组件。-戈登·贝尔

简而言之,您拥有的代码越少,出错的机会就越少。如果不需要某些东西,请切掉。
如果存在过于复杂的代码,请对其进行简化,直到剩下真正的功能元素为止。

这里重要的是,所有这些都涉及功能,并且仅具有执行功能所需的最低限度。它没有说明它的表达方式。

尝试使用干净的代码所做的事情与上面的说法不符。您要添加到LOC,但不添加未使用的功能。

最终目标是拥有可读的代码,但没有多余的额外功能。这两个原则不应相互抵触。

一个比喻是在造汽车。该代码的功能部分是底盘,发动机,车轮……是什么使汽车行驶。分解方式更像是悬架,动力转向等,它使操作变得更容易。您希望您的机械师在执行工作的同时尽可能简单,以最大程度地减少出现问题的机会,但这不会妨碍您获得良好的席位。


2

现有答案中有很多智慧,但是我想补充一个因素:语言

为了获得相同的效果,某些语言比其他语言需要更多的代码。特别是,尽管Java(我怀疑是问题所在的语言)非常著名,并且通常非常扎实,清晰和直接,但是一些更现代的语言更加简洁和富于表现力。

例如,在Java中,轻松地用50行编写一个具有三个属性的新类,每个属性都有一个getter和setter以及一个或多个构造函数,而您可以在一行Kotlin *或Scala中完成完全相同的操作。(如果您还想要合适的,和方法equals(),则可以节省更多费用。)hashCode()toString()

结果是在Java中,额外的工作意味着您更有可能重用真正不合适的通用对象,将属性压缩到现有对象中,或者单独传递一堆“裸”属性。而使用简洁,富有表现力的语言时,您更有可能编写更好的代码。

(这突显了代码的“表面”复杂度与其实现的想法/模型/处理的复杂度之间的区别。代码行对前者并不是一个不好的衡量标准,但是与后两者无关)

因此,正确行事的“成本”取决于语言。也许一种好的语言的标志就是不会让您在做得好和做简单之间做出选择!

(*并不是真正适合使用插头的地方,但是Kotlin非常值得一看,恕我直言。)


1

假设您当前正在使用类Contact。您正在编写另一种用于验证电子邮件地址的方法,这一事实证明了该类Contact未处理单一职责。

它还正在处理一些电子邮件责任,理想情况下,这应该是它自己的类。


证明您的代码是ContactEmail类的融合的进一步证明是,您将无法轻松测试电子邮件验证代码。要使用正确的值以一种大方法获得电子邮件验证代码,将需要大量的操作。请参见下面的方法。

private void LargeMethod() {
    //A lot of code which modifies a lot of values. You do all sorts of tricks here.
    //Code.
    //Code..
    //Code...

    //Email validation code becoming very difficult to test as it will be difficult to ensure 
    //that you have the right data till you reach here in the method
    ValidateEmail();

    //Another whole lot of code that modifies all sorts of values.
    //Extra work to preserve the result of ValidateEmail() for your asserts later.
}

另一方面,如果您有一个单独的Email类,带有用于电子邮件验证的方法,则要对验证代码进行单元测试,您只需对Email.Validation()测试数据进行简单的调用即可。


奖励内容: MFeather谈论可测试性与良好设计之间的深层协同作用。


1

已经发现,LOC的降低与缺陷的减少相关,仅此而已。假设任何时候降低LOC,就减少了缺陷的可能性,本质上是陷入相信关联等于因果关系的陷阱。降低的LOC是良好开发实践的结果,而不是使代码良好的原因。

以我的经验,可以用更少的代码(在宏级别)解决问题的人比编写更多的代码来完成相同工作的人更有技能。这些熟练的开发人员为减少代码行而要做的是使用/创建抽象以及可重用的解决方案来解决常见问题。他们不用花时间计算代码行数,也不必为在此处或此处剪切行而烦恼。他们确实编写的代码通常比必要的更为冗长,而只编写较少的代码。

让我给你举个例子。我不得不处理有关时间段的逻辑,它们如何重叠,它们是否相邻以及它们之间存在什么差距。当我第一次开始解决这些问题时,我将有许多代码块在各处进行计算。最终,我建立了类来表示计算重叠,补码等的时间段和运算。这立即删除了大量代码,并将它们转变为几个方法调用。但是这些类本身根本不是写得很简洁。

明确地说:如果您试图通过在此处或此处更简短地剪切一行代码来减少LOC,则您做错了。这就像通过减少所吃的蔬菜来减肥一样。编写易于理解,维护和调试的代码,并通过重用和抽象来减少LOC。


1

您确定了一个有效的权衡

因此,这里确实存在一个折衷,这是整个抽象固有的。每当有人尝试将N行代码放入其自己的函数中以对其进行命名和隔离时,它们同时使读取调用站点变得更加容易(通过引用一个名称,而不是引用该名称背后的所有细节,)以及更复杂(您现在的含义已经纠缠在代码库的两个不同部分中)。“简单”与“困难”相反,但它不是“简单”的同义词,而与“复杂”相反。两者不是相反的,抽象总是增加复杂性以便插入某种形式或另一种形式。

当业务需求中的某些更改使抽象开始泄漏时,我们可以直接看到增加的复杂性。也许某些新逻辑会在预提取代码的中间最自然地消失,例如,如果抽象代码遍历了某些树,而您确实想在您收集代码的同时收集(或采取行动)某种信息遍历树。同时,如果您已对该代码进行了抽象,则可能还有其他调用站点,并且将所需的逻辑添加到方法的中间可能会破坏其他调用站点。看到,只要我们更改一行代码,我们只需要查看该行代码的直接上下文即可;当我们更改方法时,我们必须对整个源代码执行Cmd-F命令,以查找可能因更改该方法的约定而中断的任何内容,

在这些情况下,贪心算法可能会失败

复杂性也使代码在某种意义上不易读,而不是更多。在先前的工作中,我处理了非常仔细且精确地构造为多层的HTTP API,每个端点均由控制器指定,该控制器将验证传入消息的形状,然后将其交给某个“业务逻辑层”管理器。 ,然后向“数据层”提出一些请求,该“数据层”负责对某个“数据访问对象”层进行多次查询,该层负责创建多个实际上可以回答您问题的SQL委托。我要说的第一件事是,大约90%的代码是复制粘贴样板,换句话说,它是无操作。因此,在许多情况下,读取任何给定的代码通过都是非常“容易的”,因为“哦,这个管理员只是将请求转发到该数据访问对象。”许多上下文切换和查找文件,并试图跟踪您不应该跟踪的信息,“在此层中此名称为X,在另一层中名称为X',在另一层中名称为X”其他层。”

我想当我离开时,这个简单的CRUD API处于这样一个阶段:如果您以每页30行的价格打印它,那么它将在一个书架上占用10至20册五百页的教科书:这是一本完整的重复性百科全书码。在基本复杂性方面,我不确定其中是否存在一半的基本复杂性教科书。我们可能只有5-6个数据库图来处理它。对其进行任何细微的更改都是一项艰巨的任务,得知这是一项艰巨的任务,添加新功能非常痛苦,以至于我们实际上拥有可用于添加新功能的样板模板文件。

因此,我亲眼看到了如何使每个部分都非常易读和明显,从而使整个部分变得非常不可读和不明显。这意味着贪婪算法可能会失败。您知道贪婪算法,是吗?他说:“我将在当地采取任何措施来最大程度地改善这种状况,然后我相信自己会在全球范围内处于改善状态。” 这通常是一次美丽的尝试,但在复杂的环境中也可能会错过。例如,在制造过程中,您可能会尝试提高复杂制造过程中每个特定步骤的效率-进行更大的批量生产,对地板上的人大喊大叫,他们似乎无所事事而忙于其他事情-并且这通常会破坏系统的整体效率。

最佳做法:使用DRY和长度拨打电话

(注意:此部分标题有些开玩笑;我经常告诉我的朋友,当有人说“我们应该做X,因为最佳实践这样说 ”时,他们有90%的时间不谈论诸如SQL注入或密码散列之类的东西或任何方面(单方面的最佳做法),因此该声明可以在90%的时间内转换为“我们应该这样做,因为我这样说。”就像他们可能从某些企业获得一些博客文章而做得更好X而不是X',但通常不能保证您的业务与该业务相似,并且通常还有其他公司的其他文章使用X'而不是X做得更好。因此,请不要使用标题认真地。)

我建议的依据是Jack Diederich的名为Stop Writing Classes(youtube.com)的演讲。他在讲话中提出了几点要点:例如,当一个类只有两个公共方法,并且其中一个是构造函数/初始化程序时,您就可以知道它实际上只是一个函数。但是在一种情况下,他正在谈论他如何将字符串假想库替换为“ Muffin”声明自己的类“ MuffinHash”,dict该类是Python拥有的内置类型的子类。该实现完全是空的-有人刚刚想到:“以后我们可能需要向Python字典添加自定义功能,为防万一,让我们现在介绍一个抽象。”

他的挑衅反应很简单,“如果需要,我们总是可以以后再做。”

我认为我们有时会假装我们将来的程序员会比现在变得更糟,因此我们可能想插入一些可能使我们将来感到高兴的小东西。我们预期未来的需求。“如果流量比我们预期的大100倍,则该方法将无法扩展,因此我们必须将前期投资投入到这种更难扩展的方法中。” 非常可疑。

如果我们认真对待该建议,那么我们需要确定何时“以后”到来。可能最明显的事情是出于样式原因而确定事物长度的上限。而且我认为,剩下的最佳建议是使用DRY(不要重复自己)以及有关行长的启发式方法,以弥补SOLID原理中的漏洞。根据30行是文本的“页面”并与散文进行类比的启发,

  1. 当您要复制粘贴功能时,将其重构为功能/方法。就像偶尔有复制粘贴的正当理由,但您应该始终对此感到不舒服。真正的作者不会使您在整个叙述中重复阅读一个大而冗长的句子50次,除非他们真的在尝试突出主题。
  2. 函数/方法理想情况下应为“段落”。大多数函数的长度应为一页的一半左右,或1-15行代码,并且仅应允许将函数的10%更改为一页的一半,45行或更多。一旦您拥有120余行代码和注释,就需要将其分解为几部分。
  3. 理想情况下,文件应为“章节”。大多数文件的长度应少于或等于12页,因此应包含360行代码和注释。也许只允许文件的10%到50页长或1500行代码和注释。
  4. 理想情况下,您的大多数代码应与函数的基线或一级深入缩进。根据对Linux源代码树的一些启发,如果您对它有信仰,则可能只在基线内将10%的代码缩进2级或更多,而将5%缩进3级或更多。尤其是这意味着需要“包装”其他问题的事物,例如大try / catch中的错误处理,应该从实际逻辑中撤出。

就像我在上面提到的那样,我针对当前的Linux源代码树测试了这些统计数据,以找到大致的百分比,但是在文学类比中,它们也可以说是合理的。

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.