我最近在一篇文章提到,工程师应该怎样避免使用大量的库、包以及其他依赖关系。我建议的另一种方案是,如果你没有达到重用第三方代码的阈值时,那么你就可以自己编写代码。
在本文中,我将讨论一个在重用和从头开始编写之间的抉择。这些技术能为你提供良好的综合优势。你将会听到我提到的一些在不同编程语言/环境下的例子。但是,这篇文章的层次足够高,而且说实话,你在什么环境下工作,并不重要。
我们一再要求她把字写得小一点。她浪费了那么多纸张。
四种编码技术
设想你所处的情况是这样的,你希望你的软件能够完成当前无法完成的任务。比如,你也许想要一份 PDF 格式的报告,方便打印。但是你的软件并不会输出 PDF。作为一名工程师,你要做的工作就是解决这个问题。
下图展示了四种不同的技术来添加这个功能。我并没有提到“这四种技术”。当然,我肯定还有更多的技术,包括镖靶和猴子。
为什么这么多决定都要权衡利弊呢?这真的很让人恼火。
重用:根据你的环境,你可能会在 lib 中建立链接,或者在 pom.xml 或 package.json 中添加一个条目。不管你怎么做,“重用”就是抓取别人的代码,通过它的接口来使用,而不去关心它的内部结构。这段代码还可能会“偷渡”到一群横向的依赖关系中去,这会给你带来麻烦。最少的努力,带来的是最少的控制。
复制:从 Github repo、代码片段集或者其他地方,选择你所需要的代码。然后把它直接粘贴到你的项目中。这段代码已经存在于你的代码库中,而不再是一个依赖关系。
重写:采用一些适合你的第三方代码,通过重写,使其成为你自己的代码。
编写:你自己编写所有的代码,用自己的脑子,不借用任何人的东西。Ayn Rand 和 Casey Muratori 将尊重你的决定。最大的努力,将得到最大限度的控制。
为了确保我上面的图表给人留下正确的印象,我给了它两把斧子——一把是努力,一把是控制。
另一方面,这家伙的两把斧子也让人印象深刻。
所有这四种技术都是在努力和控制之间的权衡。
你可以通过重用快速地获取大量的功能,然后以软件膨胀、黑盒调试、安全修补和耦合升级的形式遭受缺乏控制的痛苦。在重用的情况下,你不一定会面临这些问题,但是这样做的风险会增大。在你的项目中添加的每一行代码就好像是一张彩票,会“赢得”一个非常糟糕的问题。
或者在这张图的另一端,你从头开始编写所有的代码,你将会对进入项目的新代码进行完全的控制。不过,你可以花些无聊的时间去把所有的东西都按你的想法去编写,而你却成了编程的天后。
在我以前的一篇文章《多编写,少重用》(Write More, Reuse Less)中,我已经详细地阐述了重用和编写之间的权衡。
我将会深入探讨两种中间技术(复制和重写)。
复制
有许多可以复制代码的好地方。如果你要完成的任务可以用一句话来完成,而且代码不超过 100 行即可实现,那么,你只需把问题输入到搜索引擎,就能轻松地找到。
与 DuckDuckGo 相比,谷歌最大的优势在于:“DuckDuckGo”作为动词,在会话中会让人感到难堪。
用这种方式来搜索一些简单的代码任务的问题,你将很快就能找到代码的天堂。
我从 StackOverflow、W3Schools、MDN、Unity Answers 和各种我懒得记住的地方复制了很多代码。我总是先从搜索引擎开始,在那里提出问题。通常情况下,我会先从我工作的语言/平台开始,然后再提问。在我敲代码的时候,看一下自动完成的内容往往很有帮助。
GitHub Copilot
从复制人类答案的机器人那里复制你的答案。
如果你更愿意相信由复杂的人工智能为你编写的代码片段,可以试试 GitHub Copilot。这是一个集成到 IDE 中的插件。基本上,你并不需要在搜索引擎中输入你想要的东西,而是将该文本输入一个略微详细的源码注释即可。然后,实现的源代码就会自动填充在下面。
是真的!我不是在开玩笑。请看一些关于它的视频。
视频:https://youtu.be/FHwnrYm0mNc
超越代码片段
如果你想要为更大的功能部分复制代码,那么只要:
寻找包含这些功能的开源项目;
将所需的文件复制过来。
我刚才提出的一种做法,不是很傻就是很疯狂。我真心希望你能想到下面那个问题。别让我跟你扯淡。
“什么时候从第三方项目中复制会比直接导入更好?”
如果你仅仅想将一个依赖关系的所有未改变的文件复制到你的项目中,那么我将会发现这种复制方法的缺陷。你复制的源码不会轻易地被更新到依赖关系的新版本。这就意味着你会错过 Bug 修复、新功能和安全补丁。
尽管在一些情况下,对某个特定版本的代码进行快照非常有价值,但是你可以通过构建清单(例如 Java 中的 pom.xml,Node 中的 package.json)使用固定版本来完成同样的事情。而且如果你决定要升级的话,那么你可以轻易地更新一个固定版本。
另外一个潜在的复制理由是,你可能需要对项目的源码进行修改。如果你要进行增量更改,你可以最好这样做:1. 从原项目中创建一个复刻项目;或者 2. 把你的更改贡献给原项目。这样可以让你在以后更容易从原项目中收到修改。
有三种不同的方式可以让你在你的项目中修改他人的代码。最上面的一种做法是不好的。
这也许是你大规模、全面地修改你的代码库。你准备在以后的合并项目中,把一切后路都堵死。那真是太好了。这往往是有正当理由的。但是我在本文中所提供的定义是,对源码的大规模结构性的修改属于“重写”技术,下面我会详细讨论。
我唯一能想到的一个很好的理由就是,将第三方项目的代码复制(而非重写)到你的项目中:你只需在这个项目中得到一些源码即可。
而这种理由出现的频率超出了你的想象。大型 Node 包,如 Lodash 和 TurfJS,都是非常聪明的,它们会提供子集包,这些子集包只是为了你需要的特定功能在函数级颗粒度来导入。不过,在野外中也有很多臃肿的怪物。
Nodelerfish 需要你的爱。但不要让它进入你的项目中。
你也许只需要使用 50 行代码就可以完成的库 API。但是单体导入的库,可能会拉来数十万行代码,特别是那些具有横向依赖关系。在这种情况下,将一个子集复制下来,而非将其导入到整个该死的烂摊子中,这完全是很合理的。
开源许可证
我不是一个律师。既然我不是律师,我就会把我在开源许可方面的实践告诉你。你可以咨询顶级律师团队,以达成你自己的决策。
笔者作为非律师,是如何做的:
如果我把源码复制到我的项目中,我把它和导入代码一样对待,并遵循所有的许可条款。在开源软件许可的条款中,一般都是指分发源码或者从许可源码中构建的行为。如果复制的代码是我发布版本的一部分,那么它也算作分发。对于我使用的确切的开源许可证,我也非常谨慎,例如,GPL 可能会要求我使用 GPL 许可证来许可我自己的代码库,而 MIT 许可证几乎不要求我做任何事情。
我还喜欢将复制的代码归入“第三方”目录(例如 Github 上的例子),然后在文档的顶部添加注释,这样就可以保证所有的内容都是一致的。
他们不认识我,但 Matt Daly 和 Chris Anderson 是我的代码兄弟。
重写的优点
你知道你正在复制的代码里有什么吗?它是适合你的代码吗?
你能够而且应当对其他人的代码进行判断。或许不要对这段代码做出无情的评论。但是,请务必为自己的利益,私下评估第三方代码。
重写代码片段
如果是 200 行以内的代码片段,或者是复制的代码,我一定会逐行重写代码,这样可以了解到这些代码是如何工作的,并且做一些改动,使之更适合我的项目。这是为数不多的几次在其他方面进行毫无意义的、风格化的编辑,是有价值的。
我要举一个具体的例子来说明。现在,我要从互联网上某个地方随机找出一个代码片段,然后进行重写。
左边的代码是原来的。右边的代码是我重写的。
我重写的代码片段是由 StackOverflow 上一位名叫“Mark”的人发布的。他的代码通过对每一个点与它的左右邻居进行平均,使折线图中参差不齐的高峰和低谷变得平滑。
概括地说,在重写代码中,我做了以下的改动:
为变量重新命名,这样更容易显示它们的用途;
使用更加具体的方式来声明变量;
把某些代码重构为一个单独的函数,来描述其用途;
删除另外一个第三方库(HighCharts)中的一些数据结构。
把空白的地方改了改。
我编写代码的风格并不比 Mark 的好。它只不过是碰巧以正确的方式触动了我的大脑。这并不足以让 Mark 的代码发生改变。下面是重写他的代码片段的真实益处:
我学会了这个算法。我现在完全明白这个折线图平滑的工作原理了。
我创建的代码对我而言更易于维护,即使对 Mark 来说并不是这样。
我可以把那些和我需求无关的部分代码删除掉。我确认了代码没有任何错误,比如,无限循环。
我已经确认了没有引入安全漏洞,比如在 DOM 中注入一个 querystring 变量。
我已经确认了,这些代码没有添加任何额外的依赖关系,比如某些统计库。
我了解到另外一位工程师很享受编写代码的乐趣,并且有机会把他的实践为我自己所用。
所以,这种浅重写是一种很好的方式,可以把别人的代码导入到你的项目中。有些问题是可以避免的。你可以根据你的用例和其他需求对代码进行调整。另外,你还可以在学习新的算法和实践中,成长为一名工程师。
重写的许可考虑因素
笔者作为非律师,是如何做的:
我想,完全的逐行重写应该可以免除几乎所有开源软件许可的法律义务。但是我始终坚持着开源的理念,例如,在源码的注释中引用原作,或者为原项目提供帮助。
更深入的重写和修剪
有时候,你希望导入多个文档或数千行的源码,并做大量的改动,以让新代码适合你的项目。尤其是,修剪掉你实际上不需要的东西,是很好的做法。
下面是一个简单的复制和修剪的方法:
将所有的依赖源文件复制到你的项目中。
确保你所需的功能能够在实践中起作用。单元测试一般都是非常有用的。
把你不需要的部分删除。
重复第 2 和第 3 步,直至剩余的代码能够符合你的需求。
至少对剩余的代码进行一次浅重写,这样才能保证你能够了解并从中得到其他的益处。
你的 IDE 的选择和配置应该能够很好地支持你完成这项任务,包括提示、语法高亮和通知功能,这些功能可以向你显示:
哪些代码在调用函数。(修剪它们?)
哪些函数从未被调用。(修剪它们!)
哪些变化会破坏你的构建。(取消修剪!)
如果你的 IDE 没有为你对这些进行适配,可以花点时间去做更好的设置。
git commit 和 git checkout 可以让你设定一个很好的状态,在你因过度修剪而破坏一些东西的时候,能够恢复到之前的状态。这是一张很好的安全网,可以使你的工作速度更快。
我都能听见你想说什么了……
“可是,要重写代码的话,实在是太费事了!”
我不是说你一定要重写。只有在一些情况下,如果你这么做,就能得到很好的回报。我来告诉你一个真实的案例,我重写了一个第三方项目,并且从中获益良多。
我先从依赖树开始,如下所示:
在花了半天时间完成重写之后,我去掉了 5 个依赖关系的需求,最后得到了如下结果:
有一个名为“microphone-stream”的 NPM 包,我在 Web 应用中使用它来发送从麦克风捕获的样本缓冲区到语音识别包(Cieran O'Reilly 的 vosk-browser,如果你有兴趣的话)中的一个接口。
我最初是在“让它工作”的开发阶段使用 microphone-stream。它包含在一个示例 Web 应用项目中,我已经将其复制到我的项目中。microphone-stream 运行得很好,直到我升级了一个构建工具(Webpack),这破坏了 readable-stream 的构建,这是一个更高级别的依赖包。我研究了对这两个第三方库中任何一个可能的 PR 修复。不过,向仓库提交一个好的修改需要花费好几天时间。我由于种种理由而拒绝了其他的变通方案。
通过查看 microphone-stream 的代码,我意识到我并不需要该库的核心功能:一个 Node.js 风格的流接口。因此,我认真地重写了那些我真正需要的那部分代码,把那些我不想要的东西删除掉。
一路走来,我在源码中发现了这样的宝藏:
还有这个:
我很感激地将这些想法合并到我重写的代码中。
作者 Nathan Friedly 在这样的源码注释中阐述了他的思考过程。也许他拯救了我,可能让我以后不用再找漏洞来修复了。从这个角度来看,重写比从头开始编写要好。你可以“捕捉”到别人来之不易的知识。
因此,对我来说,这是一个明显的案例,重写,而非重用,可以节约我的时间,并且让我得到更好的结果。重写也比从头开始编写要好,因为这样可以让我了解其他工程师的真实经验,否则我可能会错过这些经验。
复制和重写——试试吧!
这是好东西。这算不上作弊。
只要遵循开源许可的条款,和你的工程师伙伴成为好邻居吧。
你不希望复制或重写所有的东西。但是要学习辨别哪些情况值得你这么做。
并享受与其他人工作中的联系。那些数以百万计的项目,都是由伟大的头脑构建的。
作者介绍:
Erik Hermansen,博主,撰写关于工程、技术,以及人机共同构成的系统的文章。
原文链接:
https://levelup.gitconnected.com/copying-other-peoples-code-is-very-cool-717e8a72aa3b
活动推荐:
2023年9月3-5日,「QCon全球软件开发大会·北京站」 将在北京•富力万丽酒店举办。此次大会以「启航·AIGC软件工程变革」为主题,策划了大前端融合提效、大模型应用落地、面向 AI 的存储、AIGC 浪潮下的研发效能提升、LLMOps、异构算力、微服务架构治理、业务安全技术、构建未来软件的编程语言、FinOps 等近30个精彩专题。咨询购票可联系票务经理 18514549229(微信同手机号)。
评论