"编写整洁的代码是你必须做到的,才能称自己为专业人士。没有合理的借口不做最好的努力。" 《代码整洁之道》
在这篇文章中,我想谈谈我是如何编写代码的。我将我的方法称为"脏编码",因为我经常违背《代码整洁之道》的建议,这是一种流行的编码方法。
现在,我并不真的认为我的代码有那么脏:有些地方有点粗糙,但大部分情况下我对它很满意,发现它足够容易维护,质量也合理。
我也不是试图通过这篇文章说服你采用脏编码。相反,我想展示以这种方式编写相当成功的软件是可能的,并希望能为软件方法论讨论提供一些平衡。
我已经编程一段时间了,见过许多不同的软件开发方法都有效。有些人喜欢面向对象编程(我喜欢),其他非常聪明的人讨厌它。有些人喜欢动态语言的表达能力,其他人讨厌它。有些人严格遵循测试驱动开发成功交付产品,其他人只在项目结束时添加一些端到端测试,许多人则介于这些极端之间。
我见过使用所有这些不同方法的项目成功交付和维护软件。
所以,再次强调,我的目标不是说服你我的编码方式是唯一的方式,而是向你展示(特别是容易被"整洁代码"等术语吓到的年轻开发者),你可以用许多不同的方法拥有成功的编程生涯,而我的方法就是其中之一。
本文将讨论的三个"脏"编码实践是:
如果你想跳过文章的其余部分,这就是要点。
我认为大函数没问题。事实上,我认为代码库中某些大函数通常是好事。
这与《代码整洁之道》形成对比,后者说:
"函数的第一规则是它们应该小。函数的第二规则是它们应该比小更小。" 《代码整洁之道》
当然,这总是取决于我正在做的工作类型,但我通常倾向于将我的函数组织成以下形式:
作为"关键"函数的例子,考虑htmx中的issueAjaxRequest()
。这个函数将近400行!
绝对不整洁!
然而,在这个函数中有很多上下文需要保持,它列出了一系列必须按相当线性的顺序进行的特定步骤。将其拆分成其他函数没有任何重用价值,我认为这样做会损害函数的清晰度(对我来说也很重要的是可调试性)。
我喜欢大函数的一个重要原因是,我认为在软件中,在其他条件相同的情况下,重要的事情应该大,而不重要的事情应该小。
考虑"整洁"代码与"脏"代码的视觉表示:
当你将函数拆分成许多大小相同的小实现时,你最终会将实现的重要部分分散在整个模块中,即使它们在一个更大的函数中表达得很好。
一切看起来都一样:一个函数签名定义,后面跟着一个if语句或for循环,可能有一两个函数调用,然后返回。
如果你允许重要的"关键"函数更大,就更容易从函数海洋中挑出它们,它们显然很重要:看看它们,它们很大!
总的来说,所有类别中的函数也更少,因为许多代码已经合并到更大的函数中。更少的代码行专用于特定的类型签名(可能会随时间变化),更容易记住重要甚至中等重要的函数名称和签名。当你这样做时,总的代码行数也往往会减少。
我更喜欢进入一个新的"脏"代码模块:我能更快地理解它,并更容易记住重要的部分。
关于理想函数大小的实证(软件中的可怕词汇!)证据是什么?
在《代码大全》的第7章第4节中,Steve McConnell列出了一些支持和反对较长函数的证据。结果好坏参半,但他引用的许多研究表明,较大而非较小的函数在每行错误指标上表现更好。
还有一些更新的研究主张较小的函数(<24行),但关注的是他们所谓的"变更倾向"。关于错误,他们说:
SLOC与错误倾向(即#BuggyCommits)之间的相关性显著低于四个变更倾向指标。
当然,较长的函数中有更多代码,所以每行代码的错误倾向相关性会更低。
来看看一些来自现实世界复杂且成功的软件的例子?
考虑SQLite中的sqlite3CodeRhsOfIn()
函数,这是一个流行的开源数据库。看起来超过200行,浏览SQLite代码库会提供许多其他大函数的例子。SQLite以极高的质量和良好的维护著称。
或者考虑Google Chrome网络浏览器中的ChromeContentRendererClient::RenderFrameCreated()
函数。看起来也超过200行。同样,浏览代码库会提供许多其他长函数的例子。Chrome正在解决软件中最难的问题之一:成为一个好的通用超媒体客户端。然而他们的代码对我来说看起来不太"整洁"。
接下来,考虑Redis中的kvstoreScan()
函数。更小,大约40行,但仍比《代码整洁之道》建议的大得多。快速浏览Redis代码库会提供许多其他"脏"的例子。
这些都是基于C的项目,也许小函数的规则只适用于面向对象的语言,比如Java?
好吧,看看IntelliJ中CompilerAction
类的update()
函数,大约90行。同样,浏览他们的代码库会揭示许多其他超过50行的大函数。
SQLite、Chrome、Redis和IntelliJ...
这些都是重要、复杂、成功且维护良好的软件,但我们可以在它们中找到大函数。
现在,我不想暗示这些项目中的任何工程师以任何方式同意这篇文章的观点,但我认为我们有相当好的证据表明,在软件项目中较长的函数是可以的。似乎可以安全地说,仅仅为了保持函数小而拆分函数是不必要的。当然,你可以考虑为了其他原因这样做,比如代码重用,但仅仅为了小而小似乎没有必要。
我是测试的忠实粉丝,强烈推荐将测试软件作为构建可维护系统的关键组成部分。
htmx本身之所以可能,只是因为我们有一个良好的测试套件,帮助我们在开发时确保库的稳定性。
如果你看一下测试套件,你可能会注意到相对缺乏单元测试。我们很少有直接调用htmx对象函数的测试。相反,测试大多是集成测试:它们设置特定的DOM配置和一些htmx属性,然后,例如,点击一个按钮并验证之后DOM状态的某些方面。
这与《代码整洁之道》推荐的广泛单元测试,结合测试优先开发形成对比:
第一定律 在编写生产代码之前,你必须先编写一个失败的单元测试。 第二定律 你不能编写比足以失败的单元测试更多的代码,而不编译就是失败。 第三定律 你不能编写比足以通过当前失败测试更多的生产代码。
我通常避免做这种事情,特别是在项目的早期。早期你通常不知道你的领域正确的抽象是什么,你需要尝试几种不同的方法来弄清楚你在做什么。如果你采用测试优先的方法,你最终会得到一堆测试,随着你探索问题空间,试图找到正确的抽象,这些测试会被破坏。
此外,单元测试鼓励对你编写的每个函数进行详尽测试,所以你最终会有更多测试与特定实现绑定,而不是高级API或代码模块的概念思想。
当然,你可以在改变时重构测试,但现实是,一个庞大且不断增长的测试套件在项目中会形成自己的质量和动量,特别是随着其他工程师的加入,随着测试的增加,改变变得越来越困难。你最终会为测试代码创建测试助手、模拟等。
所有这些代码和复杂性往往会随着时间的推移将你锁定在特定的实现中。
在许多项目中,我更喜欢的方法是早期进行一些单元测试,但不要太多,等到模块的核心API和概念已经明确。
那时,我用集成测试彻底测试API。
根据我的经验,这些集成测试比单元测试更有用,因为即使你改变实现,它们也能保持稳定和有用。它们与当前代码库的绑定不那么紧密,而是表达更高级别的不变量,更容易在重构中存活。
我还发现,一旦你有了一些更高级别的集成测试,你就可以进行测试驱动开发,但在更高级别上:你不考虑代码单元,而是考虑你想实现的API,为该API编写测试,然后以你认为合适的方式实现它。
所以,我认为你应该推迟承诺大型测试套件,直到项目的后期,而且测试套件应该在比测试优先开发建议的更高级别上进行。
通常,如果我能编写一个更高级别的集成测试来演示一个错误或功能,我会尝试这样做,希望更高级别的测试对项目有更长的保质期。
我使用的最后一个编码策略是,我通常努力最小化项目中的类/接口/概念数量。
《代码整洁之道》并没有明确说你应该最大化系统中的类数量,但它提出的许多建议往往会导致这种结果:
- "优先选择多态而非If/Else或Switch/Case"
- "类的第一规则是它们应该小。类的第二规则是它们应该比小更小。"
- "单一职责原则(SRP)指出,一个类或模块应该有一个,且只有一个变更的理由。"
- "你可能会注意到的第一件事是程序变得更长了。从一页多一点变成了近三页。"
与函数一样,我不认为类应该特别小,或者你应该优先选择多态而非简单(甚至是一个长而粗糙的)if/else语句,或者给定的模块或类应该只有一个变更的理由。
我认为这里的最后一句话很好地暗示了为什么:你最终会有更多的代码,可能对系统的实际好处很小。
你经常会听到人们批评"上帝对象"的概念,我当然理解这种批评的来源:一个不连贯的类或模块,有一堆不相关的函数,显然是一件坏事。
然而,我认为对"上帝对象"的恐惧往往会导致相反的问题:过度分解的软件。
为了平衡这种恐惧,让我们看看我最喜欢的软件包之一,Active Record。
Active Record提供了一种将ruby对象映射到数据库的方式,它被称为对象/关系映射工具。
在我看来,它在这方面做得很好:它使简单的事情变得简单,中等的事情足够简单,当需要时,你可以轻松地转向原始SQL。
(这是我称之为"分层"API的一个很好的例子。)
但Active Record对象的功能不止于此:它们还提供了在Rails的视图层构建HTML的优秀功能。它们不包括HTML特定的功能,但它们提供了在视图端有用的功能,比如提供检索错误消息的API,甚至在字段级别。
当你在编写Ruby on Rails应用程序时,你只需将Active Record实例传递给视图/模板。
与更重度分解的实现相比,验证错误作为自己的"关注点"处理。现在你需要传递(或至少访问)两个不同的东西才能正确生成HTML。在Java社区中,采用DTO模式并拥有另一组完全不同于ORM层的对象传递给视图并不罕见。
我喜欢Active Record的方法。从纯粹主义者的角度来看,它可能没有分离关注点,但我的关注点通常是将数据从数据库获取到HTML文档中,Active Record在这方面做得很好,不需要我在过程中处理一堆其他对象。
这帮助我最小化系统中需要处理的对象总数。
会有一些功能悄悄进入模型,可能有点"视图"味道吗?
当然,但这并不是世界末日,它减少了我必须处理的层和概念数量。有一个类处理从数据库检索数据,持有领域逻辑,并作为向视图层呈现信息的容器,对我来说极大地简化了事情。
我已经给出了我的脏编码方法的三个例子:
我再次提出这些,不是要说服你像我一样编码,也不是要暗示我的编码方式在任何方面是"最优"的。
而是为了给你,特别是年轻的开发者,一种感觉,你不必按照许多思想领袖建议的方式编写代码,也能拥有成功的软件生涯。
如果有人称你的代码"脏",你不应该感到害怕:许多非常成功的软件就是以这种方式编写的,如果你专注于核心思想和软件工程,你很可能会成功,尽管它有多"脏",甚至可能正是因为如此!