我构建大型技术项目的方法
原文由 Mitchell Hashimoto 于 发布,订阅该博客
无论是从零开始搭建新项目、实现一个大型功能,还是着手一次大规模重构,想要保持动力并完成大型技术项目往往都不容易。对我非常有效的一个方法是:持续看到实实在在的成果,并以此来安排工作的先后顺序。
我们都体会过启动新项目时的那种兴奋感。头几周恨不得立刻坐在电脑前开干。随后慢慢地,你开始分心、找借口,花在上面的时间越来越少。如果这是正式工作,你只能硬着头皮艰难地拖到终点,每一天都很煎熬。如果只是出于兴趣,多年后再回首,你只会感叹本可以做成的事却没能实现。
我发现,当我把大任务拆成一块一块、每一块都能带来切实可见的进展时,我往往更能完成工作,并在整个项目过程中保持热情。每个人被激励和驱动的方式都不同,所以这套方法未必适合你,但就普遍情况而言,我还没见过哪个工程师不会为一场精彩的演示而兴奋。而目标就是:始终给自己一场精彩的演示。
我并不声称这篇文章里的任何观点是新颖独创的。它无疑与许多众所周知的软件工程或管理实践有相通之处。我只是想分享自己处理大型技术工作的方式,以及为何会这样做。
在本文中,我会以我的终端模拟器项目为例贯穿始终,这样就能分享真实、具体的经验。本来还有很多别的项目可以举例,但我选择这个,是因为它与我的本职工作无关,而且时间较近、记忆犹新。
我想非常明确地说,我绝不是在指责那些没有完成项目的人。只要你乐在其中、感到有成就感(或者根本不在乎),那就很好,祝你做得更好。这篇博文面向的是那些更想把项目做完的人,或是单纯想了解我是如何努力把项目做完的人。
起点
一开始,你面对一个庞大的项目,必须弄清楚从何下手。对我来说,这是最难的部分,我可能会花上几个小时——有时甚至几天——来回摇摆,纠结于正确的起点。
以我的终端模拟器为例,我知道如果想完成这个项目,有若干大型组件必不可少:终端解析、Shell 进程的运行与管理、字体渲染、网格渲染、输入处理(键盘/鼠标)等等。通往“完成”的路上,有数百个相对庞大的子项目。
如果我一开始的目标就是做出一个能跑 Neovim 的可启动终端,那麻烦就大了。即便抛开未知的未知不谈,这个目标听起来就太大了。我能直观地意识到,这条路上有太多组件:GUI 渲染、进程启动、终端解析与状态管理。这是个糟糕的目标,太大了,我很可能一两个月后就失去兴趣。
相反,我会去思考什么样的项目是现实可行的、能让我尽快看到成果的。一旦用这个标准来筛选,可行的子项目数量就会急剧减少。比如:
- VT 解析——解析终端转义序列
- 空白窗口渲染——打开一个窗口并绘制一块空白画布
- 子进程启动——启动 bash、zsh、fish 等子 Shell,配置好 TTY 并能读取它的输出(也就是最初的 Shell 提示符)
在这个阶段,我不会去罗列所有大型子项目。我只是大致把握项目的轮廓,然后找出一个可以独立构建、并且能切实看到某种成果的子项目。
这是经验最能发挥作用的阶段。经验更丰富的工程师往往能更有效地描绘出项目的轮廓,更准确地识别各个子组件,并看清它们如何拼合在一起。经验不足时,或是在不熟悉的领域,我就只能尽力猜测,并预料到有更大可能在某个时刻推倒重来。
早期成果
早期的工作往往不太直观可见,这让看到切实的成果显得困难。比如,如果我为终端选择先做 VT 解析,不接上某种 UI 就没法看到它是否在工作。再比如,在另一个项目中如果先做数据库表结构和最小化 API,同样也得写个客户端,配上 CLI 或 GUI,才能看到成果。
当然,如果你一开始选的子项目就是 UI,那很快就能看到成果!出于各种原因,我很少先从前端开始,通常先做后端。而无论如何,你最终都会做到后端,遇到类似的挑战。
度过这个阶段的最佳工具是自动化测试(在这个阶段通常是单元测试)。自动化测试让你能够真正运行代码、看到它在工作,同时也有助于保持良好的工程习惯。
这也为挑选最初几个任务提供了另一个指引:如果不是图形界面相关的工作,就要选那些无需太多折腾就能测试的东西,这样才能看到成果。
对我的终端项目,我决定先从 VT 解析入手,因为当时这是终端中我不太了解的部分,而且感觉非常容易测试:给它一段示例输入字符串,期望得到某个解析后的动作或事件作为输出。
看到“1 项测试通过”“4 项测试通过”“13 项测试通过”这样不断递增的过程,对我来说非常令人兴奋。我在运行自己写的代码,而且它跑通了。我知道自己正在推进大项目中某个关键子组件的进展。
冲刺演示
我做早期子项目的目标,不是打造一个已完成的子组件,而是做出一个足够好用的子组件,好让我能继续推进到通往演示的下一步。✨
这种权衡不仅仅体现在功能上,也可能体现在算法或设计层面。比如,你可能知道未来需要用上真正的数据库、复杂的数据结构或支持流式数据。但对于最初的一批工作,你完全可以只用内存数据、字典这类内置数据结构,并要求所有输入/输出一次性给出。
我认为这个权衡非常重要,所以我要再重复一遍:不要让完美成为进步的敌人。更进一步,不要让那些你知道将来必须做的改进,阻碍你继续推进到下一件事。目标是做出演示。
无论在做什么,我都会尝试每周做出一到两个演示,期间穿插上一节所说的自动化测试反馈。
做演示也能为你提供宝贵的产品反馈。即使功能还不完整,你也能很快直观地感受到某些东西用起来是否顺手。这些算不上“最小可行产品”,因为它们其实还不可用,但已经足够让工程师进行有价值的自我反思。
这是我认为经验反而会起反作用的地方。我见过资深工程师陷在打造完美事物的过程中,等到做出演示时才发现这东西很烂。烂的不是实现,而是产品或功能本身就很烂。
回想一下,对于终端项目,我选择的第一个任务是 VT 解析。在早期阶段,我只通过自动化测试来看到成果。为了做出第一个演示,我写了一个 Shell 脚本,它会运行某个命令、捕获其输出、喂给我的 VT 解析器,然后输出所有解析到(或未能解析)的结果。久而久之,我把这个 CLI 当作我的第一个“UI”来不断迭代——我会用 ASCII 来渲染终端网格。
这给了我巨大的满足感,因为我可以运行 man 或 ls 这类简单程序,或是 vim 这类更复杂的程序,来观察我的解析器是正常工作还是崩溃了——而后者在某种意义上同样令人兴奋。
在这种情况下,我写的这个 CLI 从长远看几乎毫无用处(我很快就把它扔掉了)。但花上一两天把它当作演示做出来,却给了我重要的进展感,而亲眼看到东西跑起来,也帮助我保持了动力。
为自己而构建
这一节更多适用于个人项目,而非工作指派的项目。即使你期望为他人发布软件,也要按需只构建你需要的部分,并且尽快用上自己的软件。
我在处理自己亲身遇到的问题时总是更有动力1。而且,如果一个为你自己设计的产品连你自己都觉得不好用,那它很可能对别人来说也不好用。因此,我从演示到真正可用的产品的路径,就是找到一条最短路径,只构建我认为自己需要的功能。
对我的终端来说,这意味着首先要能加载我的 Shell 配置(fish),并在此基础上能够启动和使用 Neovim。于是我让所有工作都直奔实现这些所需的功能:只实现那些程序用到的转义序列,只渲染我日常使用的字体,等等。我最初省略的功能示例包括:滚动、鼠标选中、搜索、标签页/分屏等。
然后我开始把自己的终端当作主力终端日常使用。这一步通常会有几次反复;你会发现自己其实需要某个被省略或遗忘的功能。在我最初试用自己的终端时,我发现方向键毫无反应,还存在一些微妙(但会打断工作流)的渲染 bug 等等。于是我又暂时放弃使用它,但这也为我提供了下一步要处理的具体任务。
此外,能够使用包含自己所写代码的软件,总让我感到非常自豪,这通常也有助于激励我继续做下去。
总结
把大问题分解成小问题。重要的是,每个小问题都必须有某种能让你清晰看到工作成果的方式。
对小问题只需解决到足以推进大问题的演示环节的程度,然后就转向下一个小问题。
只需解决足够多的小问题,让你能够开始构建可运行的软件演示,然后继续迭代更多功能。尽可能频繁地做演示。
如果适用(个人项目、解决你自身实际问题的公司项目等),优先实现那些能让你用上自己软件的功能。然后继续优先解决你自己的问题。
根据后续改进的需要,回过头来迭代各个组件,并按需重复这一过程。
结语
差不多就是这样了。我在个人项目、团队项目、工作项目、学校项目等各种项目中都遵循这一大致模式,它也是我保持动力的方式2。
注意,我有很多东西没提!我没有谈发布。我知道很多人觉得发布很有激励作用。我认为一个项目不一定要发布才算成功。对我而言,发布是太过重大的事件,无法长期激励我。我也没有谈工具(Git 工作流、CI 等)。我在多份工作中都使用这套方法,并让它适配既定的任何流程。诸如此类。
我想这也恰恰说明,这是一套多么个人化的流程。我想每个人都需要找到某种能以健康方式强化自身动力的方法。我意识到看到成果能极大地激励我,于是围绕这一点构建了自己的工作方式,到目前为止效果一直很好。
脚注
随机一篇博客
评论
登录后参与讨论