编写易于删除,而不是易于扩展的代码
原文链接
http://programmingisterrible.com/post/139222674273/write-code-that-is-easy-to-delete-not-easy-to
翻译:马琳
校对:郭晓磊,汤涛
感谢郭晓磊同学对文章地改动。感谢汤涛叔叔的大力支持。文章在正月初九翻译完毕,一直未发。由于编程经验有限。翻译难免有不正确的地方。欢迎大家纠正错误。
编写易于删除,而不是易于扩展的代码
“毫无目的地去编码,将会导致无法维护和删除” —— Jean-Paul Sartre《Programming in ANSI C》
我们编写的每一行代码都应该是可维护的。为了避免写出大量的代码,我们应该构建一个可复用的软件。代码可复用的问题将在未来潜移默化地改变你的编程思想。
假设你越多地依赖一个API,那么当它变化时,你将不得不为它引入很多其它的变化。
在一个大型系统中,各个模块之间代码怎样配合以及怎样管理它们之间的相互依赖关系是一个非常重要的难题,并且这个难题会随着project地逐渐膨大而变得愈加困难。
对于代码行数,眼下我的观点是,不应该把它们当作“写出了多少行代码”,而是“付出了多少行代码” EWD 1036
假设我们把’代码的行数’看做是‘花费的行数’,那么当我们删除代码时。这相当于降低我们的维护成本。
我们应该尝试建立一次性使用的软件,而不是花大力气构建可复用的软件。
告诉你个小秘密:删代码比写代码更有趣:)。
编写易删除的代码:要不断地提醒自己避免创建依赖关系,而不是去管理它们。
对代码进行分层:要创建易于使用的API。而不是易于实现但使用繁琐的API。
切割你的代码:将逻辑复杂难于实现的代码,以及易发生改变的代码,与其它的代码隔离开来。
尽量不要硬编码,应该同意代码在执行期能够发生改变。不要试图一次就做全然部事情,首先不要写大量的代码。
首先:不要急着開始写代码
代码行数除了能告诉我们代码的数量级(如50行、500行、5000行、10000行、25000行…)外,并不能告诉我们很多其它的信息。
替换一百万代码明显要比替换一万行代码要花费很多其它的时间、金钱和精力。并且会让人感到痛不欲生。
代码越多越难去除,节省一行代码也差点儿不会产生不论什么作用。
既然如此。首先删除那些你能够避免写的代码。
步骤1:复制粘贴代码
在基于已有代码的基础上构建可重用的代码,远比凭空构建easy很多。再说,你非常可能已经通过文件系统复用了大量的代码。何必操心这么多呢?有一点冗余是没有关系的。
拷贝粘贴代码,比“创建一个库函数,然后在使用时获取它的句柄”的方式更节省时间。其实。一旦你公布了一个公共API,那么你将非常难再改变它。
某个程序猿的代码一旦调用了你写的函数,那么它将不得不依赖这个函数中全部的有意或无意的实现。
到时。他将不再信任你的文档说明。而仅仅相信他自己所观察到的结果。删除一个函数内部的代码要比删除函数更简单。
步骤2:不要复制粘贴代码
当你已经复制粘贴了非常多次某段代码了。这时也许是时候把它们提炼成一个函数了。这是东西“从我的标准库解救我”了:“打开一个配置文件,并给我一个哈希表”,“删除这个文件夹。”像这样的没有不论什么状态的函数,或者仅仅包括一些全局常量比方环境变量的函数。终于将写在一个命名为“util”的文件里。
旁白:做一个util文件夹,并让不同的文件拥有不同的功能。
一个单独的工具文件会一直增长,直到它太大了而难以分开。
使用一个单独的工具文件是不合理的。你的应用或者project中特定的代码越少。它们越easy被复用,而改变或删除的可能性越小。
库代码如日志输出,或第三方的API。文件处理,或进程管理。
其它一些好的演示样例代码你不打算删除,它们是哈希表。和其它collections。
不是由于它们通常具有非常简单的接口,而是由于他们在一定的时间范围内不会增长。
不是说,把不论什么代码都写的易于删除,而是我们应该尽力将难以删除的部分和easy删除的部分分隔开。
步骤3:编写很多其它的样板代码
尽管写一些库能够避免复制粘贴,通常我们终于还是为了使用它们而复制粘贴了大量的代码,但我们称呼它们为样板代码。样板代码非常像复制粘贴,但你每次在不同的场合都会改动当中的一些代码,而不是一遍又一遍去改动一些非常细微的地方。
像复制粘贴,通常我们复制部分代码,是为了避免引入依赖,获得灵活性,在冗长中并使用它。
那些须要样板代码的库,一般是一些像网络协议,线格式,或解析包。它们在行为(一个程序应该做什么)和协议(程序能够做什么)上没有交集,没有限制的选项。
这段代码非常难删除:它是必需的用于和还有一台计算机进行交互或处理不同的文件,这时候我们要做的是在业务逻辑内将它丢掉。
在代码复用中这并非一个运用:我们尽量使经常变化的部分远离那些相对精巧的部分。
即使我们须要写样板去使用它,我们还是有必要将库中代码之间的依赖关系或责任最小化。
你将写非常多行代码,但请在easy删除的部分中写你的代码。
第4步:不要写样板代码
假设库想要迎合全部口味,那么样板代码的效果最好。
但有时,实在太多反复。如今是时候封装你的具有良好扩展性的库,让这个库带有行为,流程和状态这些项。构建简单易用的api是把你的样板代码变成一个库。
你可能觉得这个并不常见:其实,最受欢迎的和拥护的python httpclient,请求就是一个成功的样例。它提供了一个简单的界面,它的底层由一个功能强大的urllib3库支持着。使用http请求时迎合常见的工作流,隐藏了很多来自用户的实际细节。
同一时候,urllib3管道,连接管理,不隐藏不论什么来自用户的东西。
与其说我们隐藏了细节当我们在一个库的基础上包装还有一个库,倒不如说我们分离关注:关于流行的http请求,urllib3将给你提供一个工具来选择自己的行为。
我不主张你去创建一个/协议/,/行为/文件夹,但你想尝试和保持你的util文件夹自由的业务逻辑,并建立更易于使用的库上easy实现的。你不须要写完一个库然后在还有一个库之上再写。
通常包装第三方库也是一个好方法,即使它们不是协议型。你能够建立一个适用于你编写代码的库,而不是将目光锁定在整个项目的层次上。建立一个使用方便的API和构建一个可扩展的API往往相互矛盾。
关注这个问题使我们能够让一些用户高兴,同一时候不会对其它带来不便。当你開始使用一个设计良好的API时。分层是easy,可是在一个不好的基础上想写出一个设计良好的API的确不是一个令人愉快的经历。
好的Api在设计的时候会考虑到使用它的程序猿,同一时候在分层设计的时候应该意识到我们不能取悦全部人。
分层能够降低那些之后能够删除的代码的编写。可是删除代码后使用起来不会令人感到愉快(在业务逻辑的范畴内没有造成破坏)。
第五步:编写一大堆代码
你已经完毕了复制粘贴,重构,分层,组合,可是在一天结束的时候还有一些事情须要做。有时候放弃是一种非常好的选择,在每天最高效的时间写代码中最重要的部分。
业务逻辑代码的特点是一系列永无止境的边界情况和高速迭代和一些棘手的bug。这非常好。对这些事情我能够从容应对。其它的如‘游戏代码’,或 ‘基础代码’ 是一样的:偷工减料节省大量的时间。
这么做的原因是什么呢?通常修复一个大的bug比修复18个相互交叉的小bug要easy的多。大多数编程都是在探索,经常非常快就会多次出错,非常难一次性编写正确。
这是一个充满了乐趣和创造性的过程。
假设你打算写自己第一个游戏程序的话:不要写一个引擎。
相同,不要写一个web框架在编写应用程序之前。第一次去写一堆烂代码。除非你真的不知道怎样分解。
Monorepos相似的权衡:你不知道怎样去分解你的代码,坦白说解决一个大的错误比解决20紧密耦合错误easy非常多。
当你知道代码将非常快废弃,删除,或被替换,你能够避免进入一些窘境。特别是假设你编写一次性客户站点,网页。
我不是建议你写毫无价值的代码10次以上,掩饰自己的错误。引用玻璃市语录:“除了第一次,一切都应该自上而下地建立”。
你应该试着每次犯新的错误,冒新的风险,并通过迭代慢慢地解决错误。成为一个专业的软件开发人员是靠大量遗憾和错误积累造就的。
你从成功中什么都学不到。这并非说你知道好的代码是什么样子,烂代码的伤疤能够刷新你的认知。
终于项目失败或成为遗留代码。
失败的出现总是多于成功。非常easy写一大堆烂代码并且这些烂代码将会让你非常难堪,比收拾一堆狗屎还难堪。
删除全部的代码比分段删除更加easy。
步骤6:将你的代码分解成碎片
一大堆代码是最easy构建但维护起来将付出巨大的代价。改动代码时有一种牵一发而动全身的感觉。删除全部的代码是easy的。可是不能这样做。最后仅仅能删除代码片段。
我们对代码依据单一责任模式进行分层,从特定的平台到特定的领域,我们须要找到一种方法去梳理逻辑。
開始的时候有非常多困难的设计决策,这样的情况是非常有可能解决的,假设每一个模块在设计的时候将这写决策隐藏。
D. Parnas
我们隔离最令人沮丧的部分。将它们彼此隔离,然后编写,维护,或删除代码。
我们不是为了能够复用模块代码而到处构建它们,而是为了能够更好的维护它们。
不幸的是,有些问题总是交织在一起,非常难将它们彼此分开。
尽管单一责任模式建议,“每一个模块应该仅仅处理一个难题”,更重要的是‘每一个困难的问题是仅仅由一个模块处理’。当一个模块做两件事情时,经常由于一个部分的改变须要改变还有一个部分。
经常easy有一个可怕的组件包括一个简单的接口,而不是两个组良好的配合。
我不会为了迎合“松耦合”的概念而到处定义一些东西。而是理解它的思想。当我看到它时,相关的代码就会变得好非常多。
在一个系统中你能够删除一个部分而不用重写其它部分通常被称为松散耦合,比起怎样去做解释起来的确非常easy。
即使硬编码一个变量一旦能够松散耦合,也许使用一个命令行就能够标记一个变量。松耦合是能够改变你的思想但并不会改变太多的代码。
比如,Microsoft Windows为了达到松耦合有内部和外部api。外部API和桌面程序的生命周期相关,而内部API处理底层内核相关。
隐藏这些api提高了Windows系统的灵活性同一时候在软件的开发过程中并没有产生非常多破坏。
HTTP也是一个松耦合的样例:将缓存放到HTTP服务端。将你的图片移动到CDN中,仅仅是改变链接。
并没有对浏览器造成影响。
HTTP错误代码也是松耦合的一个样例:与webserver交互的特定响应状态码。当你得到一个400的错误码时(你訪问的页面域名不存在或者请求错误),再次请求一遍会还会得到相同的结果。而一个500的错误码再次请求可能会发生变化。因此,HTTPclient能够替程序开发人员处理很多错误。
你的软件在怎样处理错误的问题上,必须考虑分解成小的模块来处理。说起来easy做起来难。
我已经决定,不再使用乳胶。而是制定可靠的分布式系统来处理软件错误。
阿姆斯特朗,2003
Erlang / OTP在它选择怎样处理错误时有一个相对独特的东西被称之为:监督树。概况的讲,每一个Erlang系统中的进程都由一个监督者来启动和管理。
当一个进程遇到一个问题,它退出。当进程退出时,它由监管者又一次启动。
(这些监管者被引导程序启动,当它遇到错误时,它将被引导引导程序又一次启动)
这个核心的理念是对失败的迅速响应和重新启动而不是对错误的处理。
这样的错误处理似乎是反直觉的,当发生错误时通过放弃来获得可靠性,但又能够抑制瞬态故障而把事情断断续续完毕。
错误处理和恢复最好放在你代码库的外层来完毕。
这就是所谓的端对端原则。端对端原则觉得,在连接的断点处理问题比在中间处理要简单非常多。假设你在内部处理不论什么事情,最后你还得在顶层检查一遍。假设每一个层上都必须处理错误,那么为什么要这么麻烦地在它们内部进行处理呢?
有非常多方式会将一个系统紧密地绑定在一起,对错误的处理就是当中的一个。
紧密耦合的样例有非常多,可是这对一个被称为糟糕的设计的IMAP协议来说是不公平。
在IMAP中差点儿每一个操作都像雪花一样,具有独特的选项和处理方式。
错误处理是痛苦的:错误可能出如今还有一个操作的过程中。
不是uuid,IMAP能够生成唯一的标记来识别每一个消息。
可是这些标示可能在一个操作进行的最后发生改变。
很多操作不具有原子性。它花费了超过25年才使得一个电子邮件从一个文件夹移动到还有一个能够可靠地完毕。有一个特殊的编码utf-7,还有一个独特的base64编码。
我从来都没有使用过它们。
相比之下,用两个文件系统和数据库来进行远程存储是一个更好的样例。在一个文件系统中,你有一组固定的操作,可是众多的对象都能够操作它。
尽管SQL比文件系统在接口方面使用的更加广泛,它遵循了相同的模式。一些操作集,和大量对行的操作。尽管你不能总是把一个数据库更换为还有一个,但在查询的领域看。SQL他比不论什么其它语言都easy。
松耦合的样例比方一个带有中间件。过滤器和管道的系统。比如,Twitter的Finagle 在服务中使用通用的API,这同意超时处理,重试机制,client和server代能够码毫不费力进行身份验证检查。
(我敢肯定假设这里我没有提到UNIX管道有人将会抱怨我)
首先我们将我们的代码进行分层,可是总有一些层共享一个接口:一组通用的行为和操作的实现。
好的松耦合的样例通常有统一的接口。
一个好的代码底层不须要全然模块化。模块化地编程使编码充满了乐趣,就像是乐高积木之所以非常有趣,是由于能够组合它们。
一个好的代码底层有一些冗长。一些空间。和足够的距离在移动代码块的时候。不至于让你在编码的过程中深陷当中。
松耦合的代码不一定是easy删除的,但它更easy替换,也更easy改变。
第七步:保持持续的编码
编写全新的代码比使用老代码更easy实现一个新的想法。不是说你应该去写非常过微服务和一些零碎的东西。而是当你进行编程时能够确保你的系统能够支撑一个或者两个实验。
功能标示是改变你的想法的一种方式。
尽管功能标志被看做是一种带有实现性质的方法,它们同意代码在更改后无需又一次部署软件。
Google Chrome就是一个非常好的样例,它从这样的方法中受益良多。他们发现,最难的部分是保持定期的公布周期,在一个周期内须要合并大量的功能分支。
能够逐渐地增加新代码,无需又一次编译、较大的变化能够分解成小的合并,而不会影响现有的代码。早些时候新的特性出如今同一个代码库中,执行的新特性代码将会影响到其它部分的代码,随着时间的推移这样的影响会变得越来越明显。
功能标志不仅仅是一个命令行开关,这是将新特性从合并分支版本号中解耦,从部署版本号中解耦的一种方法。当花费几个小时,几天,甚至几周推出了一个新软件,能够在执行中改变你的想法变得越来越重要。问不论什么的一个运维project师:不论什么能够在晚上唤醒你去工作的系统是一个值得你去控制的系统。
它不须要让你迭代非常多次,而是须要你有一个循环反馈。
它与其说是你为了复用而构建模块,到不如说是为了改变而隔离组件。处理变化不仅仅是开发新特性同一时候也是摆脱旧的东西。
花费三个月的时间去编写可扩展性代码,你将会感到一切都是值得的。
我谈论的策略,分层,隔离,通用的接口,组成——主要思想不是说为了写一个好的软件,而是怎样去构建一个能够随时改变的软件。
因此,管理的问题不是要去构建一个试验系统,而是把它扔掉。你会这样做。无论怎样,你将会因此扔掉一个。弗雷德布鲁克斯
你不须要扔掉一切而是你须要删除一些。好的代码并非一開始就能够写出来。好的代码仅仅是遗留下来的代码而不是获取的代码。
好的代码easy删除。