我已经读过罗伯特·C·马丁(Robert C. Martin)的一本名为《清洁代码》的书。在本书中,我看到了许多清理代码的方法,例如编写小的函数,仔细选择名称等。这似乎是迄今为止我所读过的最有趣的有关干净代码的书。但是,今天我的老板不喜欢我读完本书后写代码的方式。

他的论点是


写小函数很痛苦,因为它迫使您进入每个小函数以查看代码的作用。
即使主循环超过300行,也可以将所有内容放入主大循环中。
仅写入
不要编写带有注释名称的函数,而是将复杂的代码行(3-4行)上面带有注释;同样,您可以直接修改失败的代码

,这与我阅读的所有内容均不符。您通常如何编写代码?一个主要的大循环,没有小的功能吗?

我使用的语言主要是Javascript。由于我删除了所有小的明确命名的函数并将所有内容放入一个大循环中,因此我现在真的很难阅读。但是,我的老板喜欢这种方式。

一个例子是:

 // The way I would write it
if (isApplicationInProduction(headers)) {
  phoneNumber = headers.resourceId;
} else {
  phoneNumber = DEV_PHONE_NUMBER;
}

function isApplicationInProduction(headers) {
   return _.has(headers, 'resourceId');
}

// The way he would write it
// Take the right resourceId if application is in production
phoneNumber = headers.resourceId ? headers.resourceId : DEV_PHONE_NUMBER;
 


在我读过的书中,例如,注释被认为是无法编写干净的代码,因为如果您编写小的函数,它们就会过时,并且经常导致未更新的注释(修改代码而不是注释) 。但是,我要做的是删除注释,并使用注释的名称编写一个函数。

评论

请注意,约翰·卡马克(John Carmack)可能会同意您的老板。

phoneNumber = headers.resourceId?:DEV_PHONE_NUMBER;

确认您想就位,管理层告诉您如何进行工作,而不是需要解决的问题。

@rjmunro除非您真的喜欢您的工作,否则请记住,开发人员少于工作。因此引用马丁·福勒的话:“如果您不能更改组织,请更改组织!”我建议老板应该告诉老板,而老板告诉我如何编码。

永远不要使用isApplicationInProduction()函数!您必须具有测试,并且如果您的代码与生产时的行为有所不同,则测试无用。就像有意在生产中使用未经测试/未发现的代码一样:这没有任何意义。

#1 楼

首先以代码示例为例。您赞成:



 if (isApplicationInProduction(headers)) {
  phoneNumber = headers.resourceId;
} else {
  phoneNumber = DEV_PHONE_NUMBER;
}

function isApplicationInProduction(headers) {
   return _.has(headers, 'resourceId');
}
 


老板会写它是:

 // Take the right resourceId if application is in production
phoneNumber = headers.resourceId ? headers.resourceId : DEV_PHONE_NUMBER;
 


在我看来,两者都有问题。当我阅读您的代码时,我立即想到的是“您可以将if替换为三元表达式”。然后,我读了您老板的代码,然后想:“为什么他用注释替换了您的函数?”。

我建议最佳代码介于两者之间:

< pre class =“ lang-js prettyprint-override”> phoneNumber = isApplicationInProduction(headers) ? headers.resourceId : DEV_PHONE_NUMBER; function isApplicationInProduction(headers) { return _.has(headers, 'resourceId'); }

这两个优点都给了你:简化的测试表达式,并用可测试的代码替换注释。 >
关于老板对代码设计的看法:


写小函数很痛苦,因为它迫使您进入每个小函数以查看代码在做什么。


如果函数命名正确,则不是这种情况。
isApplicationInProduction是不言而喻的,无需检查代码即可查看其功能。实际上,事实恰恰相反:检查代码所显示的意图要比函数名称所揭示的要少(这就是为什么老板必须诉诸注释的原因)。主大循环即使主循环超过300行,读取速度也更快


扫描速度可能更快,但是要真正“读取”代码,您需要才能有效地执行它。使用小函数很容易,而使用100行的方法确实很难。
我不同意。如您的代码示例所示,名称小巧的函数可提高代码的可读性,并且应在例如对某个功能的“方式”不感兴趣的情况下使用。


不要使用注释的名称编写函数,而是将复杂的代码行(3-4行)放在注释的上方。这样,您可以直接修改失败的代码


我真的不明白这背后的原因,但前提是它确实很严重。我希望看到The Expert Beginner Twitter帐户以模仿形式编写的内容。注释有一个根本的缺陷:它们没有被编译/解释,因此不能进行单元测试。代码被修改,注释被留下来,您最终不知道哪一个是正确的。

编写自文档代码很困难,有时还需要补充文档(甚至以注释的形式) 。但是“鲍勃叔叔”认为注释是编码失败的观点经常成立。 。但最终,如果您不能说服他进行更改,则必须要么排队,要么找一个可以更好地编码的新老板。

评论


小功能易于单元测试

–莫格说要恢复莫妮卡
16年11月14日在14:35

Quoth @ ExpertBeginner1:“我讨厌在我们的代码中到处看到大量的小方法,所以从现在开始,所有方法的最小值都是15 LOC。

–格雷格·培根
16年11月14日下午17:00

“注释有一个根本的缺陷:它们不能被编译/解释,因此不能进行单元测试”在这里扮演魔鬼的提倡者,如果用“函数名”替换“注释”,这也是正确的。

–mattecapu
16年11月14日在17:01

@mattecapu,我将接受您的倡导,并再对您进行一遍。任何旧的垃圾开发人员都可以在注释中随意使用,以描述一段代码的作用。用功能名称简洁​​地描述那段代码需要熟练的沟通者。最好的开发人员是熟练的沟通者,因为编写代码主要是与其他开发人员进行沟通,而与编译器则是次要问题。好吧,一个好的开发人员将使用命名功能,并且不添加任何注释;糟糕的开发人员将他们的糟糕技能隐藏在使用评论的借口后面。

– David Arno
16年11月15日在15:24



@DavidArno所有函数都有前置条件和后置条件,问题是是否记录它们。如果我的函数采用的参数是以英尺为单位的距离,则必须以英尺而不是公里为单位。这是前提条件。

–Jørgen Fogh
16年11月17日在12:38

#2 楼

还有其他问题

这两个代码都不是好的,因为它们基本上都是通过调试测试用例来膨胀代码。如果您出于任何原因要测试更多内容怎么办?



 phoneNumber = DEV_PHONE_NUMBER_WHICH_CAUSED_PROBLEMS_FOR_CUSTOMERS;
 




 phoneNumber = DEV_PHONE_NUMBER_FROM_OTHER_COUNTRY;
 


是否要添加更多分支? />
一个重要的问题是,您基本上重复了部分代码,因此您实际上并未在测试真实的代码。您编写调试代码来测试调试代码,而不是生产代码。这就像部分地创建并行代码库。

您正在与老板争论如何更巧妙地编写不良代码。
相反,您应该解决代码本身的固有问题。 />
依赖注入

代码应该是这样的:

 phoneNumber = headers.resourceId;
 


这里没有分支,因为这里的逻辑没有任何分支。该程序应从标题中提取电话号码。

。如果要作为结果,请放入DEV_PHONE_NUMBER_FROM_OTHER_COUNTRY。一种方法是简单地为测试用例注入一个不同的headers.resourceId对象(很抱歉,如果这不是正确的代码,我的JavaScript技能会有点生锈): prettyprint-override“> headers

假设function foo(headers){ phoneNumber = headers.resourceId; } // Creating the test case foo({resourceId: DEV_PHONE_NUMBER_FROM_OTHER_COUNTRY}); 是从服务器收到的响应的一部分:理想情况下,您有一台完整的测试服务器,可以交付各种headers用于测试。这样,您可以按原样测试实际的生产代码,而不是一半的重复代码可能会或可能不会像生产代码那样工作。

评论


我确实考虑过用自己的答案来解决这个话题,但是感觉已经足够长了。因此对您+1 :)

– David Arno
16年11月10日在20:35

@DavidArno我打算将其添加为您的答案的注释,因为当我初次阅读该问题时,该问题仍然被锁定,令我惊讶的是它再次被打开,因此将其添加为答案。也许应该补充一点,有许多用于进行自动化测试的框架/工具。尤其是在JS中,似乎每天都有新的版本出现。但是可能很难将其卖给老板。

–空
16年11月10日在20:59

@DavidArno也许您应该将答案分解成较小的答案。 ;)

–磷虾
16年11月11日在11:05

@ user949300使用按位或不是明智的;)

–•uriousdannii
16年11月12日在12:57

@curiousdannii是的,注意到编辑太迟了...

–user949300
16年11月12日在15:29

#3 楼

对此没有“正确”或“错误”的答案。但是,基于36年的设计和开发软件系统的专业经验,我将提出自己的看法。...没有“自我记录代码”之类的东西。为什么?因为这种说法完全是主观的。
评论永远不会失败。失败就是没有注释就无法理解的代码。
一个代码块中300条不间断的代码行是维护的噩梦,极易出错。

直接说出您提供的示例……将isApplicationInProduction()放入其自己的例程中是比较明智​​的选择。今天,该测试仅是对“标头”的检查,可以在三元(?:)运算符中进行处理。明天,测试可能会更加复杂。另外,“ headers.resourceId”与应用程序的“生产状态”没有明确的关系;我认为需要对这种状态的测试与基础数据脱钩。子例程将执行此操作,而三元程序则不会。此外,为什么resourceId是对“生产中”的测试有用的注释。例行程序应该比“仅仅代码”更多地封装一个想法。我支持amon对phoneNumber = getPhoneNumber(headers)的建议,并补充说getPhoneNumber()应该对isApplicationInProduction()进行“生产状态”测试

评论


好评论之类的东西并不是失败。但是,注释几乎完全是它们应解释的代码,或者只是方法/类/等之前的空注释块。肯定是失败的。

– jpmc26
16-11-10在20:55



比起任何英语描述的功能以及处理和不处理的特殊情况,可能有更小的代码和更易于阅读的代码。此外,如果将某个函数提取到其自己的方法中,则读取该函数的人将不知道其调用者处理或未处理哪些极端情况,并且除非该函数的名称非常冗长,否则检查调用者的人可能不知道哪个角落案例由功能处理。

–超级猫
16年11月10日在22:19

评论绝不是本质上的失败。注释可能是失败的,当注释不正确时也是如此。可以在多个级别上检测到错误的代码,包括在黑盒模式下的错误行为。只有认识到描述了两个模型并且其中一个是不正确的,才可以由人类理解在白盒模式下检测到错误的注释。

– Timbo
16年11月11日,0:33

@Timbo您的意思是,“ ...至少其中之一是不正确的。” ;)

– jpmc26
16-11-11在2:41



@immibis如果没有注释就无法理解代码的功能,则代码可能不够清晰。注释的主要目的是阐明代码为什么要执行其所执行的操作。编码员向未来的维护者解释了他的设计。该代码永远无法提供这种解释,因此注释可以填补这些理解上的空白。

–格雷厄姆
16年11月11日在11:45

#4 楼


“实体不能成倍增加。”

— Occam的Razor


代码必须尽可能简单。错误喜欢隐藏在复杂性之间,因为它们很难发现。那么,什么使代码简单呢?

小单元(文件,函数,类)是个好主意。小型单位易于理解,因为您一次需要了解的东西较少。正常的人一次只能玩7个概念。但是大小不只是用代码行来衡量。我可以通过“遍历”代码(选择短的变量名,采用“聪明的”快捷方式,将尽可能多的代码粉碎到一行上)来编写尽可能少的代码,但是最终结果并不简单。试图理解这样的代码更像是逆向工程,而不是阅读。

缩短功能的一种方法是提取各种辅助函数。当它提取出一个独立的复杂性时,这可能是一个好主意。孤立地讲,这种复杂性比嵌入一个不相关的问题中要容易得多(管理和测试!)。但是每个函数调用都具有认知上的负担:我不仅要了解我当前代码中的代码,我还必须了解它如何与外部代码交互。我认为可以说您提取的函数比提取的函数引入了更多的复杂性。这就是老板所说的“小功能之苦”,因为它迫使您进入每个小功能以查看代码在做什么。”有时,即使无聊的功能只有几百行,它们也很容易理解。这往往发生在初始化和配置代码中,例如在没有拖放编辑器的情况下手动创建GUI时。您可以合理提取没有独立的复杂性。但是,如果格式易读且有注释,那么跟踪发生的事情确实并不困难。

还有许多其他复杂性指标:作用域中的变量数应尽可能小。这并不意味着我们应该避免使用变量。这意味着我们应该将每个变量限制在需要的最小范围内。如果我们从不更改变量所包含的值,变量也会变得更简单。它通过一段代码来测量独立路径的数量。每个条件,此数字都呈指数增长。每个条件循环都使路径数量加倍。有证据表明,得分超过10分太复杂了。这意味着可能得分为5的非常长的函数可能比得分为25的非常短而密集的函数更好。我们可以通过将控制流提取到单独的函数中来降低复杂度。

您的条件是可以完全提取出的一部分复杂度的示例:



 =“ lang-js prettyprint-override”> function bigFatFunction(...) {
  ...
  phoneNumber = getPhoneNumber(headers);
  ...
}

...

function getPhoneNumber(headers) {
  return headers.resourceId ? headers.resourceId : DEV_PHONE_NUMBER;
}
 


这仍然非常有用。我不确定这是否会大大降低复杂度,因为此条件不是非常有条件的。在生产中,它总是走同一条路。


复杂性永远不会消失。它只能随机播放。许多小事情比几件事大简单吗?这在很大程度上取决于情况。通常,有些组合感觉很不错。发现不同复杂度因素之间的折衷需要直觉和经验,还有一点运气。

知道如何编写非常小的函数和非常简单的函数是一项有用的技能,因为您无法做出选择不知道其他选择。盲目遵循规则或最佳实践,而不考虑如何将它们应用于当前情况,最好的结果是平均结果,而最糟糕的情况是程序设计。

,这就是我与您的老板不同意的地方。他的论点不是无效的,但是“清洁代码”书也不是错误的。遵循老板的指导可能更好,但是考虑到这些问题并试图找到更好的方法这一事实非常有希望。随着经验的积累,您会发现更容易找到适合您代码的分解因子。 Hoffa,它提供了使代码简单的高级视图。)

评论


我是将军,我很喜欢您的回复。我同意,但是对mcabes圈复杂度度量提出了质疑。从我所看到的来看,它并不能代表对复杂性的真实衡量。

–罗伯特·巴伦(Robert Baron)
17年7月12日在14:24

#5 楼

罗伯特·马丁(Robert Martin)的编程风格是两极分化的。您会发现很多程序员,甚至是经验丰富的程序员,他们都会找到许多借口,以说明为什么“太多”的拆分太多,以及为什么将功能保持更大一点是“更好的方法”。但是,大多数这些“争论”通常是不愿意改变旧习惯和学习新知识的一种表达。

不要听他们的话!

只要您可以通过将一段代码重构为具有表达性名称的单独函数来保存注释,就可以这样做-它很可能会改善您的代码。鲍勃·马丁(Bob Martin)在其干净的代码本中没有做那么多的事情,但是我过去看到的导致维护问题的绝大多数代码包含了太大的功能,而不是太小的功能。因此,您应该尝试尝试使用自描述名称编写较小的函数。

自动重构工具使提取方法变得容易,简单且安全。而且,请不要以为那些建议使用300行以上的代码编写功能的人会认真对待-这些人绝对没有资格告诉您应该如何编码。

评论


“不听他们的!”:鉴于OP的老板要求其停止拆分代码,OP应该避免您的建议。即使老板不愿改变他的旧习惯。还要注意,如先前的答案所强调,OP的代码和他老板的代码都写得不好,并且您(有意或无意)在答案中都没有提及。

– Arseni Mourzenko
16年10月10日在21:49

@ ArseniMourzenko:并不是我们每个人都必须在老板面前屈服。我希望OP的年龄足够大,可以知道他什么时候必须做正确的事情,或者什么时候必须做老板所说的。是的,我并没有故意进入示例的细节,已经有足够的其他答案在讨论这些细节了。

–布朗博士
16-11-10在22:03



@DocBrown同意。 300行对整个班级都是可疑的。 300行功能是淫秽的。

– JimmyJames
16年11月10日在22:27

我看过很多类,它们的长度超过300行,这是非常好的类。 Java非常冗长,如果没有那么多代码,您几乎无法在类中做任何有意义的事情。因此,“一个类中的代码行数”本身并不是一个有意义的指标,甚至超过了我们认为SLOC对于程序员生产力而言是有意义的指标。

–罗伯特·哈维(Robert Harvey)
16-11-10在23:36



另外,我已经看到鲍勃叔叔的明智建议被误解和滥用,以至于我怀疑它是否对经验丰富的程序员有用。

–罗伯特·哈维(Robert Harvey)
16-11-10在23:38



#6 楼

在您的情况下:您需要一个电话号码。显而易见,如何获得电话号码,然后编写明显的代码。还是不清楚如何获取电话号码,然后为它编写方法。

对于您而言,如何获取电话号码并不明显,因此您需要为其编写方法。实现不是很明显,但是这就是为什么要将其放入单独的方法中,因此只需要处理一次。注释将很有用,因为实现并不明显。

“ isApplicationInProduction”方法毫无意义。从您的getPhonenumber方法调用它并不会使实现变得更加明显,而只会使您难以确定正在发生的事情。

不要写小函数。编写具有明确目的并满足该明确目的的函数。

PS。我一点都不喜欢这种实现。它假定没有电话号码意味着它是开发版本。因此,如果生产中不存在电话号码,则您不仅不处理它,还替换一个随机电话号码。想象一下,您有10,000个客户,而17个没有电话号码,并且您在生产中遇到麻烦。应该直接检查您是在生产中还是在开发中,而不应从其他任何方面检查。

评论


“不要编写小的函数。编写具有明确定义的目的并满足那个明确定义的目的的函数。”那是分割代码的正确标准。如果一个函数执行太多(如多个)完全不同的函数,则将其拆分。单一责任原则是指导原则。

–罗伯特·布里斯托-约翰逊
16-11-20在5:45

#7 楼

看来您真正想要的是这样的:

phoneNumber = headers.resourceId || DEV_PHONE_NUMBER


对于任何读过它的人来说,这应该是不言自明的:如果可用,请将phoneNumber设置为resourceId,或者默认如果不是,请访问DEV_PHONE_NUMBER。你从那里跑。读取这些信息的标题没有任何意义。

评论


它的作用是自我解释的(有点猜测您使用的语言),但是发生的事情一点也不明显。显然,开发人员假定电话号码存储在生产版本中的“ resourceId”下,而资源ID在开发版本中不存在,并且他想在开发版本中使用DEV_PHONE_NUMBER,这意味着电话号码存储在一个奇怪的地方。标题,这意味着如果生产版本中缺少电话号码,则情况将变得非常糟糕。

– gnasher729
16年11月12日23:41

#8 楼

即使忽略了这两种实现都不是那么好的事实,我也会注意到,这至少在抽象出单次使用琐碎函数的水平上本质上是一个品味问题。

行数在大多数情况下不是有用的度量标准。

完全平凡的纯顺序代码(设置或类似内容)的300行(甚至3000行)是很少有问题(但最好是自动生成或作为数据表之类的东西),100行嵌套循环以及许多复杂的退出条件和数学运算,就像在高斯消除法或矩阵求逆法中可能会发现的那样,或者轻松跟随。

对我来说,除非声明该事物所需的代码量比构成实现的代码量小得多,否则我不会编写一个单一使用函数(除非我有理由说想要成为能够轻松地进行故障注入)。
很少有条件满足此要求。

现在我来自一个小型的核心嵌入式世界,在这里我们还必须考虑诸如堆栈深度和调用/返回开销之类的问题(这再次反对这里似乎提倡的微小功能) ,这可能会影响我的设计决策,但是如果我在代码审查中看到原始功能,则会得到旧版本的usenet标记。

口味是设计的难点,只有真正的经验才能带给我,我不确定它是否可以简化为关于函数长度的规则,甚至循环复杂性也有其局限性(有时是只是很复杂,但是您可以解决它们。)
这并不是说干净的代码不会讨论某些好东西,它确实可以,并且应该考虑这些事情,但是应该使用本地定制以及现有代码库的功能重量也是如此。

在我看来,这个特定的示例似乎是琐碎的细节,我会更关注更高层次的内容,因为这对轻松理解和调试系统的能力至关重要。

评论


我完全同意-考虑到将它包装在一个函数中,这将需要非常复杂的单行代码……我当然不会包装三元/默认值行。我已经包装了一个内衬,但是通常这是10个管道来解析某些内容的shell脚本,如果不运行它,代码将变得难以理解。

–临时狼
16年11月12日,0:11

#9 楼

让我直言不讳:在我看来,您的环境(语言/框架/类设计等)并不真正适合“干净”的代码。您在几行代码中混合了所有可能的东西,而这些代码本来应该并不太紧密。知道resourceId==undef意味着您不在生产中,在非生产系统中使用的是默认电话号码,resourceId保存在某些“标题”中,那么单个功能有哪些业务?我假设headers是HTTP标头,因此您甚至还可以将最终用户的决定权留在哪个环境?

将其中的一部分分解为函数并不能帮助您解决潜在的问题。

需要寻找一些关键字: />解耦
内聚力
依赖注入

通过零代码行移动,使用和使用代码,您可以用零行代码实现所需的功能现代框架(对于您的环境/编程语言可能存在,也可能不存在)。

根据您的描述(“主功能中的300行代码”),甚至单词“功能”(而不是方法)让我假设您要实现的目标毫无意义。在那种老式的编程环境中(即基本的命令式编程,结构很少,当然没有有意义的类,没有像MVC这样的类框架模式),做任何事情实际上没有多大意义。没有根本性的改变,您将永远无法摆脱困境。至少您的老板似乎允许您创建函数来避免代码重复,这是一个很好的第一步!

我知道您所描述的代码类型以及程序员的类型都很好。坦率地说,如果是同事,我的建议会有所不同。但是,因为它是您的老板,所以您为此奋斗是没有用的。如果您仅部分地“做您的事”,而您的老板(可能还有其他人)像以前那样继续工作,不仅您的老板会否决您,而且您添加代码的确会导致更糟糕的代码。如果您适应他们的编程风格(当然,仅在使用此特定代码库时),并尝试在这种情况下尽力而为,那么对最终结果可能更好。

评论


我100%同意这里有一些隐式组件应该分开,但是在不了解更多语言/框架的情况下,很难知道OO方法是否有意义。从纯粹的功能(例如Haskell)到纯粹的命令性(例如C),解耦和“单一责任原则”在任何语言中都是重要的。如果老板允许,第一步是将主函数转换为调度程序函数( (例如大纲或目录),它们将以声明方式(描述策略,而不是算法)阅读,并将工作分配给其他功能。

– David Leppik
16年11月11日在18:44

JavaScript是原型,具有一流的功能。它本质上是OO,但不是经典意义上的,因此您的假设可能不正确。在YouTube上观看克罗福德影片的提示时间...

– Kevin_Kinsey
16-11-15在19:59

#10 楼

不要将所有内容都放在一个大循环中,但也不要经常这样做:

 function isApplicationInProduction(headers) {
   return _.has(headers, 'resourceId');
}
 


大循环的问题在于,当跨过许多屏幕时,很难看到其整体结构。因此,请尝试取出大块,最好是承担单个责任并可以重用的大块。太过分了。如果您不打算重复使用上述功能,则会损害代码的可读性和可维护性。要深入研究细节,您必须跳到函数而不是能够内联读取细节,并且函数调用所占用的空间几乎不会少于细节本身的空间。在做得太多的方法与做得太少的方法之间找到平衡。除非要从一个以上的地方调用它,否则我绝不会分解上面提到的一个小函数,即使那样,我也要三思而后行,因为就引入新逻辑以及这样几乎不能保证拥有它自己的存在。

评论


我知道,一个布尔布尔值的内衬很容易阅读,但仅靠它真的只能解释“正在发生什么”。我仍然编写包装简单三元表达式的函数,因为该函数的名称有助于解释“为什么”执行条件检查的原因。当新手(或您自己在6个月内)需要了解业务逻辑时,这特别有用。

– AJX。
16-11-21在13:48

#11 楼

“干净”是编写代码的目标之一。这不是唯一的目标。另一个值得追求的目标是同居。非正式地讲,共地域性意味着试图理解您的代码的人们无需四处走动即可查看您在做什么。使用命名函数代替三元表达式似乎是一件好事,但是根据您拥有的此类函数的数量以及它们的位置,这种做法可能会变得很麻烦。我无法告诉您您是否已经越过界限,只能说如果人们在抱怨,您应该倾听,特别是如果这些人对您的就业状况有发言权的话。

评论


“……除了要说人们在抱怨外,你应该倾听,尤其是那些人对你的就业状况有发言权的时候。” IMO这真是个坏建议。除非您是一个严重贫穷的开发人员,需要欣赏您能获得的任何工作,否则请始终遵循“如果您不能换工作,就换工作”的原则。永远不要迷恋一家公司;他们比您更需要他们,因此,如果他们没有提供您想要的东西,那就走到更好的地方。

– David Arno
16年11月11日在19:07

在我的职业生涯中,我已经走了一些路。我认为我从未有过与老板就如何编码达成100%的对等的工作。我们是具有自己的背景和哲学的人。所以我个人不会因为有一些我不喜欢的编码标准而辞职。 (经理们喜欢的弯弯曲曲的命名约定似乎与我编写自己的设备时编写代码的方式特别相反。)但是,您是对的,一个体面的程序员不必为继续工作而担心太多。

–user1172763
16年11月11日在20:59



#12 楼

通常,使用小功能是一个好习惯。但是理想情况下,我相信引入函数应该要么将大的逻辑块分开,要么通过将其DRY出来来减小代码的整体大小。您提供的示例都使代码更长,并且需要更多的时间供开发人员阅读,而简短的选择并不能说明"resourceId"值仅在生产中存在。像这样的简单事情在尝试使用它时既容易忘记又会令人困惑,特别是如果您还不熟悉代码库的话。

我不会说您绝对应该使用三元,与我一起工作的某些人更喜欢稍长的if () {...} else {...},这主要是个人选择。我倾向于使用“一行完成一件事情”,但是我基本上会坚持使用代码库通常使用的任何方法。

使用三进制时,如果逻辑检查使行太长或太复杂,则考虑创建一个命名良好的变量来保存值。

 // NOTE "resourceId" not present in dev build, use test data
let isProduction = 'resourceId' in headers;
let phoneNumber = isProduction ? headers.resourceId : DEV_PHONE_NUMBER;
 


我也会喜欢说如果代码库扩展到300个行函数,那么它确实需要细分。但我建议您使用稍宽的笔触。

#13 楼

您提供的代码示例,您的老板是正确的。在这种情况下,最好使用一条清晰的线条。

通常,将复杂的逻辑拆分成较小的部分对于提高可读性,代码维护以及子类具有不同行为(即使只是很小)的可能性也更好。

不要忽略以下缺点:函数开销,晦涩(函数不执行注释和函数名所暗示的意思),复杂的意大利面条逻辑,潜在的死函数(一次创建是出于特定目的不再被称为)。

评论


“函数开销”:由编译器决定。 “模糊不清”:OP尚未表明它是检查该属性的唯一还是最佳方法;您也不能肯定。 《复杂的意大利面条逻辑》:在哪里? “失效功能的潜力”:这种失效代码分析是垂手可得的成果,而缺乏它的开发工具链还不成熟。

–类风湿
16-11-12在12:26



答案更多地集中在优点上,我也只想指出缺点。调用sum(a,b)之类的函数总是比“ a + b”更昂贵(除非该函数由编译器内联)。其余的缺点表明,过于复杂会导致其自身的一系列问题。错误的代码就是错误的代码,仅仅因为它被分解成较小的字节(或保存在300行循环中)并不意味着它更容易被吞噬。

– Phil M
16-11-14在21:38



#14 楼

我可以想到至少两个支持长函数的参数:


这意味着每行都有很多上下文。一种形式化的方式:绘制代码的控制流程图。在函数入口和函数出口之间的顶点(〜=线)处,您知道所有传入的边。函数越长,此类顶点越多。


许多小函数意味着存在一个更大,更复杂的调用图。在随机函数中选择一条随机行,然后回答问题“在哪条上下文中执行此行?”调用图越大,越复杂,就越难,因为您必须查看该图中的更多顶点。心神。在选择彼此之间时,请使用您的经验。
注意:我并不是说您的老板是正确的,只是他的观点可能并不完全没有价值。

我我认为我认为好的优化参数不是函数长度。我认为从下面的角度来看,更有用的一种想法:在其他条件相同的情况下,最好能够从代码中读出业务逻辑和实现的高级描述。 (如果可以找到相关的代码位,则始终可以阅读底层的实现细节。)

评论David Arno的答案:


写小函数之所以痛苦,是因为它迫使您进入每个小函数以查看代码在做什么。 isApplicationInProduction是不言而喻的,不需要检查代码以查看其作用。实际上,事实恰恰相反:检查代码所显示的意图要比函数名称所揭示的要少(这就是您的老板必须诉诸注释的原因)。

该名称清楚表明了返回值的含义,但并未说明执行代码的效果(=代码的作用)。名称(仅)传达了有关意图的信息,代码传达了有关行为的信息(有时可以从中推断出意图的各个部分)。
有时您想要一个,有时是另一个,所以这种观察并不能创建一个-通用的有效决策规则。


将所有内容放入主大循环中,即使主循环超过300行,读取速度也更快扫描可能更快,但要真正“读取”代码,您需要能够在脑海中有效执行它。使用小的函数很容易,使用100行的方法确实很难。

我同意必须在脑海中执行它。如果您在一个大型函数中有500行功能,而在许多小型函数中却有500行功能,那么我不清楚为什么这样做会更容易。您想知道效果A是发生在效果B之前还是之后。在大功能情况下,使用Page Up / Down查找上下两行,然后比较行号。在多小函数的情况下,您必须记住效果在调用树中的何处发生,并且如果您忘记了,则必须花费大量的时间来重新发现此树的结构。
遍历时支持功能的调用树,您还面临着确定何时从业务逻辑转到实现细节的挑战。我声称没有证据*,调用图越简单,区分起来就越容易。有优点也有缺点。


如果必须复制代码,则仅编写小函数

我不同意。如您的代码示例所示,名称小巧的函数可提高代码的可读性,并且应在[例如]您对某项功能的“方式”而不是“什么”感兴趣时使用。

您是否对“如何”或“什么”感兴趣,这是您正在阅读代码的目的的功能(例如,获得一般性想法或跟踪错误)。编写程序时,您无法读取代码的目的,并且您很有可能出于不同的目的而读取代码。不同的决定会针对不同的目的进行优化。
这就是老板最不赞同的观点的一部分。


不要用注释的名称,在上面加上注释的复杂代码行(3-4行)。这样,您可以直接修改失败的代码

,假设它确实很严重,我真的不明白这背后的原因。 [...]注释有一个根本缺陷:注释未经编译/解释,因此无法进行单元测试。代码被修改,注释被留下来,您最终不知道哪一个是正确的。另外,由于多个调用站点可能会按名称调用给定的功能,因此更改名称有时会更加艰巨且容易出错。评论没有这个问题。但是,这有点投机。要真正解决这个问题,可能需要有关程序员是否更可能更新误导性注释和误导性名称的数据,而我没有。

#15 楼

您的老板显然从未听说过单元测试。您可以为较小的函数编写单元测试,从而分别测试每个函数的正确性。您不能使用做很多事情的大型循环来完成此任务,因为最终将得到的信息是,最终结果是错误的,但不是错误在哪里?如果分别测试所有功能,则会发现该功能做错了。
如果功能真的很小,您甚至可以从理论上证明它们的正确性,但是随着复杂度的提高,使用大循环将几乎不可能

编写小函数很痛苦,因为它迫使您进入每个小函数以查看代码的作用。

如果选择了名称正确地,光是名字就可以告诉他该功能的作用。 countNumberOfCustomers()肯定会得到香蕉的当前市场价格,这是个疯狂的猜测,我会说“这取决于客户的数量”。当您突出显示函数调用而不必打开函数时,每个不错的现代IDE都会在某处(弹出窗口,某些编辑器区域)显示文档注释。这样,您还可以得到参数含义的解释。

即使主循环超过300行,也可以将所有内容放入主大循环中。

CustomerList clist = fetchCustomerList(MainServer);

是一行代码,很容易阅读,也很容易看到它的作用。 fetchCustomerList()但是可能是20行代码。因此,将这段代码拉到那里意味着我用20行替换了一行。许多其他功能也会发生相同的情况。如何更快地阅读?

只比编写一行要快20行?在这里给出。

不要使用注释名称编写函数,而是将复杂的代码行(3-4行)放在注释上方;同样,您可以直接修改失败的代码

代码应始终代表自己。注释只能说明代码无法解释的内容。如果您的老板对函数使用了无用的名称,难怪他永远不知道函数做什么,并且总是必须查找其代码。因此,他试图通过在您身上强制使用仅因该代码样式而存在的代码样式来解决问题。
由于您我的缘故,我建议您让老板注意这一点(我在重要部分的开头加了书签):
https://youtu.be/7EmboKQH8lM?t=2435
一直观察到55:25。老板对你的要求是“不礼貌”。如您所愿,编写代码是“礼貌的”,因为它允许读者提早退出。这样可以节省读者很多时间,并使您更容易理解代码。也许一旦他看了那个演讲,或者至少我选了15分钟,他就会明白这一点。

评论


测试太细粒度是浪费的精力,并且不会测试重要的事情,因此进行任何更改(无论是有益的)都变得更加困难。还是您说所有这些微功能无论如何都应该是接口的基本部分,而不是实现细节?

–重复数据删除器
20年5月2日,11:13

@Deduplicator为每行代码创建一个自己的函数也没有意义,永远不要相反。通常,用函数调用交换一行代码不会使事情更容易阅读。通常,函数调用应隐藏执行有意义的操作的多行代码,这些操作可以视为“原子”操作。我只是反对将所有内容放入一个大循环中更具可读性,并且几乎无法测试。在某些情况下,仅将一行代码放入自己的函数中是合理的。仍在寻找一个好的样本来改善我的答案。

–梅基
20-5-2在11:41

@Deduplicator我也建议您观看我现在链接到我的回复的视频。请注意,这是第1部分,整个讨论分为5部分。我认为在第3部分中,他谈到了测试。只要一个函数可以有意义地分为两个函数,那么一个函数就不会做一件事,但是一个函数应该总是只做一件事,而它的测试应该总是只测试一件事。对于所有体面的开发人员来说,整个演讲是必须看到的,相信我,它将改变您对在该领域所做的一切的看法。

–梅基
20年7月8日在13:33

似乎我有点过分批判。是的,函数只能做一件事情,而不能做更大。

–重复数据删除器
20年7月8日在14:07

#16 楼

具有讽刺意味的是,不仅这两种方法都存在问题,而且它们共享存在问题的理由。他们俩都可以遵循这样的建议:以声明性的方式编写代码,而不必以命令的方式编写代码。
例如,三元表达式是声明性的,因为它们只说phoneNumber = a ternary expression,而在每个分支中分配给phoneNumber的if / else则必须我们分支。是的,这两种方法在逻辑上是等效的,但是声明性代码的优点是分块,人类阅读,编写和编辑代码非常需要。分块是指人们用其他概念来定义一个概念,这样他们可以减少必须同时考虑的概念数量,从而改善了我们对7个负2个概念的处理能力。声明式包装命令式逻辑的唯一方法。函数之所以在编程中如此有用的原因之一是,调用函数允许用单个声明行来总结任意数量的命令式逻辑。实际上,三元表达式正在调用“函数”,只是将函数拆分为两个运算符。在替代的历史记录或另一种语言中,语法为类似函数ifthenelse(headers.resourceId, headers.resourceId, DEV_PHONE_NUMBER)。语言设计者为什么曾经给我们提供要么基于操作员的替代方案?因为这是一个有用的声明。如果愿意,可以称其为语法糖,但其动机远不止于此。
但是使布尔值检查函数调用的好处是,您可以声明isApplicationInProduction(headers)而不是强制性地编写headers.resourceId_.has(headers, 'resourceId')或其他实现细节得到答案。你不在乎它是如何完成的。您只在乎,如果应用程序正在生产中,则使用资源ID,否则使用开发人员的电话号码。这表明您和老板的代码之间存在折衷,
function isApplicationInProduction(headers) {
   return headers.resourceId;
}

phoneNumber = isApplicationInProduction(headers) ? headers.resourceId : DEV_PHONE_NUMBER;

根据语言的不同,我们可以使用空值运算符在一行中完全没有任何功能来很好地做到这一点:
phoneNumber = headers.resourceId ?? DEV_PHONE_NUMBER;

这是一个很漂亮的运算符。为什么要发明它?好吧,这是分块的另一个示例:a ?? b声明了强制性的结果,即检查a是否为null,如果不是,则返回a,否则返回b。注意在这些示例中,为什么我说要多声明性地而不是强制性地写,而不是声明性地而不是命令性地:区别不仅是频谱,而且是嵌套的。
300行函数从这个观点。这样的功能有什么作用?好吧,那个那个那个那个那个那个。好,为这些活动命名。毫无疑问,在这样的功能中,您已经看到了“段落”,以注释为首,以空白行分隔。这些段落应该是功能;如果将它们设为函数,则用一个函数调用每次替换一个段落都会用声明性描述替换命令式胆量。
这样的想法是,声明在幕后执行命令式工作的内容远比这样的示例大得多。每次从库中导入语言时,都会发生这种情况,让一种语言手动处理其他语言,这也是各种OOP原理(例如封装和Demeter律)的原因。我们要求类的代码调用实例只能访问实例类在其合同中所保证的内容,不是因为担心a.b.c合法性较不安全或类似的事情,而是因为类很容易改变他们的工作方式完成。回到我建议的不带空字符运算符的代码,它允许我们检查应用程序是否在生产中与我们检查的事实脱钩。当然,与另一种方法相比,所有方法所做的都是重新排列代码,将逻辑放在一行而不是另一行。但这就是您的重构。
您会惊讶于多少干净的代码归结为使代码更具声明性-在实践中,如此递归。一旦您注意到它,您将在任何地方看到它。没错,只要有for循环,您就会看到命令性代码。但您还会在其他方法中看到声明性代码。与上面的示例(函数以及三元和空值运算符)完全不同,如果您曾经在C#中使用过SIMD计算,映射或过滤器,Python理解或LINQ,那么您已经在使用声明式代码。