“每一行代码都是无缘无故地编写的,出于弱点而维护,然后偶然删除”让-保罗·萨特(Jean-Paul Sartre)用ANSIC编写的程序。
编写的每一行代码都要付出代价:维护。为了避免为大量代码付费,我们构建了可重用的软件。代码重用的问题在于它会妨碍您以后改变主意。
API的使用者越多,您必须重写的引入更改的代码就越多。同样,您对第三方API的依赖越多,当它发生变化时,您的损失也就越大。管理代码如何组合在一起,或者哪些部分依赖于其他部分,这是大型系统中的一个重大问题,而且随着项目年龄的增长,这一问题会变得越来越困难。
我今天的观点是,如果我们想要计算代码行,我们不应该将它们视为“产生的行”,而应该将其视为“花费的行”EWD1036。
如果我们将“代码行”视为“花费的行”,那么当我们删除代码行时,我们就降低了维护成本。我们应该尝试构建一次性软件,而不是构建可重用的软件。
我不需要告诉您,删除代码比编写代码更有趣。
要编写易于删除的代码:重复自己以避免创建依赖项,但不要重复自己来管理它们。代码也要分层:用实现更简单但使用起来笨拙的部分构建易于使用的API。拆分代码:将难以编写和可能更改的部分与代码的其余部分隔离开来,并相互隔离。不要硬编码每个选项,也许可以在运行时更改一些。不要试图同时做所有这些事情,也许从一开始就不要编写这么多代码。
代码的行数本身并不能告诉我们太多,但是代码的大小可以告诉我们50,500,000,10,000,000等等。一百万行的代码块将比一万行的代码块更烦人,而且需要更换的时间、金钱和精力明显更多。
虽然您拥有的代码越多,就越难去除,但是保存一行代码本身绝对不会节省任何东西。
即便如此,最容易删除的代码还是您最初避免编写的代码。
通过在代码库中使用的几个示例来构建可重用代码是事后更容易完成的事情,而不是预测您稍后可能需要的示例。从好的方面来说,仅仅通过使用文件系统,您可能已经重用了大量代码,为什么要担心那么多呢?少量冗余是健康的。
最好复制粘贴代码几次,而不是创建库函数,只是为了掌握如何使用它。一旦您将某些东西设置为共享API,就会使其更难更改。
调用函数的代码将依赖于其背后实现的有意和无意行为。使用您的函数的程序员将不依赖于您记录的内容,而依赖于他们观察到的内容。
删除函数内部的代码比删除函数简单。
当您复制和粘贴某些内容的次数足够多时,也许是时候将其拉到一个函数中了。这是“从我的标准库中拯救我”的东西:“打开一个配置文件并给我一个哈希表”,“删除这个目录”。这包括没有任何状态的函数,或者具有一些全局知识(如环境变量)的函数。最终保存在名为“util”的文件中的内容。
另外:创建一个util目录,并将不同的实用程序保存在不同的文件中。单个util文件将始终增长,直到它太大,但又太难拆分。使用单个util文件是不卫生的。
代码对您的应用程序或项目越不具体,它们就越容易重用,更改或删除的可能性也就越小。库代码,如日志记录、第三方API、文件句柄或进程。您不打算删除的代码的其他很好的示例是列表、哈希表和其他集合。不是因为它们通常只有非常简单的接口,而是因为它们的范围不会随着时间的推移而增长。
我们不是让代码易于删除,而是尽量使难以删除的部分远离易于删除的部分。
尽管编写了避免复制粘贴的库,但我们通常会通过复制粘贴编写更多代码来使用它们,但是我们给它起了一个不同的名字:样板。样板很像复制-粘贴,但是您每次都要在不同的位置更改一些代码,而不是一遍又一遍地更改相同的位。
与Copy Paste一样,我们复制部分代码以避免引入依赖关系,获得灵活性,并以冗长的方式为其买单。
需要样板的库通常是网络协议、线路格式或解析工具包之类的东西,这些东西很难在不限制选项的情况下将策略(程序应该做什么)和协议(程序可以做什么)交织在一起。这段代码很难删除:通常需要与另一台计算机对话或处理不同的文件,而我们最不希望做的事情就是将其与业务逻辑混为一谈。
这不是代码重用的练习:我们试图保持频繁更改的部分,远离相对静态的部分。最小化库代码的依赖性或职责,即使我们必须编写样板才能使用它。
您正在编写更多的代码行,但是您是在易于删除的部分中编写这些代码行。
当图书馆被期望迎合所有人的口味时,样板模式效果最好,但有时会有太多的重复。是时候用一个对策略、工作流和状态有意见的库来包装您的灵活库了。构建简单易用的API就是将您的样板转换为库。
这并不像您想象的那么少见:最受欢迎和喜爱的python http客户端之一Requests就是一个成功的示例,它提供了一个更简单的接口,其底层是一个更详细、更易于使用的库urllib3。当使用http时,Requests迎合了常见的工作流,并且对用户隐藏了许多实用的细节。同时,urllib3执行流水线操作、连接管理,并且不会对用户隐藏任何内容。
当我们将一个库包装在另一个库中时,并不是隐藏了细节,而是分离了关注点:请求是关于流行的http冒险,urllib3是关于给你选择自己冒险的工具。
我并不主张您创建/protocol/和/policy/目录,但是您确实希望尝试使您的util目录不受业务逻辑的影响,并在更易于实现的库之上构建更易于使用的库。您不必完成一个库的编写即可开始编写另一个库。
包装第三方库通常也很好,即使它们不是协议风格的。您可以构建适合您的代码的库,而不是在整个项目中锁定您的选择。构建易于使用的API和构建可扩展的API往往是相互矛盾的。
这种关注点的划分让我们可以让一些用户感到高兴,而不会让其他用户不可能做到这一点。当您从一个好的API开始时,分层是最容易的,但是在一个糟糕的API之上编写一个好的API却难得令人不快。好的API是为将要使用它的程序员设计的,而分层是意识到我们不能一下子取悦所有人。
分层与其说是编写我们可以稍后删除的代码,不如说是使难以删除的代码易于使用(而不会被业务逻辑污染)。
你已经复制-粘贴,你已经重构,你已经分层,你已经组合,但是代码在一天结束时仍然要做一些事情。有时候,最好的办法就是放弃,编写大量无用的代码来将其余部分整合在一起。
业务逻辑是以一系列无休止的边缘案例和快速而肮脏的黑客行为为特征的代码。这样挺好的。我对此没意见。其他风格,如“游戏代码”或“创建者代码”也是一样的:偷工减料,节省大量时间。
原因呢?有时候,删除一个大错误比尝试删除18个交错的小错误要容易得多。很多编程都是探索性的,犯错几次然后迭代要比认为第一次正确要快得多。
对于更有趣或更有创造性的努力来说,这一点尤其正确。如果您正在编写您的第一个游戏:不要编写引擎。同样,在编写应用程序之前不要编写Web框架。第一次去写一堆乱七八糟的东西。除非你是灵媒,否则你不会知道如何分割。
Monorepos也有类似的权衡:您不知道如何预先拆分代码,坦率地说,一个大错误比20个紧密耦合的错误更容易部署。
当您知道哪些代码将很快被丢弃、删除或轻松替换时,您可以偷工减料。特别是如果你制作一次性的客户网站、活动网页。任何您拥有模板并冲印副本的地方,或者您填补框架留下的空白的地方。
我不是建议你把同样的泥球写上十遍,来弥补你的错误。引用佩利斯的话说:“一切都应该自上而下地建造,除了第一次。”您应该尝试每次都犯新的错误,承担新的风险,并通过迭代慢慢积累起来。
成为一名专业的软件开发人员正在积累一系列的遗憾和错误。你从成功中什么也学不到。这并不是因为您知道好的代码是什么样子,而是坏代码的伤疤在您的脑海中记忆犹新。
无论如何,项目要么失败,要么最终成为遗留代码。失败比成功更容易发生。写下十个大泥球,然后看看它会把你带到什么地方,比试着擦亮一个大便要快得多。
删除所有代码比分段删除要容易得多。
大泥球是最容易建造,但维护费用最高的。看似简单的更改最终以一种特别的方式几乎触及了代码库的每一个部分。以前容易整体删除的内容现在不可能分段删除。
同样,我们将代码分层以分离责任,从特定于平台到特定于领域,我们需要找到一种方法来梳理顶部的逻辑。
[开始]列出困难的设计决策或可能更改的设计决策。然后,每个模块都被设计成对其他模块隐藏这样的决定。D·帕纳斯(D.Parnas)。
我们不是将代码拆分成具有共同功能的部分,而是根据代码不与其他部分共享的内容将代码拆分。我们将最令人沮丧的部分彼此隔离以进行写入、维护或删除。
我们构建的模块不是围绕着能够重用它们,而是围绕着能够改变它们。
不幸的是,有些问题比其他问题更相互交织,更难分离。虽然单一责任原则建议“每个模块只能处理一个硬问题”,但更重要的是“每个硬问题只由一个模块处理”。
当一个模块做两件事时,通常是因为更改一个部分需要更改另一个部分。通常,拥有一个界面简单的糟糕组件要比拥有两个需要仔细协调的组件容易得多。
今天,我将不再试图进一步定义我理解为包含在这个速记描述(“松散耦合”)中的材料的种类,也许我永远也不会成功地这样做。但是当我看到它的时候我就知道了,这个案例中涉及的代码库并不是这样的。SCOTUS大法官斯图尔特
可以在不重写其他部件的情况下删除部件的系统通常被称为松散耦合,但是解释一个部件是什么样子要比首先如何构建容易得多。
即使只硬编码一次变量也可能是松散耦合的,或者在变量上使用命令行标志。松散耦合是指能够在不更改太多代码的情况下改变主意。
例如,Microsoft Windows具有用于此目的的内部和外部API。外部API绑定到桌面程序的生命周期,内部API绑定到底层内核。隐藏这些API为微软提供了灵活性,而不会在此过程中破坏太多软件。
HTTP也有松散耦合的例子:在HTTP服务器前面放置一个缓存。将您的图像移动到CDN,只需更改指向它们的链接。两者都不会破坏浏览器。
HTTP的错误代码是松散耦合的另一个例子:Web服务器上的常见问题都有唯一的代码。当您收到400错误时,再次执行此操作将获得相同的结果。A500可能会有变化。因此,HTTP客户端可以代表程序员处理许多错误。
在将其分解为更小的部分时,必须考虑软件如何处理故障。要做到这一点,说起来容易做起来难。
我决定不情愿地在EX使用L。在存在软件错误的情况下制作可靠的分布式系统。阿姆斯特朗,2003。
Erlang/OTP在选择如何处理故障方面相对独特:监管树。粗略地说,Erlang系统中的每个进程都由一个主管启动和监视。当进程遇到问题时,它会退出。当进程退出时,管理程序会重新启动该进程。
(这些主控引擎由引导进程启动,当主控引擎遇到故障时,将由引导进程重新启动)。
关键思想是快速故障和重启比处理错误更快。这样的错误处理可能看起来有违直觉,在错误发生时通过放弃来获得可靠性,但断断续续地关闭事物有一个抑制瞬时错误的诀窍。
错误处理和恢复最好在代码库的外层完成。这称为端到端原则。端到端原则认为,在连接的远端处理故障比在中间处理故障更容易。如果你在里面有任何处理,你还是要做最后的顶层检查。如果顶层的每一层都必须处理错误,那么为什么还要费心在内部处理它们呢?
错误处理是将系统紧密绑定在一起的众多方式之一。紧耦合的例子还有很多,但是单独指出一个设计不好有点不公平。除了IMAP。
在IMAP中,几乎每个操作都是雪花,具有独特的选项和处理方式。错误处理是痛苦的:错误可能会在另一个操作的结果中途出现。
IMAP生成唯一的令牌来标识每条消息,而不是UUID。这些也可能在手术结果的中途发生变化。许多操作不是原子的。花了25年多的时间才找到了一种可靠地将电子邮件从一个文件夹移动到另一个文件夹的方法。有一种特殊的UTF-7编码,也有一种独特的Base64编码。
相比之下,文件系统和数据库都是更好的远程存储示例。对于文件系统,您有一组固定的操作,但是您可以操作大量的对象。
尽管SQL看起来像是一个比文件系统宽得多的接口,但它遵循相同的模式。集合上的多个操作,以及要操作的多个行。尽管您不能总是将一个数据库换成另一个数据库,但是在任何自制查询语言上都可以更容易地找到与SQL一起工作的东西。
松散耦合的其他示例是具有中间件或过滤器和管道的其他系统。例如,Twitter的finagle为服务使用了一个通用API,这使得通用超时处理、重试机制和身份验证检查可以毫不费力地添加到客户端和服务器代码中。
(我敢肯定,如果我在这里不提到UNIX管道,就会有人抱怨我)。
首先,我们对代码进行分层,但现在这些层中的一些层共享一个接口:具有各种实现的一组公共行为和操作。松散耦合的好例子通常是统一接口的例子。
健康的代码库不必是完全模块化的。模块化的位使编写代码变得更加有趣,就像乐高积木一样有趣,因为它们都适合在一起。一个健康的代码库有一些冗长、一些冗余,并且移动部分之间的距离刚刚好,这样您就不会把手困在里面。
松耦合的代码不一定容易删除,但它更容易替换,也更容易更改。
能够在不处理旧代码的情况下编写新代码,这使得尝试新想法变得容易得多。这并不是说您应该编写微服务而不是巨型代码,但是您的系统应该能够在您计算出您正在做的事情时支持一个或两个实验。
功能标志是稍后改变主意的一种方式。尽管功能标志被视为试验功能的方式,但它们允许您在不重新部署软件的情况下部署更改。
谷歌Chrome是它们带来的好处的一个壮观的例子。他们发现,保持定期发布周期最困难的部分是将长期存在的功能分支合并在一起所需的时间。
通过无需重新编译即可打开和关闭新代码,可以将较大的更改分解为较小的合并,而不会影响现有代码。由于同一代码库中出现的新特性较早,因此当长时间运行的特性开发会影响代码的其他部分时,这一点变得更加明显。
功能标志不仅仅是一个命令行开关,它还可以将功能发布与合并分支解耦,并将功能发布与部署代码解耦。当推出新软件可能需要数小时、数天或数周的时间时,能够在运行时改变主意变得越来越重要。问问任何SRE:任何可以在夜间唤醒您的系统都值得在运行时进行控制。
你迭代的次数并不多,但你有一个反馈循环。与其说构建模块是为了重用,不如说是隔离组件以进行更改。处理变化不仅仅是开发新功能,还包括淘汰旧功能。编写可扩展代码就是希望在三个月的时间内,您可以把一切都做好。编写可以删除的代码是在相反的假设下工作。
我谈到的策略-分层、隔离、公共接口、组合-不是关于编写好的软件,而是如何构建可以随着时间的推移而变化的软件。
因此,管理问题不是是否建立一个试点系统并将其丢弃。你会这么做的。[…]。因此,计划扔掉一个;不管怎样,你会的。弗雷德·布鲁克斯。
你不需要把它全部扔掉,但你需要删除一些。好的代码不在于第一次就正确。好的代码只是不会妨碍的遗留代码。