Thoughtworks 徐昊:程序员究竟是搞技术的,还是做工程的?

本文由极客时间整理自 Thoughtworks 全球技术策略顾问、中国区 CTO 徐昊在直播中的演讲《程序员究竟是搞技术的,还是做工程的?》

作者|徐昊

编辑|李辰洋

在我们软件行业里,很多人非常关注自己个人的技术水平:Java 语言出了新版本,我不会;Java 里有个 Kotlin,我不会用;JavaScript 上新框架的版本更新很快,我跟不上;等等。

它们的出现经常会给我们带来一些焦虑。面对这些新技术,要怎么办?学不会就要被淘汰吗?这是大家常有的困惑。而实际上,这种困惑背后的核心问题是:程序员究竟是搞技术的,还是做工程的?这是我们在思考自身职业定位时,要首先去思考和回答的一个问题。

应该知道,技术能力和工程能力是同等重要的,但工程能力却是我们长期忽略和欠缺的。比如我很少听到有人说,自己做软件的工程能力有一点薄弱:

我不知道怎么去做软件的发布和部署;

我不知道怎么在 2-3 人的团队里去 Setup 一个有效的部署流水线;

我不知道怎么在某些环境下完成对软件的测试。

而这,恰恰是我们行业现在所面临的一个重要问题。我们盲目地以为只要技术水平好了,职业发展就能很好。但实际上真正应该提到的,或者说跟职业发展密切相关的,首先是我们的工程能力。

当工程水平得到提升后,哪怕技术水平很有限,也能在工程能力的范围下得到很好的发挥。那么到底什么是技术能力,什么又是工程能力呢?

技术能力还是工程能力?

举两个例子,你可以感受一下其中的明显差异。

一个例子是在 LeetCode 上刷题。最近行业里还挺流行干这事儿的,不停地刷题可以让你对数据结构算法更熟悉,也能更熟练地用语言去解决一些小的技术问题。

另一个例子是在项目组里写 CRUD。如果从难度的角度判断,我想你肯定会觉得刷题对于技术的要求更高,而在项目中写 CRUD 的技术含量并不高。

没错,因为它们要求的能力完全不同。你试着回想一下,在 LeetCode 上刷题时,通常会希望自己能在限定时间内尽可能快地、一次性地把代码写完。也就是说,代码写完就完成了,很少需要在此基础上进行二次开发或反复修改。以写为主,而改为辅。

相反,在项目组做 CRUD 时,从技术上看,我们就是在做 CRUD。但与此同时,还需要理解“我为什么要做 CRUD”。这就牵扯到应该如何理解业务上下文和业务逻辑等问题。

随着业务的发展,很少会出现一次性的、代码写完就完成了的情况。因而在项目组做 CRUD 时,写代码可能仅仅占据整个代码生命周期的 5%。剩下的 95%,都是根据需求变化和功能调整,在 CRUD 的基础上再不断迭代。

可以说,从刷题中获得的技术能力,哪怕再强,一旦放到项目组中实际应用,至多能把效率提高 10 倍。假设原来是 5%,提高 10 倍就是 0.5%。即便如此,仍然有 95% 需要在代码的基础上反复修改调整。而这些能力,从刷题中是学不到的。

从而发现,在项目组中做 CRUD 其实更需要我们的工程能力,而在 LeetCode 上刷题,需要的仅仅是技术能力。

再来讲一个比较极端的情况。假设在项目中出现“需求不会做”的情况,此时你面临着两个选择:一是马上向别人求助;二是先自己死磕,实在不行了再找人帮忙。我看弹幕里大多数人选择的是后者。

先说我的结论。如果在我的团队里,我希望你能选择前者。事实上,无论我是 TL 还是团队中的其他人,都希望你能选择前者。选择前者,代表你的技术能力可能较弱,完成不了需求,但却有着非常强的工程能力。

因为工程能力强的第一步,就是不要成为别人的绊脚石。

从工程角度讲,保证项目在低风险的情况下进行,是一个极高的能力优先级。如果存在不能实现的需求,你可以说“这个地方我不会,谁能帮帮我?”这是一个非常好的工程实践。不要因为面子不想让别人知道自己不会,就选择先憋一憋。这样做,只会浪费团队的时间,给团队带来风险。

当然你也可能会说,向别人求助,难道不会浪费别人的时间吗?我要说的是:这不是由你决定的。作为项目的 TL 或者 PM,他会统一全篇的安排,看看是让你继续憋一憋,还是交由其他人来做。如果其他人也不会,可能就需要去请外脑。

从项目风险的角度讲,我们需要通过传递真实的信息来尽可能地降低项目风险,在团队中构建一种信任关系。你说自己能做完就真的能做完,而不会出现“你说自己能做完,其他人觉得你大概率做不完”的情况。

从个人的工程能力讲,构建一种信任关系,不去成为别人的绊脚石,这是程序员最重要的素养。但是在实际工作中,我们很少考虑这样的问题。当然,我们举的是一个极端的例子,目的是帮助你理解,当我们强调技术和工程能力时,能够极端到何种情况。

到这里,我可以将工程能力总结为:在团队协作环境下,长期稳定输出,并持续提高水平的能力。接下来我展开讲解一下。

何为工程能力?

如前所述,工程能力的前提是在团队协作的环境中,意思是这个项目不是你一个人扛了就算。

我们在学校里自学编程时,永远会假设需要我一个人从头干到尾。假设你是项目中最强的 TL,是项目整体的负责人,那么的确需要这样一个人来兜底。但在实际工作中,绝大部分人都是在团队协作的环境下工作,因而就要求我们有“协作”的行为。换句话说,工程能力要求我们怎样才能变成一个更好的 Team  Player。

工程能力的第二个要求是长期稳定。这与我们在 LeetCode 上刷题不同。在实际工作中,用多快的速度写完代码其实没那么重要,毕竟未来还需要修改。所以更重要的是,我们要写出更好改、更好读、更容易懂的代码。

不需要在短时间内输出一定数量的代码,比如不需要在 1 小时内就写出 700 行代码。更需要的是保持每天两百行的输出水平,就足够好了。长期来看,随着需求的变化,如果能始终保持这样的输出速率,才是我们真正希望程序员能产生的稳定输出的水平。

最后是持续提高水平。协作能力更注重长期持续输出、持续学习与持续提高,它甚至直接决定了我们是否能够具有良好的工程能力。

那么以此为前提,我要强调的是,TDD(Test-Driven Development,测试驱动开发)是目前最具工程效能的开发流程。接下来,就看看 TDD 是怎么帮助我们在团队协作的环境下,完成稳定输出并持续提高的。

最具工程效能?

首先,在团队协作的环境下,做 TDD 时需要先对需求进行任务拆解。换句话说,当任务拆分完成后,实际上就向团队中的所有人表明了我们是怎么理解需求的,将计划怎么去实现它。

这种表明方法并不是“把大象关进冰箱需要三步”这样简单。而是需要把每一步理解到位,把任务切实有效地转化成一个或多个测试。如果能转化成测试,说明真的理解了需求。反之,则说明理解不到位。

实际上,不光我理解了需求,也向其他人证明我的确理解了这个需求,同时还有测试的支撑。这是在团队协作中证明你是一个靠谱员工的基本点。

其次是长期稳定的输出。TDD 之所以能保证长期稳定的输出,是因为需要我们将测试和代码放在一起去写。为什么这么说呢?

估计你听过“测试就是文档”,这句话并不完全对。测试天然不是文档,而只是记录我们实现软件的过程。当实现过程经过加工与整理之后,才能变成真正有效的文档。所以说测试提供了大量技术,比我们在代码中写注释或在文档中写编码设计,更接近实际的实现情况。

而 TDD 这种自动化执行的方法,不仅可以帮助我们验证当时对于需求的理解、产生了什么样的偏差。也很容易帮助我们追溯 Bug 是怎么产生的,以及为什么会产生。在此基础之上,我们的输出才是长期稳定的。这就是我在强调的测试的两个主要目的:发现错误和定位错误。

要知道,我们的生产效率之所以变得越来越慢,之所以出现不敢改祖传代码的情况,是因为所做的修改一旦出错,就无法定位到底是因为什么出的错。

因而在构造软件的过程中,就需要其中包含一个探测技术,帮助我们定位错误到底在哪里。TDD 中由任务列表产生的测试,既是我们实现软件过程的记录,同时也可以帮助我们发现软件中存在的问题,以及问题产生的定位。

第三个要求是持续提高我们的水平。一方面,在使用 TDD 开发的过程中,我们对需求和架构会有越来越清晰的认识,而需求和架构也会直接反映在任务列表中。事实上,在任务列表中,当产生了越来越清晰的任务,越来越容易转化成测试的任务时,我们的能力本身也就提高了。

另一方面,在写测试的过程中,对于同一类型的任务,实现它的效率不仅可以变得越来越高,而且这个效率还是可以被度量的。比如我之前实现类似的任务时,需要一天的时间。但随着时间的发展,随着技术越来越熟练、认知越来越高,需要的时间就越来越短。因为 TDD 可以帮助我们框定一个大的度量范围。

TDD 的整体工作流程

明确了这些,接下来我们就来进一步探讨 TDD 的工作流程。如下图所示,是我在课程中主要使用的一张图。粗看上去,可能跟你理解的 TDD 有很大的偏差(注:图片来自极客时间专栏《徐昊·TDD 项目实战 70 讲》)。

首先来看 TDD 的完整工作流程。比如有一组需求,我们需要把需求分解成功能点。从用户可感知的角度,或者是从大的功能块上进行分解时,这些功能点可以直接转化成测试。不过功能点直接转化成测试比较麻烦,那么我就需要在功能点内进行进一步的上下文划分。我们把它叫做功能上下文。

功能上下文的划分怎么来的呢?最简单的方法就是从架构上来,因为架构是系统中存在多少种组件,以及组件与组件之间交互的一种方式。比如在 MVC 框架的场景下组件就有三种:Model、View 和 Controller。假如我想实现一个用户登录的功能,从功能点上分,就有两个大的功能点:用户输入正确密码可以登录、用户输入错误密码不让登录。

当我们想实现第一个大的功能点时,就需要做一个用户登录的 View、一个 Controller 和一个代表用户的 Model。那么在输入正确密码可以登录的上下文中,不同的组件可以分解成更细小的功能的任务项。

比如说,在 View 菜单上正确登录一个功能时,上面有一项是“我当前的登录界面可以显示用户名密码”的提示,还有第二项是“当用户名输密码时应该被原码覆盖”,等等,会有一些具体的任务项。

很多人在学 TDD 时有一个很大的疑惑:不知道测试从哪儿来。因为当我们看 Kent Beck 的书时,会觉得他天马行空,好像随意写写就出现了很多代码。这背后其实是有很多考量的。

他的考量就是把需求先分解成功能点,由功能点再沿着我们对架构的理解,分解成功能上下文。然后再从上下文分解成具体的任务项,由任务项去写测试。这才是更完整的 TDD 的流程,而测试也是从这儿来的。

同样的问题还有重构。一个很容易跟它混淆的概念是重写,就是拿到代码后,在这上面直接重写一遍。但重构是有一个严格的定义。重构讲的是,我希望它的功能不变,结构变得更好。也就是代码功能本身不变,但是需要让代码结构变得更好。这意味着什么呢?意味着重构调整的是架构。

仔细看图,红 / 绿 / 重构循环到底是如何循环起来的呢?简单来说,就是需求分解成功能点,架构和组件之间的关系在功能点映射成功能上下文,并在功能上下文中分解成任务项,任务项再转成测试。然后通过一个或多个测试的失败,直到让测试通过。经过重构,我们就能重新梳理系统中的组件和架构。而重新梳理的组件和架构,会影响功能上下文的再次分解。

换句话讲,当你在 TDD 中去做一次重构,很大可能是当你下一次再用做开发时,对于任务的拆解会发生改变,所以它才能形成一个完整的循环。

分析到这里,就有一个很有意思的发现。TDD 实际上并不是一种编码技术。因为它并不能驱动我们在任务项中把功能都实现出来,它真正驱动的是架构。这正是我们讲的编码架构师,是真正的实干型而非 PPT 型架构师。

那么为什么讲 TDD 是一个工程化的开发过程呢?要知道,架构(组件与组件间的关系)需要在团队间共享。换句话讲,我们可以从中观察一个架构师是不是把工作做好了。

我在做咨询时经常会进行类似的尝试。我会将团队里的开发工程师分别叫到不同的房间,让他们对将要迭代的功能进行任务分解。最后发现,五个人分解出了五种不同的样子。实际上就表明这个团队的架构师没有做任何工作。

架构是整个软件开发过程中最奇怪的一类产出,它本身不发挥任何价值,而只能指导别人的工作。我们只有理解了架构是什么,才能写出正确的架构和正确的代码。当我们说这个架构产生了,如果只存在于纸上,那它不会发挥任何作用。或者说这可能是一个非常容易被破坏的架构愿景。只有架构进入到每个人的头脑中,指导工程师进行具体工作的时候,它才能够发挥作用。

如果所有人根据架构组件拆分出的功能点和功能上下文都是一致的,意味着架构愿景得到了一个比较好的规划,那么剩下的事情才会变得比较简单。

所以在团队中推行 TDD 失败,从来都不是大家不会先写测试,不会进入红 / 绿循环造成的。而是因为没有有效地维护架构愿景,不知道应该按照什么样的方式进行需求分解。

可以说,在做任何软件开发时,理解需求、懂得架构,都是我们开始的前提和出发点。TDD 就是以这种形式告诉我们,必须以一种能被消费、能看得见摸得着的方式向别人展示“我真的懂了需求”。而我真的懂了需求,是因为我可以把需求分解成功能点。我真的懂了架构,是因为我可以在功能点内对架构进行上下文的切分。

如果做到了这些,那么在后续的软件开发中,可能 70% 的问题就都不存在了。TDD 仅仅提供了效率而已。

但是恰恰相反,在我们软件行业,大家非常关注自己个人的技术水平,而不强调工程实践的能力。所以我们行业里普遍缺乏这两种能力:

给我一个需求,我能够恰如其分地分解成对应的功能点;

给我一个架构愿景,我可以把功能点切分成对应的功能上下文。

怎么获得这些能力呢?最简单的办法就是!你可以把 TDD 看成一种训练手段。当你在每一次实践中去强调 TDD,那么最终会变成一个更好的程序员,因为你一直在锤炼程序员工程化水平的最核心的能力。

所以通过 TDD,哪怕我们完全不考虑自动化测试的部分,完全不考虑自动化测试带来的优势,仅仅强调从需求分解和架构愿景上去理解需求,将其变成可验收的任务,我们都能在软件开发效率上得到巨大的飞跃和提高。这就是我为什么讲,TDD 仍然是目前最具有工程效能的开发过程。

不过,TDD 也是目前最难掌握的工程化方法。

最难掌握

对于 TDD,不仅个人学起来困难,在团队中推行也很困难。拿我的学习经历来举例。我从小学四年级就开始参加各种算法竞赛,可以说,我不缺技术水平,也不觉得 TDD 会给我带来什么额外的好处。然而我在 2002 年刚接触 TDD 时,很快就发现它的确能帮助我更有效率地、更有把握地实现工程化的代码。

在自学 TDD 时,我把 Kent Beck 的书看了很多遍。当看到第 30 遍时,突然发现书里有一个非常小的列表,一直在说明下一步要做什么。虽然书中有一个章节都在讲任务列表,但在很长一段时间里,我都没有将任务列表与测试直接关联在一起。

后来再回过头思考,才发现原来 TDD 是先产生一个任务列表,而我可以在任务列表上再进一步分解,分解成能够被测试的任务。我花了很长时间来琢磨这件事。同样,这也是很多同学在学习 TDD 时遇到的一个大的困难点。

另外一个挣扎的事情就是,使用 TDD 开发的程序员或多或少都有自己的风格和习惯,而别人的习惯和方法并不完全适用于我们。

TDD 可以被看作是一种编程习惯或者编程方法。就像大家都在跑步,但每个人的摆臂、抬腿动作却不太一样。TDD 也是如此。所以当我去实际使用 TDD 时,对于 Kent Beck 个人的方法,比如三角法,会觉得好像没有什么必要。

于是我强迫自己用 TDD 来编写所有的程序。不光用 TDD 写过应用类的项目,还写过编译器。经过一年多的训练,我才觉得差不多掌握了 TDD。所以它的确是非常难掌握的一种开发方法。

在这近二十年里,我一直尝试通过引入一些实践来降低 TDD 的学习门槛。在课程最后,我也会介绍如果结合了架构、需求分解和测试策略,该怎样利用一些工具帮助我们在团队中更有效地推行 TDD。

如果十年前你问我 TDD 的流程是什么,那给的图肯定跟这个完全不同,当时的图中是没有架构愿景的。所以这个课是我最新尝试的一个总结,希望各位同学能为我提供一些建议,这也是我开设这门课的目的。

小    结

当思考职业发展时,我建议不要把眼睛仅仅盯在技术能力上。比如 AI,需要追吗?除非你想立志成为 AI 工程师,否则更需要问的是:AI 何时会工程化?当 AI 工程化后,将会以何种形式与软件工程发生关系?当 AI 进入软件行业,我们做事的方法和风格会发生何种改变?

应该知道,去规划自己的职业生涯时,技术能力和工程能力是同等重要的,但工程能力却是我们长期忽视的和欠缺的。从根本上讲,就是不光要注重自己的技术能力,同时也要注重自己的工程能力。那么 TDD 是我认为目前效能最高的工程化的开发方法,当然,它也是难以掌握的。

本文文字及图片出自 新浪网

余下全文(1/3)
分享这篇文章:

请关注我们:

发表评论

邮箱地址不会被公开。 必填项已用*标注