2

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

 2 years ago
source link: https://www.techug.com/post/xuhao-are-programmers-technical-or-engineering/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

2

本文由极客时间整理自 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 是我认为目前效能最高的工程化的开发方法,当然,它也是难以掌握的。

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK