在成为高效程序员的路上


在成为高效程序员的路上

一、前言

一个好的程序员的工作效率是普通程序员的10倍。你要有一个认识:我们所遇到的问题,大多不是程序问题

软件行业里有一本名著叫《人月神话》,其中提到两个非常重要的概念:本质复杂度(Essential Complexity)和偶然复杂度(Accident Complexity)。大部分程序员忙碌解决的问题,都不是程序问题,而是由偶然复杂度导致的问题 。

我们该如何减少偶然复杂度引发的问题,让软件开发工作有序、高效地进行?

(1)你需要一个思考框架

  • Where are we?(我们现在在哪?) 现状
  • Where are we going?(我们要到哪儿去?) 目标
  • How can we get there?(我们如何到达那里?) 实现路径

(2)你需要四个思考原则

以终为始任务分解沟通反馈自动化

二、以终为始

(1)遇到事情,倒着想

任何事物都要经过两次创造:一次是在头脑中的创造,也就是智力上的或者第一次创造(Mental/First Creation),然后才是付诸实践,也就是实际的构建或第二次创造(Physical/Second Creation)

对做软件的人来说,我们应该把“终”定位成做一个对用户有价值的软件。践行“以终为始”就是在做事之前,先考虑结果,根据结果来确定要做的事情

(2)在做任何事之前,先定义完成的标准

DoD(Definition of Done,完成的定义)

  • DoD 是一个清单,清单是由一个个的检查项组成的,用来检查我们的工作完成情况。

  • DoD 的检查项应该是实际可检查的。

  • DoD 是团队成员间彼此汇报的一种机制。 当我们有了 DoD,做事只有两种状态,即“做完”和“没做
    完”。

站在 DoD 的肩膀上

  • 它不仅局限在团队内部协作上,如果你可以放开思路,会发现 DoD 的思维在工作中用途非常广泛。 .

  • DoD 是一个的思维模式,是一种尽可能消除不确定性,达成共识的方式。

(3)在做任何需求或任务之前,先定好验收标准

功能列表式的需求描述方式,将一个完整的需求敲成了碎片。

验收标准非常重要的一环是异常流程的描述。 验收标准给出了这个需求最基本的测试用例,它保证了开发人员完成需求最基本的质量。

如果你的团队采用用户故事的格式进行需求描述固然好,如果不能,在功能列表中,补充验收标准也会极大程度地改善双方协作的效率。

(4)尽早提交代码去集成

写代码是程序员的职责,但我们更有义务交付一个可运行的软件。

(5)默认所有需求都不做,直到弄清楚为什么要做这件事

我们必须要有自己的独立思考,多问几个为什么,尽可能减少掉到“坑”里之后再求救的次数 。软件开发的主流由面向确定性问题,逐渐变成了面向不确定性问题。

精益创业 : 它要解决的是面向不确定性创造新事物 ,既然是不确定的,那你唯一能做的事情就是“试”。

精益创业的方法论里,提出“开发(build)- 测量(measure)- 认知(learn)”这样一个反馈循环。

精益创业提供给我们的是一个做产品的思考框架,我们能够接触到的大多数产品都可以放在这个框架内思考。

(6)扩大自己工作的上下文,别把自己局限在一个“程序员”的角色上

“独善其身”不是好事 不同角色工作上真正的差异是上下文的不同。 虽然写的代码都一样,但你看到的是树木,人家看到的是森林,他更能从全局思考。能想到某些问题,前提就是要跳出程序员角色思维,扩大自己工作的上下文 当你对软件开发的全生命周期都有了认识之后,你看到的就不再是一个点了,而是一条线。争取在更大的上下文工作

(7)在动手做一件事之前,先推演一番

通向结果的路径才是更重要的。对比我们的工作,多数情况下,即便目标清晰,路径却是模糊的 。在军事上,人们将其称为沙盘推演,或沙盘模拟

(8)问一下自己,我的工作是不是可以用数字衡量

一些人说,自己靠直觉就能把事情做好,其实这是一种误解,因为那种所谓的直觉,通常是一种洞见(Insight),洞见很大程度上依赖于一个人在一个领域长期的沉淀和积累,而这其实是某种意义上的大数据 。而当事情复杂到一定程度时,简单地靠感觉是很难让人相信的。 从数字中发现问题,让系统更稳定。 基于数据进行技术决策、预先设定系统指标,以及发现系统中的问题

(9) 设计你的迭代 0 清单,给自己的项目做体检

这里给出的迭代 0,它的具体内容只是基本的清单

需求方面:细化过的迭代 1 需求 、用户界面和用户交互

技术方面:基本技术准备 、发布准备

把测试当作规范确定下来的办法就是把测试覆盖率加入构建脚本。

image-20200623154857109

三、任务分解

(1)动手做一个工作之前,请先对它进行任务分解

正是通过将宏大目标进行任务分解,马斯克才能将一个看似不着边际的目标向前推进。 一个大问题,我们都很难给出答案,但回答小问题却是我们擅长的。那么,用这种思路解决问题的难点是什么呢?给出一个可执行的分解 。与很多实践相反,任务分解是一个知难行易的过程。 不同的可执行定义差别在于,你是否能清楚地知道这个问题该如何解决。

如今软件行业都在提倡拥抱变化,而任务分解是我们拥抱变化的前提。

(2)多写单元测试

对于每个程序员来说,只有在开发阶段把代码和测试都写好,才有资格说,自己交付的是高质量的代码。

自动化测试:

  • 测试框架最大的价值,是把自动化测试作为一种最佳实践引入到开发过程中,使得测试
    动作可以通过标准化的手段固定下来

  • 测试模型:蛋卷与金字塔

    image-20200623160331971
  • 测试模型:测试金字塔

    image-20200623160408621

越是底层的测试,牵扯到相关内容越少,而高层测试则涉及面更广。 小事反馈周期短,而大事反馈周期长 。虽然冰淇淋蛋卷更符合直觉,但测试金字塔才是行业的最佳实践

(3)我们应该编写可测的代码

测试驱动开发(Test Driven Development)

image-20200623160703840

测试先行开发和测试驱动开发的差异就在重构上,因为重构和测试的互相配合,它会驱动着你把代码写得越来越好。这是对“驱动”一词最粗浅的理解 测试驱动设计 :由测试驱动代码的编写。 我的代码怎么写才是能测试的,也就是说,我们要编写具有可测试性的代码。 写代码之前,请先想想怎么测 。

(4)将任务拆小,越小越好

每个任务完成之后,代码都是可以提交的。

大多数程序员之所以开发效率低,很多时候是没想清楚就动手了。

很多人看了一些 TDD 的练习觉得很简单,但自己动起手来却不知道如何下手。中间就是缺了任务分解的环节。 如果你有了一个微习惯,坚持就不难了。 一个经过分解后的任务,需要关注的内容是有限的,我们就可以针对着这个任务,把方方面面的细节想得更加清晰。

(5)按照完整实现一个需求的顺序去安排分解出来的任务

很多人可能更习惯一个类一个类的写,我要说,最好按照一个需求、一个需求的过程走,这样,任务是可以随时停下来的。 检验每个任务项是否拆分到位,就是看你是否知道它应该怎么做了 每做完一个任务,代码都是可以提交的,按照完整实现一个需求的顺序去安排分解出来的任务

(6)要想写好测试,就要写简单的测试

为什么你的测试不够好呢? 主要是因为这些测试不够简单。只有将复杂的测试拆分成简单的测试,测试才有可能做好 把测试写简单,简单到一目了然,不需要证明它的正确性。 一般测试要具备的四段: 前置准备、执行、断言和清理

一段旅程(A-TRIP) (编写可测试的代码 )

  • Automatic,自动化;
  • Thorough,全面的; 应该尽可能用测试覆盖各种场景
  • Repeatable,可重复的;
  • Independent,独立的
  • Professional,专业的

(7)想要管理好需求,先把需求拆小

基本上,闯入你脑海的需求描述是主题(epic),在敏捷开发中,有人称之为主用户故事(master story)。绝大多数问题都是由于分解的粒度太大造成的,少有因为粒度太小而出问题的。评价用户故事有一个“ INVEST 原则 Independent,独立的 、Negotiable,可协商的 、Valuable,有价值的 、Estimatable,可估算的、 Small,小 、Testable,可测试的

用户故事,之所以是故事,就是要讲,要沟通。估算的结果是相对的,不是绝对精确的,我们不必像做科研一样,只要给出一个相对估算就好。一般来说,估算的过程也是大家加深对需求理解的过程

(8)尽量做最重要的事

按照时间管理的理念,重要且紧急的事情要立即做。重要但不紧急的事情应该是我们重点投入精力的地方。紧急但不重要的事情,可以委托别人做。不重要不紧急的事情,尽量少做。 如果不把精力放在重要的事情上,到最后可能都变成紧急的事情。 当员工想不明白的事,换成老板的视角就全明白了 。

(9)做好产品开发,最可行的方式是采用MVP

精益创业就是通过不断地尝试在真实世界中验证产品想法,其中一个重要的实践是最小可行产品(Minimum Viable Product,MVP)。 我们要做的是验证一个想法的可行性,甚至不是为了开发一个软件,开发软件只是一种验证手段。可行的路径不是一个模块做得有多完整,而一条用户路径是否通畅当时间有限时,我们需要学会找到一条可行的路径,在完整用户体验和完整系统之间,找到一个平衡

(10)面对不了解的技术,我该如何分解任务?

答案很简单,先把它变成你熟悉的技术。一旦变成了你熟悉的技术,你就可以应用在这个模块中学到的,面对确定性技术的分解方案。 怎么把它变成你熟悉的技术 ,做一次技术 Spike。 Spike 的作用就在于消除不确定性,让项目经 理知道这里要用到一项全团队没有人懂的技术,需要花时间弄清楚。大概了解这项技术是干什么的了,我们要进行技术 Spike 的任务分解 技术最终是要应用到项目中的,本着“以终为始”的原则,我们就应该奔着结果做,整个的Spike 都应该围绕着最终的目标做。很多程序员见到新技术都容易很兴奋,会把所有的文档通读一遍。如果是技术学习,这种做法无可厚非,但我们的目标是做 Spike,快速地试,没有那么多时间,必须一切围绕结果来。 其目的就是为了防止发散。当时间有限时,我们只能做最重要的事。当你确定要使用这项技术时,请丢弃掉你的原型代码

(11)项目时间紧,该怎么办?

假设现在不忙了,你知道该怎么改进吗?

改进过程:

把测试覆盖率检查加入到工程里,得到现有的测试覆盖率。
将测试覆盖率加入持续集成,设定当前测试覆盖率为初始值。测试覆盖率不达标,不许提交代码。
每周将测试覆盖率调高,比如,5% 或 10%,直到测试覆盖率达到 100%。

SpringBoot项目中简单易用的测试覆盖率检查工具:jacoco

(12)多个功能同时开发,怎么办?

四、沟通反馈

(1)为什么世界和你的理解不一样

《大富翁》里的沙隆巴斯有句口头禅:人生不如意,十之八九!

我们努力地学习各种知识,为的就是更好地理解这个世界的运作方式,而沟通反馈,就是我们与真实世界互动的最好方式。

香农信息论中的一个通信模型

几个要素 :

  • 信源(Information Source),它负责产生信息(Message)
  • 发送器(Transmitter),它会对信息进行某些操作,也就是对信息编码,产生信号(Signal)
  • 信道(Channel),它是信号传送的媒介
  • 接收器(Receiver),它是对信号执行发送器的逆操作,解码信号,提取出信息
  • 信宿(Destination),它负责接收信息。
  • 噪声(Noise),指的是削弱信号的东西

理解偏差是怎么产生的?

项目经理给你传输的信号是“完成一个需求”,在项目经理脑子中,这个信号的原始信息可能是这样的:编写完成这个功能所需的代码,然后为这段代码写好自动化测试,再将它与现有系统集成好,通过测试人员的验证。你从“完成一个需求”这个信号中解码出来的信息却是:把功能代码写完。这样,问题就出现了。即便这里忽略了噪声的干扰,当编码和解码不是一个版本的时候,无论如何,项目经理的信息都很难准确地传达到你这里。

**信息的传达要经过编码和解码两个过程,无论是编码出现问题,还是解码出现问题,都会造成信息的不准确。因为每个人经历见识的差异,造成了各自编解码器的差异。 **很多程序员讲东西的通病:讲东西直奔细节,而忽略背景

编解码器算法,也就是怎么协调沟通双方更有效地进行沟通。 通过制定“完成的定义”就可以帮助改善这个过程。这就相当于,沟通的双方都有了一个编解码手册。 通过沟通反馈,不断升级自己的编解码能力

(2)你的代码为谁而写

image-20200909203113799

深入剖析怎样才能写出可维护的代码,这件事就是命名

计算机科学中只有两大难题:缓存失效和命名。—— Phil Karlton

名字起得是否够好,一个简单的评判标准是,拿着代码给人讲,你需要额外解释多少东西。

任何人都能写出计算机能够理解的代码,只有好程序员才能写出人能够理解的代码。—— Martin Fowler

我们写代码的目的是与人沟通,因为我们要在一个团队里与人协同工作。人要负责将业务问题和机器执行连接起来,缺少了业务背景是不可能写出好代码的。 虽然只是一个简单的名字修改,但从理解上,这是一步巨大的跨越,缩短了其他人理解这段代码所需填补的鸿沟,工作效率自然会得到提高。 如果了解领域驱动设计(Domain Driven Design,DDD),把不同的概念分解出来,这其实是限界上下文(Bounded Context)的作用,而在代码里尽可能使用业务语言,这是通用语言(Ubiquitous Language)的作用 。

(3)轻量级沟通:你总是在开会吗

开会是为了解决问题,但真实情况却是开了会又没有解决多少问题,这真是一个奇特的矛盾。 凡是效果特别好的会议,基本上都是用来做信息同步的。 开会是一种重量级的沟通,几乎是我们日常工作中最重的。它有很强的仪式感,所以,大家相对来说会很重视。而且会议通常会牵扯到很多人,尤其是与这个事情相关度不那么高的人。所以,改善会议的第一个行动项是,减少参与讨论的人数。 相比于会议的形式,面对面沟通因为注意力有限,参与的人数不可能太多。也因为参与的人数相对少一些,每个人的投入也会更多一些。 我们的第二个行动项是,如果你要讨论,找人面对面沟通。

有一种实践叫站会(Standup)

只说三件事:

我昨天做了什么?是为了与其他人同步进展,看事情是否在计划上。一旦偏离计划,请主动
把它提出,这样,项目经理可以过问,因为这会涉及到是否要调整项目计划;
我今天打算做什么?是同步你接下来的工作安排。如果涉及到与其他人协作,也就是告诉大
家,让他们有个配合的心理准备;
我在过程中遇到了什么问题,需要请求帮助。 就是与其他人的协作,表示:我遇到不懂的问题,你们有信息的话,可以给我提供一下

亚马逊的员工会议——拒绝使用 PPT

(4)可视化:一种更为直观的沟通方式

作为一个程序员,在这个技术快速发展的时代,我们唯有不断学习,才能保证自己不为时代所抛弃。那你是怎么跟上技术发展步伐的呢?

ThoughtWorks 技术雷达 是由 ThoughtWorks 技术咨询委员会(Technology AdvisoryBoard)编写的一份技术趋势报告,每 6 个月发布一次。ThoughtWorks 的项目多样性足够丰富,所以它能够发现诸多技术趋势。因此,相比于行业中其它的预测报告,技术雷达更加具体,更具可操作性。

image-20200909214536448

技术雷达用来追踪技术,在雷达图的术语里,每一项技术表示为一个 blip,也就是雷达上的一个光点。然后用两个分类元素组织这些 blip:象限(quadrant)和圆环(ring),其中,象限表示一个 blip 的种类,目前有四个种类:技术、平台、工具,还有语言与框架。 圆环表示技术一个 blip 在技术采纳生命周期中所处的阶段,目前这个生命周期包含四个阶段:采用(Adopt)、试验(Trial)、评估(Assess)和暂缓(Hold)。

“采用”表示强烈推荐,我会去对比一下自己在实际应用中是否用到了,比如,在 2018 年11 月的技术雷达中,事件风暴(Event Storming)放到了“采用”中,如果你还不了解事件风暴 是什么,强烈建议你点击链接了解一下。

“暂缓” 则表示新项目别再用这项技术了,这会给我提个醒,这项技术可能已经有了更优秀的替代品,比如,Java 世界中最常见的构建工具 Maven 很早就放到了“暂缓”项中,但时至今日,很多人启动新项目依然会选择 Maven,多半这些人并不了解技术趋势。

雷达图是一种很好的将知识分类组织的形式,它可以让你一目了然地看到并了解所有知识点,并根据自己的需要,决定是否深入了解。

读书雷达

构建技术雷达

构建雷达的程序库

就人脑的进化而言,处理图像的速度远远快于处理文字 , 将“可视化”应用在工作中的典型案例:看板。看板,是一种项目管理工具,它将我们正在进行的工作变得可视化。

image-20200909215830120

(5)快速反馈:为什么你们公司总是做不好持续集成?

你对持续集成的第一印象是什么?
持续集成?Jenkins?没错,很多人对持续集成第一印象都是持续集成服务器,也就是 CI服务器,当年是 CruiseControl,今天换成了 Jenkins。 很多人就把 CI 服务器理解成了持续集成。我就曾经接触过这样的团队,
他们恨不得把所有的事情都放在 CI 服务器上做:在 CI 服务器上做了编译,跑了代码检查,运行了单元测试,做了测试覆盖率的统计等等。 这种做法是可行的,但它不是最佳实践。我希望你去思考,有没有比这更好的做法呢?

想要做好持续集成,就需要顺应持续集成的本质:尽快得到工作反馈。

持续集成的两个重要目标:

  • 怎样快速地得到反馈
  • 以及什么样的反馈是有效的

执行同样的操作,本地环境会快于 CI 服务器环境。

image-20200910183940158 image-20200910184136058

只有 CI 服务器处于绿色的状态才能提交代码。 所有不能把检查只放到 CI 服务器上执行 。用好本地构建脚本(build script),保证各种各样的检查都可以在本地环境执行

image-20200910184611849

我们一般通过CI监视器来获取有效反馈CI 监视器的示例 projectmonitor
怎么呈现是有效的? 答案很简单:怎么引人注目,怎么呈现
持续集成的纪律:CI 服务器一旦检查出错,要立即修复

image-20200910184930328

(6)开发中的问题一再出现,应该怎么办?

如果在开发过程中,同样的问题反复出现,说明你的团队没有做好复盘。

定期复盘,找准问题根因,不断改善。

什么是复盘?

复盘,原本是一个围棋术语,就是对弈者下完一盘棋之后,重新把对弈过程摆一遍,看看哪些地方下得好,哪些下得不好,哪些地方可以有不同甚至是更好的下法等等。这种把过程还原,进行研讨与分析的方式,就是复盘。

什么是客体化?

为什么复盘这么好用呢?在我看来有一个重要的原因,在于客体化。 用别人的视角看问题,这就是客体化。

回顾会议是一个常见的复盘实践,定期回顾是一个团队自我改善的前提。

先在白板上给出一个主题分类。我常用的是分成三类:“做得好的、做得欠佳的、问题或建议”。 还有不同的主题分类方式,比如海星图,分成了五大类:“继续保持、开始做、停止做、多做一些、少做一些”五类。 给与会者五分钟时间,针对这个开发周期内团队的表现,按照分类在便签上写下一些事实。比如,你认为做得好的是按时交付了,做得不好的是 Bug 太多。 两个重点。一个是写事实,不要写感受。 另外,每张便签只写一条,因为后面我要对便签归类。

所有给出的行动项应该都是可检查的,而不是一些无法验证的内容。 列好了一个个的行动项,接下来就是找责任人了,责任人要对行动项负责。

5个为什么来解决复盘问题

“5 个为什么”中的“5”只是一个参考数字,不是目标。

举个例子。服务器经常返回 504,那我们可以采用“5 个为什么”的方式来问一下。

为什么会出现 504 呢?因为服务器处理时间比较长,超时了。
为什么会超时呢?因为服务器查询后面的 Redis 卡住了。
为什么访问 Redis 会卡住呢?因为另外一个更新 Redis 的服务删除了大批量的数据,然后,重新插入,服务器阻塞了。
为什么它要大批量的删除数据重新插入呢?因为更新算法设计得不合理。
为什么一个设计得不合理的算法就能上线呢?因为这个设计没有按照流程进行评审。

(7)作为程序员,你也应该聆听用户声音

怎么判断产品经理说的产品特性是不是用户真的需要的呢? 采用用户视角,你需要来自真实世界的反馈。而作为一个程序员,欠缺用户视角,在与产品经理的交流中,你是不可能有机会的,因为他很容易用一句话就把你打败:“这就是用户需求。” 由听产品经理的话,扩大成倾听用户的声音。 不断使用自家的产品,会让你多出一个用户的视角。

(8)尽早暴露问题: 为什么被指责的总是你?

发现问题的是不要选择了继续埋头苦干,直到经理询问,才把问题暴露出来不是所有的问题,都是值得解决的技术难题 遇到问题,最好的解决方案是尽早把问题暴露出来。 写程序有一个重要的原则叫 Fail Fast 意思就是如果遇到问题,尽早报错。 例如:参数校验、配置参数等第一时间抛出异常

(9)结构化:写文档也是一种学习方式

很多人回避写文档的真正原因是,他掌握的内容不能很好地结构化。 将零散的知识结构化,有很多种方式,但输出是非常关键的一环。 输出的过程,本质上就是把知识连接起来的过程。 多输出,让知识更有结构。

金字塔理论

(10)持续集成,一条贯穿诸多实践的主线

单元测试做不好,是否会影响到 CI 的效果?

持续集成的价值在于,它是一条主线,可以将诸多实践贯穿起来。也就是说,想要真正意义上做好持续集成,需要把周边的很多实践都要做好。做好持续集成的关键是,快速反馈。

比如,我们想要做好 CI,需要有一个稳定的开发分支,所以,最好采用主开发分支的方式。想用好主分支开发,最好能够频繁提交;而频繁提交需要你的任务足够小,能够快速完成;将任务拆解的足够小,需要你真正懂得任务分解。要想在一个分支上开发多个功能,那就需要用 Feature Toggle 或者 Branch by Abstraction。

image-20200912142101091

在这条线上,你有很多机会走错路。比如,你选择了分支开发模式,合并速度就不会太快,一旦反馈快不了,CI 的作用就会降低;再者,如果不能频繁提交,每次合并代码的周期就会变长,一旦合并代码的周期变长,人们就会倾向于少做麻烦事,也就会进一步降低提交的频率,恶性循环就此开启。同样,即便你懂得了前面的道理,不懂任务分解,想频繁提交,也是心有余而力不足的。而多功能并行开发,则会让你情不自禁地想考虑使用多分支模型。

想做好 CI,首先要有可检查的东西,什么是可检查的东西,最简单的就是编译、代码风格检查,这些检查可以无条件加入构建脚本。但更重要的检查,应该来自于测试,而要想做好 CI,我们要有测试防护网。

image-20200912142115793

什么叫测试防护网呢?就是你的测试要能给你提供一个足够安全的保障,这也就意味着你要有足够多的测试。换个更技术点的术语来说,就是要有足够高的测试覆盖率。

如果测试覆盖率不够,即便提交了代码,CI 都通过了,你对自己的代码依然是没有信心的,这就会降低 CI 在你的心中的地位。如果想有足够高的测试覆盖率,你就要多写单元测试。我们在前面讲过测试金字塔了,上层测试因为很麻烦,你不会写太多,而且很多边界条件,通过上层测试是覆盖不到的,所以,测试覆盖率在经过了初期的快速提升后,到后期无论如何是提上不去的。要想提升测试覆盖率,唯有多写单元测试。所以,CI 作为一个单独的实践,本身是很简单的,但它可以成为提纲挈领的主线,帮助团队不断改善自己的开发过程。

五、自动化

(1)“懒惰”应该是所有程序员的骄傲

Larry Wall 一个经典叙述:优秀程序员应该有三大美德:懒惰、急躁和傲慢(Laziness, Impatience and hubris)。

做有价值的事是重要的,这里面的有价值,不仅仅是“做”了什么,通过“不做”节省时间和成本也是有价值的。NIH 综合症(Not Invented Here Syndrome)。就是有人特别看不上别人做的东西,非要自己做出一套来,原因只是因为那个东西不是我做的,可能存在各种问题。你的日常工作是给别人打造自动化,但你自己的工作够自动化吗? 你要懂得软件设计,大多数人都是混淆了设计和实现。**在软件开发中,其它的东西都是易变的,唯有设计的可变性是你可以控制的。**

(2)一个好的项目自动化应该是什么样子的?

这里我采用的 Java 技术中最为常见的 Spring Boot 作为基础框架,而构建工具,我选择了 Gradle

我们先来了解一点 Gradle 的配置文件,它也是我们做项目自动化的重点。(分模块构建项目)

  • build.gradle,它是 Gradle 的配置文件。因为 Gradle 是由 Groovy 编写而成,build.gradle 本质上就是一个 Groovy 的脚本,其中的配置就是 Groovy 代码,这也是 Gradle 能够灵活订制的基础。
  • settings.gradle,这也是一个 Gradle 配置文件,用以支持多模块。如果说一个项目的每个模块都可以有一个 build.gradle,那整个项目只有一个 settings.gradle。

在自动化过程中,一个最基本的工作是检查。检查的工作在我们的项目中通过一个 check 任务来执行。

  • 最基本的代码风格检查要放在构建脚本中,你只要应用 Checkstyle 插件即可。可以做一些订制,比如,指定某些文件可以不做检查。
  • 测试覆盖率也应该加入到构建脚本中,只要应用 JaCoCo 插件即可。可以做一些订制,比如,生成结果的 HTML 报表,还有可以忽略某些文件不做检查。

我还提到了数据库迁移,也就是怎样修改数据库。我选择的数据库迁移工具是Flyway

做好了最基本的检查,数据库也准备就绪,接下来,我们就应该构建我们的应用了

一个最基本的项目自动化,包括了:

  • 生成 IDE 工程;
  • 编译;
  • 打包;
  • 运行测试;
  • 代码风格检查;
  • 测试覆盖率;
  • 数据库迁移;
  • 运行应用。

(3)程序员怎么学习运维知识?

有体系地学习运维知识

image-20200912145306066

(4)持续交付:有持续集成就够了吗?

将部署纳入开发的考量。

开发过程的自动化,将我们的程序打成发布包;然后讲了部署过程的自动化,通过各种工具将发布包部署起来。在每次开发完之后,将程序打包部署到环境中。开发完就自动打包,然后自动部署,听起来很像持续集成是不是?

让持续交付这个概念广为人知的是一本书,Jez Humble 和 Dave Farley 的《持续交付》(Continuous Delivery)。

什么是持续交付?简言之,它就是一种让软件随时处于可以部署到生产环境的能力。从一个打好的发布包到部署到生产环境可用,这中间还差了什么呢?那就是验证发布包,部署到环境中。持续集成阶段验证的包,往往缺少了环境的支持。持续交付中一个需要关注的点:环境。一般来说,在构建持续交付的基础设施时,会有下面几个不同的环境。

  • 持续集成环境,持续集成是持续交付的前提,这个过程主要是执行基本的检查,打出一个可以发布的包。
  • 测试环境(Test),这个环境往往是单机的,主要负责功能验证,这里运行的测试基本上都是验收测试级别的,而一般把单元测试和集成测试等执行比较快的测试放到持续集成环境中执行。
  • 预生产环境(Staging),这个环境通常与生产环境配置是相同的,比如,负载均衡,集群之类的都要有,只是机器数量上会少一些,主要负责验证部署环境,比如,可以用来发现由多机并发带来的一些问题。
  • 生产环境(Production),这就是真实的线上环境了
image-20200923112822495 image-20200923112834001

每个环境的作用是有差异的,所以,通常不会将所有的验证放在一起执行,而是要分阶段的去执行,一个阶段不通过,是不能进入下一阶段的,这种按照不同阶段组织构建的方式,称之为构建流水线(Build Pipeline)。一旦通过了各种验证,就会到构建流水线的最后一个阶段,生产发布。通常来说,生产发布这个过程不是自动化的。我们说,持续交付的关注点在于,让软件具备随时可以发布的能力,但并不等于它要立刻上线,所以,最后这一下,还要由人来决定,到底是不是要上线。如果把由人决定的是否上线变成自动化的,就成了另外一个实践:持续部署。但通常人们都会比较谨慎,最后这一下还是由人拍板比较稳妥,所以,持续交付是现在的主流。

image-20200923112852488

DevOps 是一种软件交付的理念和方法,目的是增强软件的可靠性。如果这些配置管理工具还需要有一台具体的机器去部署,放在持续交付中,也只能扮演一个部署环境的次要角色,那 Docker 的出现则彻底地改变最终交付物。结合着这些工具,我们的生成产物就由一个发布包变成了一个 Docker 镜像。

image-20200923112909814

Jez Humble 写《持续交付》时就已经想到如此完整的一个体系,受限于当时的环境,书中介绍的自动化还比较宽泛,不像今天有更加完善的工具支撑。虽然当时他对持续交付的理解已经到达如此高度,他所在的团队也做出了一个颇具先锋气质的持续交付工具,但是受限于产品推广策略,这个工具并没有成为主流,即便后来开源了。(如果你想了解一下这个工具,可以点击链接去查看)

(5)如何做好验收测试?

将验收测试自动化。

我们在前面的内容中只说了该检查,但怎么检查呢?这就轮到测试发挥作用了。

在“任务分解”的模块,我给你完整地介绍了一下开发者测试的概念,但在那个部分讲解的测试基本上还停留在单元测试和集成测试的范畴。对于整个应用该怎么测,我们并没有仔细讨论。

今天我们就来说说应用测试的话题:验收测试。

验收测试(Acceptance Testing),是确认应用是否满足设计规范的测试。这种测试往往是站在用户的角度,看整个应用能否满足业务需求。验收测试从各自为战的混乱中逐渐有了体系的是行为驱动开发(Behavior Driven Development)这个概念的诞生,也就是很多人知道的 BDD。行为驱动开发中的行为,指的是业务行为。BDD 希望促进业务人员与开发团队之间的协作,换句话说,如果你想做 BDD,就应该用业务语言进行描述。

BDD 是 2003 年由 Dan North 提出了来的。Dan North 不仅仅提出了概念,为了践行他的想法,他还创造了第一个 BDD 的框架:JBehave。后来又改写出基于 Ruby 的版本 RBehave,这个项目后来被并到 RSpec 中。

今天最流行的 BDD 框架应该是 Cucumber,它的作者就是 RSpec 的作者之一 Aslak Hellesøy。

想写好 BDD 的测试用例,关键点在用业务视角描述。验收测试(Acceptance Testing),是确认应用是否满足设计规范的测试。验收测试是技术交付必经的环节,只不过,各个团队实践水平有所差异,有的靠人工,有的用简单自动化,一些做得比较好的团队才有完善的自动化。逐渐形成了一个完整的自动化验收测试的体系。行为驱动开发(Behavior Driven Development,BDD)是一种自动化验收测试的方式。用业务的视角描述测试用例。在编写步骤定义时,还要考虑设计自己的业务测试模型。验收测试的方法不止 BDD 一种,像实例化需求(Specification by Example,SbE)也是一种常见的方法。验收测试框架也不止 BDD 框架一类

(6)你的代码是怎么变混乱的?

对程序员最好的惩罚是让他维护自己三个月前写的代码。这样的问题有解吗?一个解决方案自然就是我们前面说过的重构,但重构的前提是,你得知道代码驶向何方。对于这个问题,更好的答案是,你需要了解一些软件设计的知识。提到软件设计,大部分程序员都知道一个说法“高内聚、低耦合”

人们尝试着用各种方法拆解这个高远的目标,而比较能落地的一种做法就是 Robert Martin 提出的面向对象设计原则:SOLID

  • 单一职责原则(Single responsibility principle,SRP)
  • 开放封闭原则(Open–closed principle,OCP)
  • Liskov 替换原则(Liskov substitution principle,LSP)
  • 接口隔离原则(Interface segregation principle,ISP)
  • 依赖倒置原则(Dependency inversion principle,DIP)

如果说设计模式是“术”,设计原则才是“道”。设计模式并不能帮你建立起知识体系,而设计原则可以。道和术,是每个程序员都要有的功夫,在“术”上下过功夫,才会知道“道”的价值,“道”可以帮你建立更完整的知识体系,不必在“术”的低层次上不断徘徊。

单一职责原则的定义修改“一个模块应该仅对一类 actor 负责”,这里的 actor 可以理解为对系统有共同需求的人。

举个例子:在一个工资管理系统中,有个 Employee 类,它里面有三个方法:

  • calculatePay(),计算工资,这是财务部门关心的。
  • reportHours(),统计工作时长,这是人力部门关心的。
  • save(),保存数据,这是技术部门关心的。

之所以三个方法在一个类里面,因为它们的某些行为是类似的,比如计算工资和统计工作时长都需要计算正常工作时间,为了避免重复,团队引入了新的方法:regularHours()。

image-20200923133932458

接下来,财务部门要修改正常工作时间的统计方法,但人力部门不需要修改。负责修改的程序员只看到了 calculatePay() 调用了 regularHours(),他完成了他的工作,财务部门验收通过。但上线运行之后,人力部门产生了错误的报表。

这是一个真实的案例,最终因为这个错误,给公司造成了数百万的损失。

上面这个问题,Robert Martin 给了一个解决方案,就是按照不同的 actor 将类分解,我把分解的结果的类图附在了下面:

image-20200923133953093

编写短函数,你能看到一个模块在为多少 actor 服务,就完全取决于你的分解能力了。总之,你看到的粒度越细,也就越能发现问题。

(7)总是在说MVC分层架构,但你真的理解分层吗?

构建好你的领域模型。

作为程序员,你一定听说过分层,比如,最常见的 Java 服务端应用的三层结构

  • 数据访问层,按照传统的说法,叫 DAO(Data Access Object,数据访问对象),按照领域驱动开发的术语,称之为 Repository;
  • 服务层,提供应用服务;
  • 资源层,提供对外访问的资源,采用传统做法就是 Controller。

那为什么要分层呢?原因很简单,当代码复杂到一定程度,人们维护代码的难度就急剧上升。一旦出现任何问题,在所有一切都混在一起的代码中定位问题,本质上就是一个“大海捞针”的活。分层架构,实际上,就是一种在设计上的分解。在软件开发中,分层几乎是无处不在的,因为好的分层往往需要有好的抽象。分层真正的价值:构建一个良好的抽象。这种构建良好的抽象在软件开发中随处可见,比如,你作为一个程序员,每天写着在 CPU 上运行的代码,但你读过指令集吗?你之所以可以不去了解,是因为已经有编译器做好了分层,让你可以只用它们构建出的“抽象”。有抽象有发展。例如:Node.js 的出现让 JavaScript 成为了一个更好的抽象。

理解了分层实际上是在构建抽象,你或许会关心,我该怎么把它运用在自己的工作中。三层架构中的(资源层、数据访问层)两层重要性都不是那么高,那重要的是什么?答案便呼之欲出了,没错,就是剩下的部分,我们习惯上称之为服务层,但这个名字其实不能很好地反映它的作用,更恰当的说法应该可以叫领域模型(Domain Model)。

为什么叫“服务层”不是一个好的说法呢?这里会遗漏领域模型中一个重要的组成部分:领域对象。你的领域层只依赖于你的领域对象,第三方发过来的内容先做一次转换,转换成你的领域对象。这种做法称为防腐层。

当我们把领域模型看成了整个设计的核心,看待其他层的视角也会随之转变,它们只不过是适配到不同地方的一种方式而已,而这种理念的推广,就是一些人在说的六边形架构。怎么设计好领域模型是一个庞大的主题,推荐你去了解一下领域驱动设计(Domain Driven Design,DDD)

image-20200923135643192

(8)为什么总有人觉得5万块钱可以做一个淘宝?

用简单技术解决问题,直到问题变复杂。

今天,我们从软件行业的一个段子说起。甲方想要做个电商网站,作为乙方的程序员问:“你要做个什么样的呢?”甲方说:“像淘宝那样就好。”程序员问:“那你打算出多少钱?”甲方想了想,“5 万块钱差不多了吧!”

为什么在甲方看来并不复杂的系统,你却觉得困难重重呢?因为你们想的根本不是一个东西。在客户看来,我要的不就是一个能买东西的网站吗?只要能上线商品,用户能看到能购买不就好了,5 万块钱差不多了。而你脑中想的却是,“淘宝啊,那得是多大的技术挑战啊,每年一到‘双 11’,那就得考虑各种并发抢购。淘宝得有多少程序员,5 万块你就想做一个,门都没有。”

既然说到了淘宝,2013 年,子柳出版了一本《淘宝技术这十年》,这本书里讲述了淘宝是怎么一步步变化的。按照书中的说法,第一个淘宝是“买来的”,买的是一个叫做 PHPAuction 的系统,即便选择了最高配,也才花了 2000 美元左右。这是一个采用 LAMP 架构的系统,也就是 Linux + Apache + MySQL + PHP,这在当年可是典型的开源架构。团队所做的主要就是一些订制化工作,最大的调整就是将单一数据库的读写进行了拆分,变成了一个主库和两个从库。这种结构在今天来看,依然是很多团队做调整的首选。

image-20200923140248803

当访问量和数据量不断提升,MySQL 数据库率先抗不住了。当年的 MySQL 默认采用的是 MyISAM 引擎,写数据的时候会锁住表,读也会被卡住,当然,这只是诸多问题中的一个。

2003 年底,团队将 MySQL 换成了 Oracle。由于 Oracle 的性能要好上许多,主从的数据库架构又改回了单一数据库。但由于 PHP 访问数据库的缺省方案没有连接池,只好找了开源的 SQL Relay,这也为后续的改进埋下了伏笔。

image-20200923140321500

当数据量继续加大,本地存储就已经无法满足了,只能通过引入网络存储解决问题。数据量进一步增大之后,存储节点一拆再拆,依然不能解决问题,淘宝就踏上了购买小型机的道路。IBM 的小型机、Oracle 的数据库和 EMC 的存储,这个阶段就踏上了 IOE 之路。2004 年初,SQL Relay 已经成了一个挥之不去的痛点,于是,只能从更根本的方案上动脑筋:更换程序设计语言。作为当时的主流,Java 成了不二之选。

替换的方案就是给业务分模块,一块一块地替换。老模块只维护,不增加新功能,新功能只在新模块开发,新老模块共用数据库。新功能上线,则关闭老模块对应功能,所有功能替换完毕,则老模块下线。淘宝的数据量继续增长,单台 Oracle 很快到了上限,团队采用了今天常见的“分库分表”模式,但“分库分表”就会带来新的问题,跨数据库的数据怎么整合?于是,打造出了一个 DBRoute,用以处理分库的数据。

但是,这种做法也带来了一个新的问题,同时连接多个数据库,任何一个数据库出了问题,都会导致整个网站的故障。当淘宝的数据量再次增长,每次访问都到了数据库,数据库很难承受。一个解决方案就是引入缓存和 CDN(Content Delivery Network,内容分发网络),这样,只读数据的压力就从数据库解放了出来。

当时的缓存系统还不像今天这么成熟,于是,团队基于一个开源项目改出了一个。他们用的 CDN 最开始是一个商用系统,但流量的增加导致这个系统也支撑不住了,只好开始搭建自己的 CDN。后来,因为 CDN 要消耗大量的服务器资源,为了降低成本,淘宝又开始研发自己的低功耗服务器。随着业务的不断发展,开发人员越来越多,系统就越来越臃肿,耦合度也逐渐提升,出错的概率也逐渐上升。这时,不得不对系统进行分解,将复用性高的模块拆分出来,比如,用户信息。

业务继续发展,拆分就从局部开始向更大规模发展,底层业务和上层流程逐渐剥离,并逐渐将所有业务都模块化。有了一个相对清晰地业务划分之后,更多的底层业务就可以应用于不同的场景,一个基础设施就此成型,新的业务就可以使用基础设施进行构建,上层业务便如雨后春笋一般蓬勃发展起来。在这个过程中,有很多技术问题在当时还没有好的解决方案,或者是不适于他们所在的场景。所以,淘宝的工程师就不得不打造自己的解决方案,比如:分布式文件系统(TFS)、缓存系统(Tair)、分布式服务框架(HSF)等等。还有一些技术探索则是为了节省成本,比如,去 IOE 和研发低功耗服务器等等。

以淘宝网站的发展为例,做了一个快速的梳理,只是为了让你了解一个系统的发展,如果你有兴趣了解更多细节,不妨自己找出这本书读读。当然,现在的淘宝肯定比这更加完整复杂。

为什么我们要了解一个系统的演化过程呢?因为作为程序员,我们需要知道自己面对的到底是一个什么样的系统。

回到我们今天的主题上,5 万块钱可以不可以做一个淘宝?答案是,取决于你要的是一个什么样的系统。最开始买来的“淘宝”甚至连 5 万块钱都不用,而今天的淘宝和那时的淘宝显然不是一个系统。从业务上说,今天的淘宝固然已经很丰富了,但最核心的业务相差并不大,无非是卖家提供商品,买家买商品。那他们的本质差别在哪呢?

每次随着业务量的增长,原有技术无法满足需要,于是,就需要用新的技术去解决这个问题。这里的关键点在于:不同的业务量。虽然在业务上看来,这些系统是一样的,但在技术上看来,在不同的阶段,一个系统面对的问题是不同的,因为它面对业务的量级是不同的。更准确地说,不同量级的系统根本就不是一个系统。

淘宝的工程师之所以要改进系统,真实的驱动力不是技术,而是不断攀升的业务量带来的问题复杂度。所以,评估系统当前所处的阶段,采用恰当的技术解决,是我们最应该考虑的问题。

我做的系统没有那么大的业务量,我还想提高技术怎么办?答案是到有好问题的地方去。现在的 IT 行业提供给程序员的机会很多,找到一个有好问题的地方并不是一件困难的事,当然,前提条件是,你自己得有解决问题的基础能力。

(9)先做好DDD再谈微服务吧,那只是一种部署形式

学习领域驱动设计

在今天做后端服务似乎有一种倾向,如果你不说自己做的是微服务,出门都不好意思和人打招呼。

我们为什么要做微服务?相对于整体服务(Monolithic)而言,微服务足够小,代码更容易理解,测试更容易,部署也更简单。

怎么划分微服务,也就是一个庞大的系统按照什么样的方式分解。那应该怎么划分微服务呢?你需要了解领域驱动设计。

领域驱动设计(Domain Driven Design,DDD)它要解决什么问题呢?就是将业务概念和业务规则转换成软件系统中概念和规则,从而降低或隐藏业务复杂性,使系统具有更好的扩展性,以应对复杂多变的现实业务问题。

现如今提起建模,第一反应依然是建数据库表。这种做法是典型的面向技术实现的做法。一旦业务发生变化,团队通常都是措手不及。DDD 到底讲了什么呢?它把你的思考起点,从技术的角度拉到了业务上。DDD 最为基础的就是通用语言(Ubiquitous Language),让业务人员和程序员说一样的语言。使用通用语言,等于把思考的层次从代码细节中拉到了业务层面。越高层的抽象越稳定,越细节的东西越容易变化。有了通用语言做基础,然后就要进入到 DDD 的实战环节了。DDD 分为战略设计(Strategic Design)和战术设计(Tactical Design)。

战略设计是高层设计,它帮我们将系统切分成不同的领域,并处理不同领域的关系。从业务上区分,把不同的概念放到不同的地方,这是从根本上解决问题,否则,无论你的代码写得再好,混乱也是不可避免的。而这种以业务的角度思考问题的方式就是 DDD 战略设计带给我的。

战术设计,通常是指在一个领域内,在技术层面上如何组织好不同的领域对象。举个例子,国内的程序员喜欢用 myBatis 做数据访问,而非 JPA,常见的理由是 JPA 在有关联的情况下,性能太差。但真正的原因是没有设计好关联。如果能够理解 DDD 中的聚合根(Aggregate Root),我们就可以找到一个合适的访问入口,而非每个人随意读取任何数据。这就是战术设计上需要考虑的问题。

微服务真正的难点并非在于技术实现,而是业务划分,而这刚好是 DDD 战略设计中限界上下文(Bounded Context)的强项。

虽然通用语言打通了业务与技术之间的壁垒,但计算机并不擅长处理模糊的人类语言,所以,通用语言必须在特定的上下文中表达,才是清晰的。而这个上下文就是限界上下文。它限定了通用语言自由使用的边界,一旦出界,含义便无法保证。正是由于边界的存在,一个限界上下文刚好可以成为一个独立的部署单元,而这个部署单元就可以成为一个服务。所以要做好微服务,第一步应该是识别限界上下文。每个限界上下文都应该是独立的,每个上下文之间就不应该存在大量的耦合,困扰很多人的微服务之间大量相互调用,本身就是一个没有划分好边界而带来的伪命题,靠技术解决业务问题,事倍功半。

有Martin Fowler 在写《企业应用架构模式》时,提出了一个分布式对象第一定律:不要分布对象。同样的话,在微服务领域也适用,想做微服务架构,首先是不要使用微服务。如果将一个整体服务贸然做成微服务,引入的复杂度会吞噬掉你以为的优势。

如果你划分出了限界上下文,不妨先按照它划分模块。一次性把边界划清楚并不是一件很容易的事。大家在一个进程里,调整起来会容易很多。然后,让不同的限界上下文先各自独立演化。等着它演化到值得独立部署了,再来考虑微服务拆分的事情。到那时,你也学到各种关于微服务的技术,也就该派上用场了!

(10)持续集成、持续交付,然后呢?

  • 软件外部的自动化——工作流程讲起,让你能够把注意力专注于写好代码;
  • 软件内部的自动化——软件设计,选择恰当的做法,不贪图一时痛快,为后续的工作挖下深坑。

问题 1:持续交付是否可以再做扩展?

别把自己局限在程序员这个单一的角色中,应该了解软件开发的全生命周期。在前面的内容中,我讲了不少做产品的方法,比如,MVP、用户测试等等。如果只把自己定位在一个写代码的角色上,了解这些内容确实意义不大,但你想把自己放在一个更大的上下文中,这些内容就是必须要了解的。如果说我们一开始把持续集成定义成编写代码这件事的完成,那持续交付就把这个“完成”向前再推进了一步,只有上线的代码才算完成。

但放在整个软件的生命周期来说,上线并不是终点。把系统送上线,不是最终目的。那最终目的是什么呢?回到思考的起点,我们为什么要做一个软件?因为我们要解决一个问题。那我们是不是真正的解决了问题呢?其实,我们还不知道。

如果是采用精益创业的模式工作,我们构建产品的目的是为了验证一个想法,而怎么才算是验证了我们的想法呢?需要搜集各种数据作为证据。所以,我曾经有过这样的想法,精益创业实际上是一种持续验证,验证想法的有效性,获得经过验证的认知(Validated Learning)。

现在有一些获取验证数据的方式,比如,AB 测试。

AB 测试是一种针对两个(或多个)变体的随机试验,常常用在 Web 或 App 的界面制作过程中,分别制作两个(或多个)版本,让两组(或多组)成分相同的用户随机访问不同版本,收集数据,用以评估哪个版本更好。每次测试时,最好只有一个变量。因为如果有多个变量,你无法确认到底是哪个变量在起作用。AB 测试的概念在其他领域由来已久。2000 年,Google 的工程师率先把它应用在了软件产品的测试中,时至今日,它已经成为很多产品团队常用的做事方式。AB 测试的前提是用户数据搜集。

在开发过程中,用数字帮助我们改善工作。在产品领域实际上更需要用数字说话,说到这里,我“插播”一个例子。

很多产品经理喜欢讲理念、讲做法,偏偏不喜欢讲数字。用数字和产品经理沟通其实是更有说服力的。我就曾经遇到过这样的事情,在一个交易平台产品中,一个产品经理创造性地想出一种新的订单类型,声称是为了方便用户,提高资金利用率。如果程序员接受这个想法,就意味着要对系统做很大的调整。

我问了他几个问题:第一,你有没有统计过系统中现有的订单类型的使用情况?第二,你有没有了解过其他平台是否支持这种订单类型呢?产品经理一下子被我问住了。

我对第一个问题的答案是,除了最基础的订单类型之外,其他的订单类型用得都很少,之前做的很多号称优化的订单类型,实际上没有几个人在用。

第二个问题我的答案是,只有极少数平台支持类似的概念。换句话说,虽然我们想得很美,但教育用户的成本会非常高,为了这个可能存在的优点,对系统做大改造,实在是一件投资大回报小的事,不值得!

再回到我们的问题上,一旦决定了要做某个产品功能,首先应该回答的是如何搜集用户数据。对于前端产品,今天已经有了大量的服务,只要在代码里嵌入一段代码,收集数据就是小事一桩。前端产品还好,因为用户行为是比较一致的,买服务就好了,能生成标准的用户行为数据。对于后端的数据,虽然也有各种服务,但基本上提供的能力都是数据的采集和展示,一些所谓的标准能力只是 CPU、内存、JVM 之类基础设施的使用情况。对于应用来说,具体什么样的数据需要搜集,还需要团队自己进行设计。说了这些,我其实想说的是,持续验证虽然是一个好的想法,但目前为止,还不如持续集成和持续交付这些已经有比较完整体系做支撑。想做到“持续”,就要做到自动化,想做到自动化,就要有标准化支撑,目前这个方面还是“八仙过海各显神通”的状态,没法上升到行业最佳实践的程度。其实道理上也很简单,从一无所有,到持续集成、再到持续交付,最后到持续验证,每过一关,就会有大多数团队掉队。所以,真正能达到持续交付的团队都少之又少,更别提要持续验证了。

问题 2:Selenium 和 Cucumber 的区别是什么?

Selenium 是一个开源项目,它的定位是浏览器自动化,主要用于 Web 应用的测试。它最早是 Jason Huggins 在 2004 年开发出来的,用以解决 Web 前端测试难的问题。

Cucumber 的兴起伴随着 Ruby on Rails 的蓬勃发展,我们在之前的内容中提到过,Ruby on Rails 是一个改变了行业认知的 Web 开发框架。所以,Cucumber 最初主要就是用在给 Web 应用写测试上,而 Selenium 刚好是用来操作浏览器的,二者一拍即合。

Cucumber 提供的是一层业务描述框架,而它需要有自己对应的步骤实现,以便能够对被测系统进行操控;而 Selenium 就是在 Web 应用测试方面实现步骤定义的一个非常好的工具。

问题 3:IntelliJ IDEA 怎么学?

一个工具怎么学?我的经验就是去用。我没有专门学过 IntelliJ IDEA,只是不断地在使用它。遇到问题就去找相应的解决方案。如果说在 IDEA 上下过功夫,应该是在快捷键上。看到很多人把快捷键运用得出神入化,那不是在写一行代码,而是在写一片代码。我当时有一种特别震惊的感觉。

(11)你真的了解重构吗?

锤炼你的重构技能。

作为程序员,我们都希望自己的代码是完美的。但没有代码是完美的,因为只要你的代码还有生命力,一定会有新的需求进来,而新的需求常常是你在编写这段代码之初始料未及的。很多人直觉的选择是,顺着既有的代码结构继续写下去,这里添一个 if,那里加一个标记位,长此以往,代码便随时间腐坏了。

如果用一个物理学术语描述这种现象,那就是“熵增”,这也就是大名鼎鼎的热力学第二定律。如果没有外部干预,系统会朝着越来越混乱的方向发展。对抗熵增的一个办法就是引入负熵,让系统变得更加有序。而在代码中引入负熵的过程就是“重构”。

重构这个说法可比“调整代码”听上去高级多了。时至今日,很多人都会把重构这个词挂在嘴边:“这个系统太乱了,需要重构一下。”但遗憾的是,很多程序员对重构的理解是错的。

你理解的重构是什么呢?就以前面那句话为例:这个系统太乱了,需要重构一下。如果我们接着问,你打算怎么重构呢?一些人就会告诉你,他们打算另立门户,重新实现这套系统。对不起,**你打算做的事叫重写(rewrite),而不是重构(refactoring)。

重构,本质上就是一个“微操作”的实践。如果你不能理解“微操作”的含义,自然是无法理解重构的真正含义,也就不能理解为什么说“大开大合”的重写并不在重构的范畴之内。“微操作”,每一步都很小,小到甚至在很多人眼里它都是微不足道的。

重构,也属于微操作的行列,与我们介绍的任务分解结合起来,你就能很好地理解那些重构手法的含义了:你需要把做的代码调整分解成若干可以单独进行的“重构”小动作,然后,一步一步完成它。

比如,服务类中有一个通用的方法,它并不适合在这个有业务含义的类里面,所以,我们打算把它挪到一个通用的类里面。你会怎么做呢?大刀阔斧的做法一定是创建一个新的通用类,然后把这个方法复制过去,修复各种编译错误。而重构的手法就会把它做一个分解:

  • 添加一个新的通用类,用以放置这个方法;
  • 在业务类中,添加一个字段,其类型是新添加的通用类;
  • 搬移实例方法,将这个方法移动到新的类里面。

得益于现在的 IDE 能力的增强,最后一步,按下快捷键,它就可以帮我们完成搬移和修改各处调用的工作。在这个分解出来的步骤里,每一步都可以很快完成,而且,每做完一步都是可以停下来的,这才是微操作真正的含义。这是大刀阔斧做法做不到的,你修改编译错误的时候,你不知道自己需要修改多少地方,什么时候是一个头。你现在理解了,重构不仅仅是一堆重构手法,更重要的是,你需要有的是“把调整代码的动作分解成一个个重构小动作”的能力。

下面我准备给你提供一张关于重构的知识地图,帮你了解它与周边诸多知识之间的关系,辅助你更好地理解重构。

学习重构,先要知道重构的定义。关于这点,Martin Fowler 给出了两个定义,一个名词和一个动词。

重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。

重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。

之所以要了解重构的定义,因为重构的知识地图就是围绕着这个定义展开的。

首先,我们要对软件的内部结构进行调整,第一个要回答的问题是,我们为什么要调整。Martin Fowler 对于这个问题的回答是:代码的坏味道。代码的坏味道,在我看来,是这本书给行业最重要的启发。很多人常常是无法嗅到代码坏味道的,因此,他们会任由代码腐坏,那种随便添加 if 或标记的做法就是嗅不出坏味道的表现。

我经常给人推荐《重构》这本书,但我也常常会补上一句,如果你实在没有时间,就去看它的第三章《代码的坏味道》。顺便说一下,对比两版的《重构》,你会发现它们在坏味道的定义上有所差异,在新版的《重构》中,可变数据(Mutable Data)、循环语句(Loops)都定义成了坏味道,如果你不曾关注近些年的编程发展趋势,这样的定义着实会让人为之震惊。但只要了解了函数式编程的趋势,就不难理解它们的由来了。函数式编程已然成为时代的主流。如果你还不了解,赶紧去了解。

六、综合运用

新入职一家公司,怎么快速进入工作状态?

了解一个项目,从大图景开始

image-20200923145907533

运用思考框架吗?我们要问三个问题:

  • Where are we?(我们现在在哪?)
  • Where are we going?(我们要到哪儿去?)
  • How can we get there?(我们如何到达那里?)

现状:如果刚刚加入一家公司,哪怕我们不是一脸懵,也只是对公司业务有一个简单地了解。

目标:一般来说,我们都是打算在新公司大展身手,但这个答案太宽泛了,我们还需要进一步细化。在这个公司长远的发展是以后的事,我们还是把第一步的目标制定成能够达到上手工作的程度,比如,能够解决日常的项目问题。

达到目标:我们需要做一个分解。回想一下过往的工作经验,要在一个项目上工作起来,先要了解什么呢?很多人的第一反应是技术,我是程序员嘛,当然就是技术优先了。估计大多数人进到项目里,都是一头奔进代码里,然后,从各种细节研究起来。技术肯定是你要了解的,但它不应该是第一位的。技术解决的是“怎么做”的问题,而我们第一个应该了解的问题是“做什么”。一个软件到底在做什么,能够回答这个问题的就是业务。所以,我们排在第一优先级的事情应该是业务。了解业务和技术都只是让你扮演好你个人的角色,但我们通常都是在一个团队内工作的,所以,还要解决好与其他人协作的问题,这就需要我们了解团队本身是如何运作的。我们将大目标做了一个分解,得到了三个小目标:业务;技术;团队运作。

业务

首先是业务。这是程序员入手新项目时最容易忽略的点。在这个专栏中,我在不同的模块中都说到了知识结构的重要性,没有结构的知识是零散的。所以,不管做任何项目,都要先从大图景入手。只有了解了大图景,各种知识才能各归其位。

对于一个普通的程序员来说,业务就是这个大图景。如果你了解了业务,你自己就可以推演出基本的代码结构。但反过来,如果让你看了代码,从中推演出业务,那几乎是不可能的。事实上,每次了解到一个业务,我都会在脑子中过一下,如果是我做这个业务,我会怎么做。这样一来,我就会先在整体上有一个预判,后面再对应到实际的代码上,就不会那么陌生了。要了解业务,这个业务是做什么的,解决什么样的问题,具体的业务流程是什么样子的,等等。

在初期的了解中,我并不会试图弄懂所有的细节,因为我的目标只是建立起一个基本的框架,有了初步的了解,后续再有问题,我就知道该从哪里问起了。了解业务时,一定要打起精神,告诉自己,这个阶段,我要了解的只是业务,千万别给我讲技术。

技术

了解完业务,就该到技术了我们先从宏观内容开始。第一个问题就是这个系统的技术栈:Java、JavaScript 还是.NET,这样,我就可以对用到的工具和框架有个大致的预期。

接下来是系统的业务架构,这个系统包含了哪些模块,与哪些外部系统有交互等等。最好能够有一张或几张图将架构展现出来。现实情况是,不少项目并没有现成的图,那就大家一起讨论,在白板上一起画一张出来,之后再来慢慢整理。

我会选择从外向内的顺序了解起。首先是外部,这里的外部包括两个部分:

  • 这个系统对外提供哪些接口,这对应着系统提供的能力;
  • 这个系统需要集成哪些外部系统,对应着它需要哪些支持。

一旦涉及到与外部打交道,就涉及到外部接口是什么样子的,比如,是用 REST 接口还是 RPC(Remote Procedure Call,远程方法调用) 调用,抑或是通过 MQ(Message queue,消息队列)传递消息。不要简单地认为所有接口都是你熟悉的,总有一些项目会采用不常见的方式,比如,我曾见过有系统用 FTP 做接口的。

所有这些都相当于信息承载方式,再进一步就是了解具体的信息是什么格式,也就是协议。今天常见的协议是 JSON 格式,或者是基于某个开源项目的二进制编码,比如:Protocol BuffersThrift 等等。一些有年头的系统可能会采用那时候流行的协议,比如:XML;有一些系统则采用自己特定领域的协议,比如,通信领域有大量 3GPP 定义的协议。

了解完外部,就该了解内部了。了解内部系统也要从业务入手,对应起来就是,这个系统由哪些模块组成,每个模块承担怎样的职责。如果系统已经是微服务,每个服务就应该是一个独立的模块。

通常这也是一个发现问题的点,很多系统的模块划分常常是职责不清的,因此会产生严重的依赖问题。在前面的内容中,我多次提到限界上下文,用限界上下文的视角衡量这些模块,通常会发现问题,这些问题可以成为后续工作改进的出发点。

业务之后是技术,对应着我需要了解分层。前面说过,分层结构反应着系统的抽象。我希望了解一个模块内部分了多少个层,每个层的职责是什么。了解了这些对系统的设计,也就对系统有了一个整体的认识。

设计之后,就到了动手的环节,但还不到写代码的时候。我会先从构建脚本开始,了解项目的常用命令。我预期从版本控制里得到的是一个可以构建成功的脚本,如果不是这样,我就知道哪里需要改进了。

最后才是代码,比如,代码的目录结构、配置文件的位置、模块在源码上的体现等等,这是程序员最熟悉的东西,我就不多说了。作为初步的接触,了解基本的东西就够了,代码是我们后期会投入大量精力的地方,不用太着急。

团队运作

最后,我们还要了解一下团队运作。同样从外部开始,这个团队有哪些外部接口,比如,需求是从哪来的,产品最终会由谁使用,团队需要向谁汇报。如果有外部客户,日常沟通是怎么安排的。

再来就是内部的活动,一方面是定期的活动,比如,站会、回顾会议、周会,这些不同活动的时间安排是怎样的;另一方面是团队的日常活动,比如,是否有每天的代码评审、是否有内部的分享机制等等。

通过了解这些内容,基本上可以大致判断出一个团队的专业程度,也可以知道自己需要帮助的时候,可以找谁帮忙,为自己更好地融入团队打下基础。

你也许会问,了解这么多东西需要很长时间吧?其实不然,因为只需要从整体上有认知,如果有人很清楚团队现状的话,你可以去请教,也许一天就够了,这也是我往往能够快速上手的原因。接下来,就该卷起袖子干活了!

面对遗留系统,你应该这样做

只要产品还在发展,系统改造就是不可避免的。改造遗留系统,前提条件是要弄清楚现状,知道系统为什么要改造,是架构有问题,还是领域模型混乱,只有知道根因,才可能有的放矢地进行改造。

改造遗留系统,我给你几个建议:

  • 构建测试防护网,保证新老模块功能一致;
  • 分成小块,逐步替换;
  • 构建好领域模型;
  • 寻找行业中关于系统构建的最新理解。

小步改造遗留系统,不要回到老路上。

比如,我们一直认为电信是一个独特的领域,与 IT 技术是完全独立的,学好 CT(Communication Technology,通信技术)就可以高枕无忧了。但随着 IT 技术的不断发展,今天的电信领域也开始打破壁垒,拥抱 IT 技术,提出了 ICT 的概念(Information and Communications Technology,信息通信技术)。

分清现象与根因

面对庞大的遗留系统,我们可以再次回到思考框架上寻找思路。

  • Where are we?(我们现在在哪?)
  • Where are we going?(我们要到哪儿去?)
  • How can we get there?(我们如何到达那里?)

第一个问题,面对遗留系统,我们的现状是什么呢?

遗留系统和烂代码到底是不是问题呢?其实并不是,它们只是现象,不是根因。在动手改动之前,我们需要先分析一下,找到问题的根因。比如,实现一个直觉上需要两天的需求,要做两周或更长时间,根因是代码耦合太严重,改动影响的地方太多;再比如,性能优化遇到瓶颈,怎么改延迟都降不下来,根因是架构设计有问题,等等。我们必须要找到问题的根源,防止自己重新走上老路。

确定方案

目标是什么。对于遗留系统而言,这个问题反而是最好回答的:重写某些代码。先尝试重构你的代码,尽可能在已有代码上做小步调整,不要走到大规模改造的路上,因为重构的成本是最低的。

怎么做?我们需要将目标分解一下。

要重写一个模块,这时你需要思考,怎么才能保证我们重写的代码和原来的代码功能上是一致的。对于这个问题,唯一靠谱的答案是测试。对两个系统运行同样的测试,如果返回的结果是一样的,我们就认为它们的功能是一样的。

不管你之前对测试是什么看法,这个时候,你都会无比希望自己已经有了大量的测试。如果没,你最好是先给这个模块补测试。因为只有当你构建起测试防护网了,后续的修改才算是走在坚实的道路上。

这本书对于遗留系统的定义在我脑中留下了深刻印象:遗留代码就是没有测试的代码。这个定义简直就是振聋发聩。按照这个标准,很多团队写出来的就是遗留代码,换言之,自己写代码就是在伤害自己。

有了测试防护网,下一个问题就是怎么去替换遗留系统,答案是分成小块,逐步替换。你看到了,这又是任务分解思想在发挥作用。淘宝将系统改造成 Java 系统的升级过程,就是将业务分成若干的小模块,每次只升级一个模块,老模块只维护,不增加新功能,新功能只在新模块开发,新老模块共用数据库。新功能上线,则关闭老模块对应功能,所有功能替换完毕,则老模块下线。

这个道理是普遍适用的,差别只是体现在模块的大小上。如果你的“小模块”是一个系统,那就部署新老两套系统,在前面的流量入口做控制,逐步把流量从老系统转到新系统上去;如果“小模块”只在代码层面,那就要有一段分发的代码,根据参数将流程转到不同的代码上去,然后,根据开发的进展,逐步减少对老代码的调用,一直到完全不依赖于老代码。

image-20200923145806642

这里还有一个小的建议,按照分模块的做法,将新代码放到新模块里,按照新的标准去写新的代码,比如,测试覆盖率要达到 100%,然后,让调用入口的地方依赖于这个新的模块。

最后,有了测试,有了替换方案,但还有一个关键问题,新代码要怎么写?

要回答这个问题,我们必须回到一开始的地方,我们为什么要做这次调整。因为这个系统已经不堪重负了,那我们新做的修改是不是一定能解决这个问题呢?答案是不好说。

很多程序员都会认为别人给留下的代码是烂摊子,但真有一个机会让你重写代码,你怎么保证不把摊子弄烂?这是很多人没有仔细思考过的问题。

如果你不去想这个问题,即便今天你重写了这段代码,明天你又会怨恨写这段代码的人没把这段代码写好,只不过,这个被抱怨的人是你自己而已。

要想代码腐化的速度不那么快,一定要在软件设计上多下功夫。一方面,建立好领域模型,另一方面,寻找行业对于系统构建的最新理解。

关于领域模型的价值,我在专栏前面已经提到过不少次了。有不少行业已经形成了自己在领域模型上的最佳实践,比如,电商领域,你可以作为参考,这样可以节省很多探索的成本。

我们稍微展开说说后面一点,“寻找行业中的最新理解”。简言之,我们需要知道现在行业已经发展到什么水平了。

比如说,今天做一个大访问量的系统,我们要用缓存系统,要用 CDN,而不是把所有流量都直接转给数据库。而这么做的前提是,内存成本已经大幅度降低,缓存系统才成为了标准配置。拜 REST 所赐,行业对于 HTTP 的理解已经大踏步地向前迈进,CDN 才有了巨大的进步空间。

而今天的缓存系统已经不再是简单的大 Map,有一些实现得比较好的缓存系统可以支持很多不同的数据结构,甚至支持复杂的查询。从某种程度上讲,它们已经变成了一个性能更好的“数据库”。

有了这些理解,做技术选型时,你就可以根据自己系统的特点,选择适合的技术,而不是以昨天的技术解决今天的问题,造成的结果就是,代码写出来就是过时的。改造遗留系统,一个关键点就是,不要回到老路上。

我们应该如何保持竞争力?

在学习区工作和成长。

我们的焦虑来自于对未来的不确定性,而这种不确定性是一个特定时代加上特定行业的产物。

如何破解焦虑:成为 T 型人什么叫 T 型人?简言之,一专多能。

image-20200925151220038

有了“一专”,“多能”才是有意义的,否则,就是低水平重复,而这正是很多人职业生涯不见起色的真正原因。这里的“专”不是熟练,而是深入。其实,很多人的焦虑就源自目标太低,找不到前进的动力。给自己定下一个可以长期努力的目标,走在职业的道路上才不致于很快丧失动力。

“如果让你在一次技术大会上做分享,你会讲什么呢?”我真正的问题是,以行业标准衡量,你觉得你在哪个方面是专家呢?

怎么达到目标。既然要朝着行业中的专家方向努力,那你就得知道行业中的专家是什么样。我的一个建议是,向行业中的大师学习。读大师写的书,读书的一个好处在于,你的视野会打开,不会把目标放在“用别人已经打造好的工具做一个特定的需求”,虽然这可能是你的必经之路,但那只是沿途的风景,而不是目标。怎么才能让自己的水平不断提高呢?我的答案是,找一个好问题去解决,解决了一个好的问题能够让你的水平快速得到提升。什么是好问题?就是比你当前能力略高一点的问题,

一个“学习区”模型
  • 最内层是舒适区(Comfort Zone),置身其中会让人感觉良好,但也会因为没有挑战,成长甚微,你可以把它理解成做你最熟悉的事情。
  • 最外层是恐慌区(Panic Zone),这是压力极大的地方,完全超出了你的能力范围,你在其中只会感到无比的焦虑。
  • 中间的是学习区(Learning Zone),事情有难度,又刚好是你努力一下可以完成的,这才是成长最快的区域。

如何在实际工作中推行新观念?

问题 1:想要推行 DDD,阻力很大怎么办?

找到愿意和你一起改变的人,做一件具体的事。

问题 2:测试怎么写?

外部系统对你来说,应该只是一个接口。如果有任何外部系统,都要设计防腐层,用接口做隔离。能模拟的就模拟,能本地的就本地。关于外部系统的测试,你可以先通过接口隔离开来,然后,通过模拟服务或本地可控的方式进行测试。

重新审视“最佳实践”

产品

做产品,很多时候是面向不确定性解决问题。目前这方面最好的实践是“精益创业”。对于精益创业的最简单的理解就是“试”。试也有试的方法,精益创业提出了一个“开发(build)- 测量(measure)- 认知(learning)”这样的反馈循环,通过这个循环得到经过验证的认知(Validated Learning)。既然是对不确定产品特性的尝试,最好的办法就是低成本地试。在精益创业中,最小可行产品(MVP)就是低成本的试法。最小可行产品,就是“刚刚好”满足用户需求的产品。理解这个说法的关键在于用最小的代价,尝试可行的路径。在产品的打磨过程中,可以采用用户测试的方式,直接观察用户对产品的使用。作为程序员,我们要尽可能吃自家的狗粮,即便你做的产品不是给自己使用的产品,也可以努力走近用户。

需求

当我们确定做一个产品功能时,怎么描述需求也是很重要的。产品列表式的需求描述方式最容易出现问题的地方在于,看不清需求的全貌。用户故事是一个好的需求描述方式:作为一个什么角色,要做什么样的事,以便达成一种怎样的效果。

在用户故事中,验收标准是非常重要的一环。即便不是采用用户故事描述需求,也依然建议先将验收标准定义清楚。

开发团队对需求的理解普遍偏大,基本上都是一个主题。在开发之前,先将需求拆分成小粒度的。衡量一个用户故事拆分是否恰当,一个标准是 INVEST 原则。有了拆分出来的用户故事,就可以进行估算了,估算的过程也是对需求加深理解的过程,过大的用户故事应该再次拆分。当我们有了拆分之后的需求,就可以对需求进行进行优先级讨论了。先做重要性高的事,而不是一股脑地去做所有的需求。只有分清了需求的优先级,才能方便地对需求进行管理。

持续集成

在开发中,写出代码并不是终点,我们要把代码集成起来。集成要经常做,改动量越小,集成就可以做得越频繁,频繁到每次提交都去集成,这就是持续集成。

持续集成发展到今天已经是一套完整的开发实践。想要做好持续集成,你需要记住持续集成的关键是“快速反馈”。

  • 怎样快速得到反馈。
  • 怎样反馈是有效的。

持续集成,可以继续延展,将生产部署也纳入其中,这就是持续交付。如果持续交付,再向前一步,就可以同产品验证结合起来。持续交付的关键点,是在不同的环境验证发布包和自动化部署。不同的环境组成了持续交付的构建流水线,而自动化部署主要是 DevOps 发挥能力的地方。持续交付的发展,让交付物从一个简单的发布包变成了一个拥有完整环境的 Docker 镜像。持续集成和持续交付可以将诸多的实践贯穿起来:单元测试、软件设计、任务分解、主分支开发、DevOps 等等。所以,如果一个公司希望做过程改进,持续集成是一个好的出发点。

测试

测试是一个典型的程序员误区,很多程序员误以为测试只是测试人员的事。理解了软件变更成本,知道了内建质量之后,我们就应该清楚,测试应该体现在全部的开发环节中。这一思想在开发中的体现就是自动化测试。

想要写好自动化测试,需要先理解测试金字塔,不同的测试运行成本不同。为了让软件拥有更多的、覆盖面更广的测试,需要多写单元测试。编写测试的方式有很多,一种实践是测试驱动开发(TDD)。先写测试,然后写代码,最后重构,这就是 TDD 的节奏:红——绿——重构。测试驱动开发的本质是测试驱动设计,所以,编写可测试的代码是前提。

要想做好 TDD,一个重要的前提是任务分解,分解到非常小的微操作。学会任务分解,是成为优秀程序员的前提条件。想写好测试,需要懂得好测试是什么样子的,避免测试的坏味道。好测试有一个衡量标准:A-TRIP。我们不只要写好单元测试,还要站在应用的角度写测试,这就是验收测试。验收测试现在比较成体系的做法是行为驱动开发(BDD),它让你可以用业务的语言描述测试。

编码与设计

编码和设计,是软件开发中最重要的一环。在我看来,编码和设计是一体,想清楚才能写出好代码。很多程序员追求写好代码,却没有一个很好的标准去衡量代码的好坏。结合着软件设计的一些理念,我给你一个编写好代码的进步阶梯,希望你能达到用业务语言编写代码的程度。

用业务语言编写代码,需要对软件设计有着良好的理解。提到设计,人们的初步印象是“高内聚低耦合”,但这是一个太过高度抽象的描述。SOLID 原则是一个更具实践性的指导原则,有了原则做指导,就可以更好地理解设计模式了。

有了基础原则,我们会知道将不同的代码划分开,这样就产生了分层。好的分层可以构建出抽象,而其他人就可以在这个抽象上继续发展。对于程序员来说,构建自己的核心抽象是最关键的一步。

目前构建核心抽象最好的方式是领域驱动设计(DDD),它将我们思考的起点拉到了业务层面,通过战略设计将系统按照不同的上下文划分开来,再通过战术设计,指导我们有效地设计一个个的领域模型。

但无论怎样做设计,前提是使用适当的技术解决适当的问题,不要把技术用复杂,把团队带入泥潭。

项目准备

从头开始一个项目时,一个好的实践就是把一切都准备好。迭代 0 就是这样一个把迭代准备好的实践,从需求到技术,做好充分的准备工作再开启项目,你会显得从容不迫。在技术方面,迭代 0 最重要的准备工作就是构建脚本,它是后续很多工作的基础,比如,持续集成。

DoD

完成的定义(DoD),是一个确保合作各方理解一致的实践。它是一个清单,由一个个检查项组成,每个检查项都是实际可检查的。有了 DoD,做事就只有两种状态:完成和未完成。

站会

站会,一种轻量级的会议形式,用来同步每天发生的事情。一般来说,只说三件事:昨天做了什么,今天打算做什么,遇到了什么问题。

看板

看板,一种项目管理工具, 将正在进行的工作可视化。通过看板,可以发现团队正在进行工作的很多问题。看板有实体和电子之分,可以根据自己的项目特点进行选择。

回顾会议

回顾会议,是一种复盘实践,让团队成员对一个周期内发生的事情进行回顾。回顾会议一般分为讲事实、找重点和制定行动项三个部分。但在开始回顾之前,会先进行安全检查,确保每个人都能放心大胆地说真话。

重构

重构,是程序员的基本功,把调整代码的动作分解成若干可以单独进行的“重构”小动作,一步步完成。重构的前提是识别代码的坏味道。保证代码行为不变,需要有测试配合,而重构的方向是,重构成模式(Refactoring to Patterns)。重构的过程和编写代码的过程最好结伴而行,最佳实践就是测试驱动开发。

分支开发

分支开发模型,是每个团队都要面临的问题。行业中有两种常见的分支模型,一种是基于主干的开发模型,一种是分支开发模型。分支开发符合直觉,却不是最佳实践。主分支开发模型是与其他实践配合最好的模式,但也需要开发者有着良好的开发习惯。如果并行开发多个功能,可以考虑 Feature Toggle 和 Branch by Abstraction。

Fail Fast

Fail Fast 是一个重要的编程原则:遇到问题,尽早报错。不要以构建健壮系统为由,兼容很多奇怪的问题,使得 Bug 得以藏身。

少做事,才能更有效地工作

算法的差异

排序算法是每个程序员都会学到的内容,大家对各种算法也是如数家珍:插入排序、冒泡排序、归并排序、堆排序、快速排序等等。我们也知道各个算法的复杂度,比如,插入排序是 O(n^2),快速排序平均情况下是 O(nlogn)等等。

你有没有想过一个问题,不同算法的复杂度本质差别到底是什么呢?我们就以插入排序和快速排序为例,为什么快速排序要比插入排序快呢?因为做比较的次数少。为什么同样的排序,比较次数会有差异呢?因为插入排序每次循环只关注当前的目标,循环之间没有关系,而快速排序在做不同划分时,上一次的结果对下一次有助力,因此它省下了不少的比较次数。所谓的算法优化,其实就是尽可能利用已知的信息,少做不必要的事。

再来看一个常见的面试题,给你一堆数,找出前 100 个。很多人直觉就会想到排序,然后选出前 100 个。这种做法固然可行,但一定是做多了,因为这里需要的是找出前 100 个数,而不是要 100 个有序的数字,更不是要所有的数都有序。说到这里,你就知道了,只要把数据划分开就好,并不需要排序,如果划分点不是第 100 个元素,就向着 100 所在的方向继续划分就好。计算机是最擅长处理繁琐重复工作的,即便如此,我们依然要做算法优化,原因是当数据规模大到一定程度时,不同复杂度的算法差别就非常明显了。算法没用好,计算机硬件再好,也是徒劳的。

有效工作

插入排序并不会因为干的活多,就比快速排序得到更高的评价,因为它们比的是谁排得快。工作效率高,不是因为代码写得多,而是有效工作做得多。如果 CPU 都被无效指令占据了,哪有时间执行有效指令呢?即使你很忙碌,但工作进展依然是收效甚微,因为无效工作占据了你太多的大脑,让你不能聚焦在正经事上,当然就是效率不高了。

有效工作,需要我们把力量聚焦到正确的地方,做本质复杂度(Essential Complexity)的事情,少做无意义的事情。

我曾经在一个大公司做咨询,按照他们的统计,线上 60% 的代码从来没有运行过。我们都知道,一多半的代码增加的可不只是一多半的工作量,团队可能需要的是几倍甚至几十倍的心力去维护它。

  • 拓展自己的上下文,看到真正的目标,更好地对准靶子,比如,多了解用户,才不至于做错了方向;站在公司的层面上,才知道哪个任务优先级更高;站在行业的角度,而不局限于只在公司内成为高手,等等。
  • 去掉不必要的内容,减少浪费,比如,花时间分析需求,不做非必要的功能;花时间做好领域设计,别围着特定技术打转;花时间做好自动化,把精力集中在编码上,等等。

要想有效工作,有两点非常重要。一方面,意识上要注意自己工作中无效的部分。这就像一个开关,拨过去就好了。所以,读这个专栏有人常有恍然大悟的感觉,也有人觉得很简单。

另一方面,要构建自己关于软件开发的知识体系,这是要花时间积累的。在这个专栏中,我给你讲了很多最佳实践,就是让你知道,在某些方面,有人已经做得很好了,花时间学习,比自己从头摸索好很多。这就像所有的数学公式一样,理论上你都可以自行推导,但肯定不如从教科书上学得快。

你可以用自己的工作原则做更多本质复杂度的事情,减少无意义的时间消耗。


文章作者: 韩思远
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 韩思远 !
评论
 上一篇
MySQL实战-基础篇 MySQL实战-基础篇
MySQL实战-基础篇一、一条SQL查询语句是如何执行的?执行如下SQL,我们看到的只是输入一条语句,返回一个结果,却不知道这条语句在 MySQL 内部的执行过程。 select * from T where ID=10; 我们一起来看
2020-09-08
下一篇 
技术基础 技术基础
技术基础 洞悉技术的本质,享受科技的乐趣 https://coolshell.cn/ 程序员如何用技术变现 程序员用自己的技术变现,其实是一件天经地义的事儿。写程序是一门“手艺活儿”,那么作为手艺人,程序员当然可以做到靠自己的手艺和技能养
2020-08-31
  目录