现在可能是停止推荐Clean Code的时候了

2020-06-29 12:00:29

我们可能永远不可能得出好代码或干净代码的经验定义,这意味着任何一个人对另一个人对另一个人关于干净代码的看法必然是高度主观的。我不能从你的角度评论罗伯特·C·马丁(Robert C.Martin)2008年出版的书“清洁代码”(Clean Code),只能从我的角度。

也就是说,我对“干净代码”的主要问题是,书中的很多示例代码都非常糟糕。

在第三章,函数中,马丁给出了写好函数的各种建议。也许本章中最强烈的一条建议是,函数不应该混合不同层次的抽象;它们不应该同时执行高级和低级任务,因为这会混淆和混淆函数的职责。这一章还有其他有效的内容:马丁说函数名称应该是描述性的,一致的,应该是动词短语,并且应该仔细选择。他说,函数应该只做一件事,而且要做好。他说函数不应该有副作用(他提供了一个非常好的例子),而且应该避免输出参数而支持返回值。他说,函数通常应该要么是执行某些操作的命令,要么是回答某些问题的查询,但不能两者兼而有之。他说干的。这都是很好的建议,尽管有点不温不火,而且是入门级的。

但在这一章中混杂着更多有问题的断言。Martin说布尔标志参数是不好的做法,我同意这一点,因为源代码中朴素的真或假与显式的is_Suite或is_not_Suite相比是不透明和不清楚的……。但马丁的推理更确切地说,布尔参数意味着一个函数做了不止一件事情,而这是它不应该做的。

Martin说,应该可以从上到下读取单个源文件作为叙述性文件,每个函数的抽象级别随着我们的阅读而下降,每个函数向下调用其他函数。这远不是普遍相关的。许多源文件,我甚至可以说大多数源文件,都不能以这种方式整齐地分层。即使对于那些可以实现的,IDE也可以让我们轻松地从函数调用跳到函数实现,再跳回来,就像我们浏览网站一样。除了一本书,我们还会从上到下阅读代码吗?嗯,也许我们中的一些人是这样想的。

然后就变得很奇怪了。Martin说,函数不应该大到足以容纳嵌套结构(条件句和循环);它们不应该缩进到两个以上的级别。他说,块应该有一行长,可能由单个函数调用组成。他说,理想的函数没有参数(但仍然没有副作用??),而有三个参数的函数令人困惑,很难测试。最奇怪的是,Martin断言理想的函数是两到四行代码长度。这条建议实际上放在本章的开头。这是第一条也是最重要的规则:

函数的第一条规则是它们应该很小。函数的第二条规则是它们应该比那个小。这不是我可以证明的断言。我不能提供任何关于证明非常小的函数更好的研究的参考。我可以告诉你的是,在近40年的时间里,我编写了各种不同大小的函数。我已经写了好几篇3000行的令人厌恶的文章。我已经编写了100到300行范围内的大量函数。我已经编写了20到30行的函数。这段经历告诉我,通过长期的试验和错误,函数应该非常小。

当肯特向我展示代码时,我被所有函数是如此之小所震惊。我习惯了Swing程序中占用数英里垂直空间的函数。这个程序中的每个函数都只有两行、三行或四行长。每一个都是显而易见的。每个人都讲了一个故事。每一个都以令人信服的顺序引导你进入下一个。这就是您的函数应该有多短!

所有这些建议在第3章末尾的以下源代码清单中达到高潮。这个示例代码是Martin对源自开源测试工具FitNesse的Java类的首选重构。

package fitnesse.html;import fitnesse.responders.run.SuiteResponder;import fitnesse.wik.*;public class SetupTeardownIncluder{private PageData;private Boolean isSuite;private Wikipage testPage;private StringBuffer newPageContent;Private PageCrawler pageCrawler;public static string ender(PageData PageData)抛出异常{RETURN(PageData,False);}public。pageCrawler=testPage.getPageCrawler();newPageContent=new StringBuffer();}私有字符串呈现(Boolean IsSuite)抛出异常{this.isSuite=isSuite;if(isTestPage())include deSetupAndTeardownPages();return pageData.getHtml();}私有布尔isTestPage()抛出异常{return pageData.hasAttribute(。}private void include deSetupPages()引发异常{if(IsSuite)include deSuiteSetupPage();include deSetupPage();}private void include SuiteSetupPage()引发异常{include(SuiteResponder.SUITE_SETUP_NAME,";-setup";);}Private void include deSetupPage()引发异常{include(";setup";,";-setup";)。}private void include TeardownPages()引发异常{include deTeardownPage();if(IsSuite)include deSuiteTeardownPage();}private void include TeardownPage()引发异常{include(";teardown";,";-teardown";);}Private void include SuiteTeardownPage()引发异常{include(SuiteResponder.SUITE_teardown。}private void include(string pagename,string arg)引发异常{Wikipage inheritedPage=findInheritedPage(Pagename);if(heritedPage!=null){string pagePathName=getPathNameForPage(HeriitedPage);buildIncludeDirective(pagePathName,arg);}}私有Wikipage findInheritedPage(String Pagename)引发异常{return PageCrawlerImp。}private void buildIncludeDirective(string pagePathName,string arg){newPageContent.append(";\n!include";).append(Arg).append(";.";).append(PagePathName).append(";\n";);}}。

我再说一遍:这是马丁自己的代码,是按照他的个人标准写的。这就是理想,作为学习的榜样呈现在我们面前。

在这个阶段,我要承认我的Java技能已经过时和生疏了,几乎和这本2008年的书一样过时和生疏。但可以肯定的是,即使在2008年,这个代码也是难以辨认的垃圾?

我们有两个公共静态方法,一个私有构造函数和十五个私有方法。在15个私有方法中,有整整13个方法要么有副作用(它们修改没有作为参数传递给它们的变量,比如buildIncludeDirective,它对newPageContent有副作用),要么调用其他有副作用的方法(比如include,它调用buildIncludeDirective)。只有TestPage和findInheritedPage看起来没有副作用。它们仍然使用没有传递给它们的变量(分别是PageData和testPage),但是它们似乎是以无副作用的方式这样做的。

此时,您可能会得出结论,马丁对副作用的定义可能不包括我们刚刚调用其方法的对象的成员变量。如果我们采用这个定义,那么五个成员变量PageData、isSuite、testPage、newPageContent和pageCrawler将被隐式传递给每个私有方法调用,并且它们被认为是公平的;任何私有方法都可以自由地对这些变量中的任何一个执行它喜欢的任何操作。

副作用是谎言。您的函数承诺做一件事,但它也做其他隐藏的事情。有时它会对自己类的变量进行意外更改。有时它会使它们传递给传递给函数或系统全局变量的参数。在任何一种情况下,它们都是迂回的、破坏性的不信任,常常导致奇怪的时间耦合和顺序依赖。

我喜欢这个定义!我同意这个定义!

那么,为什么马丁自己的代码,干净的代码,除了这个什么也做不了呢?很难弄清楚这些代码到底做了什么,因为所有这些极其微小的方法几乎什么都不做,只通过副作用来工作。让我们只看一个私有方法。

私有字符串呈现(Boolean IsSuite)引发异常{this.isSuite=isSuite;if(isTestPage())include deSetupAndTeardownPages();return pageData.getHtml();}。

为什么此方法会有设置this.isSuite的值的副作用?为什么不直接将isSuite作为布尔值传递给后面的方法调用呢?为什么在花费三行代码不对PageData执行任何操作之后返回pageData.getHtml()?我们可能会做出一个有根据的猜测,即包括SetupAndTeardownPages对PageData有副作用,但是,然后呢?除非我们看一看,否则我们不能知道哪种情况。这对其他成员变量有什么其他副作用?不确定性变得如此之大,以至于我们突然不得不怀疑isTestPage是否也会有副作用。(那这个凹痕是怎么回事?你的吊带呢?)。

马丁在这一章中指出,如果您可以从一个函数中提取另一个函数,并且该函数的名称不仅仅是对其实现的重述,那么将一个函数拆分成更小的函数是有意义的。但随后他给了我们:

侧边栏:这段代码有一些不好的方面,这不是Martin的错。这是对先前存在的一段代码的重构,该代码可能不是最初由Martin编写的。这段代码已经有了一个可疑的API和可疑的行为,这两者都保留在重构过程中。首先,类名SetupTeardownIncluder很糟糕。它至少是一个名词短语,就像所有的类名一样,但它是一个经典的扼杀名词动词短语。当您在严格面向对象的代码中工作时,您总是会得到这样的类名,在这种代码中,所有东西都必须是一个类,但有时您真正需要的只是一个简单的该死的函数。

其次,PageData的内容会被销毁。与成员变量(isSuite、testPage、newPageContent和pageCrawler)不同,PageData实际上不是我们可以修改的。它最初是由外部调用方传递给顶级公共呈现方法的。Render方法执行大量工作,最终返回一个HTML字符串。然而,在这项工作中,作为副作用,PageData被破坏性地修改(请参阅updatePageContent)。当然,最好用我们想要的修改创建一个全新的PageData对象,而保持原始对象不变?如果调用者试图将PageData用于其他用途,他们可能会对其内容发生的情况感到非常惊讶。但这是马丁重构之前原始代码的行为方式。他保留了这种行为。不过,他已经非常有效地掩埋了它。

差不多吧,是的。“干净的代码”将强有力的、永恒的建议和高度可疑或过时或两者兼而有之的建议结合在一起,令人不知所措。这本书几乎完全集中在面向对象的代码上,并告诫Solid的优点,而排除了其他编程范例。它专注于Java代码,排除了其他编程语言,甚至其他面向对象的编程语言。有一章是关于嗅觉和启发式的,只不过是代码中需要注意的相当合理的符号列表而已。但有多个章节介绍了什么是基本的填充物,重点放在重构Java代码的费力的工作示例上;有整整一章研究了JUnit的内部结构。(这本书是2008年的,所以你可以想象它现在有多重要。)。这本书对Java的总体使用是非常过时的。这类事情是不可避免的-编程书籍的年代传说中很糟糕-但即使是在那个时候,提供的代码也是糟糕的。

有一章是关于单元测试的。这一章有很多很好的-如果是基本的-内容,关于单元测试应该如何快速、独立和可重复的,关于单元测试如何实现更有信心的源代码重构,关于单元测试应该如何与测试中的代码一样庞大,但严格来说更容易阅读和理解。但随后他向我们展示了一个单元测试,其中包含了他所说的太多细节:

@Test public void turOnLoTempAlarmAtThreadhold()抛出异常{hw.setTemp(Way_Too_COLD);Controler.tic();assertTrue(hw.heaterState());assertTrue(hw.blowerState());assertFalse(hw.coolState());assertFalse(hw.hiTempAlarm());assertTrue(hw.loTempAlarm());}。

@Test public void turOnLoTempAlarmAtThreshold()抛出异常{wayTooLD();assertEquals(“HBchL”,hw.getState());}。

这是为您的测试发明新的特定于领域的测试语言的价值的整体课程的一部分。我被这个断言搞糊涂了。我会使用完全相同的代码来演示完全相反的课程!不要这样做!

第一定律,除非您编写了失败的单元测试,否则不能编写生产代码。

第二定律你写的单元测试不能超过失败的程度,不编译就是失败。

第三定律您编写的产品代码不能超过通过当前失败测试所需的数量。

这三条定律将你锁定在一个大约30秒长的周期内。测试和生产代码一起编写,测试仅比生产代码提前几秒钟。

.但他没有提到这样一个事实,即在大多数情况下,将编程任务分解成微小的32秒代码是非常耗时的,而且经常明显是无用的,而且经常是不可能的。

其中有一章“对象和数据结构”,其中他提供了一个数据结构的示例:

公共接口点{Double getX();Double Gty();void setCartesian(Double x,Double y);Double getR();Double getTheta();void setPolar(Double r,Double theta);}。

这两个示例显示了对象和数据结构之间的区别。对象将其数据隐藏在抽象之后,并公开对该数据进行操作的函数。数据结构公开它们的数据,并且没有任何有意义的功能。回去再读一遍。请注意这两个定义的互补性。它们实际上是截然相反的。这种差异可能看起来微不足道,但却有着深远的影响。

是的,你对这一点的理解是正确的。马丁对数据结构的定义与其他人使用的定义不一致!书中根本没有关于使用我们大多数人认为是数据结构的干净编码的内容。本章比你预期的要短得多,包含的有用信息也很少。

我不打算重复我剩下的所有笔记。我拿了很多,把我认为这本书有问题的东西都说出来会花很长时间。我将以另一段令人震惊的示例代码结束。这来自第8章,素数生成器:

Package writatePrimes;import java.util.ArrayList;public class PrimeGenerator{private static int[]primes;private static ArrayList<;Integer>;multiesOfPrimeFators;Protected static int[]Generate(Int N){primes=new int[n];multiesOfPrimeFators=new ArrayList<;Integer>;();set2AsFirstPrime();check。For(int Candidate=3;primeIndex<;primes.length;Candidate+=2){if(isPrime(候选项))Primes[primeIndex++]=Candidate;}}私有静态布尔isPrime(int候选项){if(isLeastRelevantMultipleOfNextLargerPrimeFactor(candidate)){MultiesOfPrimeFactors.add(候选项);Return False;}Return isNotMultipleOfAnyPreviousPrimeFactor(candidate);}私有静态布尔候选){int nextLargerPrimefactor=Primes[MultiesOfPrimeFactors.isLeastRelevantMultipleOfNextLargerPrimeFactor(int()];int。}私有静态布尔isNotMultipleOfAnyPreviousPrimefactor(int候选){For(int n=1;n<;multiesOfPrimeFactors.size();n++){if(isMultipleOfNthPrimefactor(Candidate,n))返回FALSE;}返回TRUE;}私有静态布尔isMultipleOfNthPrimefactor(int候选,int n){Return==smallestOddNthMultipleNotLessThanCandidate(candidate,n);}私有静态smallestOddNthMultipleNotLessThanCandidate(int候选,multiesOfPrimeFactors.set(n,Multiple);返回Multiple;}}。

这该死的代码是什么?这些方法名称是什么?set2AsFirstPrime?SmallestOddNthMultipleNotLessthan Candidate?这是否意味着是干净的代码?这是不是意味着一种清晰、智能的筛选质数的方式?

如果这就是这位程序员产生的代码质量-在理想的环境下,在他自己的闲暇时间,没有任何实际生产软件开发的压力-那么你为什么要关注他书的其余部分呢?还是他的其他书?

我写这篇文章是因为我一直看到人们推荐Clean Code。我觉得有必要提出反对建议。

我最初阅读“清洁代码”是作为在工作中组织的阅读小组的一部分。我们每周读大约一章,连续读了十三周。

现在,你不会想要一个阅读小组在每节课结束时达成一致意见。你想要这本书引起读者的某种反应,一些额外的回应。我想,在某种程度上,这意味着这本书要么必须说出你不同意的东西,要么不能说出你认为它应该说的一切。在此基础上,“干净的代码”是可以的。我们进行了很好的讨论。我们能够使用单独的章节作为更深入讨论实际现代实践的起点。我们谈了很多书中没有涉及的内容。我们在书中有很多不同意的地方。

我可以推荐这本书吗?不是的。即使作为初学者的文本,即使上面有所有的警告?不是的。在2008年,我会推荐这本书吗?我现在会推荐它作为历史文物,教育快照吗?

..