10倍程序员工作法
最近chatgpt大火,让很多人觉得很多工作包括程序员会被取代。其他岗位我不太了解,但是对于程序员这个职业,我认为但是其实这说明,大家对于程序员在处理的问题的认知是程序员处理的是某一个独立的问题上,或者某个独立的很具体的问题上,当然这和程序员面试题有关,毕竟程序员面试题其实就是这种,面试题,说起来就是做卷子,做卷子你怎么可能有背靠互联网的机器人比,而那些不好衡量的看起来是空话的东西,在一次面试中,人还真不一定表现的比背后是所有面试经验帖的机器人高明。
但是其实这已经是忘记了程序员是为什么而存在的。程序员掌握的技术叫做信息技术,目的是为了快速的收集和处理信息,那么为什么这个技术重要呢?了解这个问题,其实也能明白AI到底给我们带来了什么。
前言
其实说起来,现在算是第四次工业革命了:
- 第一次是蒸汽时代,人类掌握了巨大的物理上的力量,机械赋予了人类更加强大的改造物理世界的力量
- 第二次是电气时代,算是第一次的一种延续,让人们可以更加快速地改造世界,同时加快了人们物质上的合作效率
- 第三次是信息时代,也就是我们大部分人经历的时代。很多人可能不理解信息为什么重要,毕竟这东西虚无飘渺,且互联网上看起来信息爆炸,根本不值钱。但其实,对于社会来说,信息就是最值钱的东西,因为,钱其实就是信息,甚至整个人类社会存在的基础也是信息,就像《人类简史》中所说,没有一个共同的想象,人类最大只能维持成一个个小部落,就像动物一样,正是一个个共同想象,让人类产生了合作。而市场的作用就是交流信息,没有任何一个人可以处理人类社会中所有的信息,就算他能接触到所有信息。再比如奢侈品,比如玉石,钻石,黄金,你凭什么说他们之间的价格不同,就是信息,他的物理本质没有贵贱之分,所以没必要问奢侈品为什么贵,没有原因。
- 第四次就是人工智能了,算是第三次的延续。为什么会有这个东西呢,就是因为信息爆炸了,太多了,个人处理不过来,处理信息的速度太低了,而很多人合作又会带来新的信息要处理,这些是管理学要做的了。在过去充当人工智能的岗位有很多,比如低端咨询岗位,他们其实就是帮忙整合信息的,因为信息太多了,比如买房子,除非你最近想买房子,不然你不可能每天去收集,去处理,所以就催生了专门处理这些信息的人呢。
所以如果说第三次工业革命是让人们可以更快的收集和处理信息了,那么第四次,chatgpt包括人工智能在我个人看来,是给了每个人更强的工具去筛选和处理信息。
这看起来没有本质变化,还是在提高处理信息的速度,但是别忘了,任何复杂问题都是简单问题耦合在一起造成的,量变产生质变。当问题的数量足够多,参与的人足够多,其实就不是一个问题了。
复杂度就是最大的问题,资本一切提高效率的努力,认识世界规律的努力,管理学等等,都是为了降低复杂度,当然整体效率提高不代表个体幸福,因为整体想要高效,其中的每个个体都不能有个性。反过来说,如果一个工具能提高足够多的效率,就是工业革命了,工业革命其实就是工具带来了极大的效率提高。
说这些,就是为了表达一个观点,程序员处理的事情其实是降低信息复杂度,把信息的分析和处理交给机器,首先要做的事程序员本身理解这种信息,然后转化为程序,所以程序员的工作价值主要并不在于编程,如果不能帮助处理信息,降低复杂度,把复杂度交给机器,再高明的技术都没用,毕竟决定科技发展方向的从来都是资本。
所以,提高程序员工作效率的关注点不应该只放在如何更快的敲代码上,当然除了极少部分为程序员做工具的人。
10x程序员工作法
以下内容来自于极客时间中的同名专栏,这里只是我个人的笔记总结,推荐大家去看看专栏
思考框架
本质复杂度和偶然复杂度
软件行业里有一本名著叫《人月神话》,其中提到两个非常重要的概念:本质复杂度(Essential Complexity)和偶然复杂度(Accident Complexity)。
简单来说,本质复杂度就是解决一个问题时,无论怎么做都必须要做的事,而偶然复杂度是因为选用的做事方法不当,而导致要多做的事。
比如你要做一个网站,网站的内容是你无论如何都要写的,这就是“本质复杂度”。而如果今天你还在用汇编写一个网站,效率是不可能高起来的,因为你选错了工具。这类选错方法或工具而引发的问题就是“偶然复杂度”。
最佳实践的四大原则
- 以终为始;
- 任务分解;
- 沟通反馈;
- 自动化。
思考框架:
- Where are we?(我们现在在哪?)
- Where are we going?(我们要到哪儿去?)
- How can we get there?(我们如何到达那里?)
四大原则与思考框架之间的联系
在实际的工作中,这个思考框架会帮助我更好地了解自己的工作。比如,当一个产品经理给我交代一个要开发的功能特性时,我通常会问他这样一些问题:
- 为什么要做这个特性,它会给用户带来怎样的价值?
- 什么样的用户会用到这个特性,他们在什么场景下使用,他们又会怎样使用它?
- 达成这个目的是否有其它手段?是不是一定要开发一个系统?
- 这个特性上线之后,怎么衡量它的有效性?
如果产品经理能够回答好这些问题,说明他基本上已经把这个工作想得比较清楚了,这个时候,我才会放心地去了解后续的细节。
以终为始就是在工作的一开始就确定好自己的目标。我们需要看到的是真正的目标,而不是把别人交代给我们的工作当作目标。你可以看出这个原则是在帮助我们回答思考框架中,Where are we going?(我们要到哪儿去?)这个问题。
任务分解是将大目标拆分成一个一个可行的执行任务,工作分解得越细致,我们便越能更好地掌控工作,它是帮助我们回答思维框架中,How can we get there?(我们如何到达那里?)的问题。
如果说前两个原则是要在动手之前做的分析,那后面两个原则就是在通往目标的道路上,为我们保驾护航,因为在实际工作中,我们少不了与人和机器打交道。
沟通反馈是为了疏通与其他人交互的渠道。一方面,我们保证信息能够传达出去,减少因为理解偏差造成的工作疏漏;另一方面,也要保证我们能够准确接收外部信息,以免因为自我感觉良好,阻碍了进步。
自动化就是将繁琐的工作通过自动化的方式交给机器执行,这是我们程序员本职工作的一部分,我们擅长的是为其他人打造自动化的服务,但自己的工作却应用得不够,这也是我们工作中最值得优化的部分。
以始为终
以始为终:如何让努力不白费
我们日常面对的真实工作场景:许多人都是刚刚听到别人要求做的一个功能,就开始脑补接下来的一切。导致的结果,就是付出的努力毫无意义。
“以终为始”是一种反直觉的思维方式,是大多数人不具备的。所以,日常生活中,我们看到很多有趣的现象。比如,大学毕业时,有很多人想考研,如果你问他为什么要考研,得到的理由通常是为了找个好工作。但考研真的能帮他找个好工作吗?不一定,因为找工作和考研根本就不是同一棵技能树。如果真的是想找个好工作,那你就应该了解工作的要求是什么,怎样才能掌握工作要求的技能。
“设计登录功能”的例子,对比“以终为始”的思维,你也许会替我的同事抱不平,他们或许也有“以终为始”的思路,只不过,他们的“终”和我这个客户的“终”不一样罢了。这就要说到做软件,本质上是在构建一个“集体想象”。
想象的共同体
我们这些做软件的人其实就是一个想象的共同体,这个“集体想象”就是我们要做的软件,任何想象都需要一个载体将其展现出来,我们编写软件的过程就是将这个“集体想象”落实的过程。
既然是“集体想象”,那么在载体将想象呈现出来之前,我们的想象很难统一起来,都或多或少存在差异。
所以,任何事物都要经过两次创造:一次是在头脑中的创造,也就是智力上的或者第一次创造(Mental/First Creation),然后才是付诸实践,也就是实际的构建或第二次创造(Physical/Second Creation)。
再回到前面“设计一个登录功能”的例子上,我的同事们在构建的其实是他们自己的想象,而不是我们共同的想象。
这其中最大的一个区别就在于,没有人会为他们自己的想象买单的。所以说,他们看到的“终”不是真正的终,只是一个自我的“终”,至于看到什么样的“终”,这取决于每个人的见识。
对做软件的人来说,我们应该把“终”定位成做一个对用户有价值的软件,能够为别人带来价值,自己的价值才能体现出来。
规划和发现
有了“以终为始”的思维,我们考虑的是别人会怎么用我们的平台。我们设计的方式是,用户到我们的网站,阅读相关文档,然后参考文档一步一步照着做。这其中的一个关键点是:文档,特别是《起步走》的文档,这是用户接触我们这个平台的第一步,决定了他对我们产品的第一印象。
所以,我们决定从写《起步走》这个文档开始,这个文档描绘了用户怎样一步一步使用我们的开发平台,完成第一个“Hello World”级别的应用。请注意,这个时候,我们一行代码都没有写。
人类是一个擅长脑补的群体,一旦有人看到了这个文档,他就已经可以构想出这个平台已经存在的样子,进而给出各种各样的反馈:“我认为这个地方可以这样做”“我觉得那个地方可以改改”。
**所有这些反馈都是真实的,因为他们已经“看到了”一个真实的东西。正是这些真实的反馈,让我们逐渐地锁定了目标。**之后,我们才开始动手写代码。
“以终为始”的方式,不仅仅可以帮我们规划工作,还可以帮我们发现工作中的问题。
亚马逊 CTO 介绍亚马逊是如何开发一项产品的,简单来说,他们采用向后工作的方法,开发一项产品的顺序为:
- 写新闻稿;
- 写 FAQ(常见问题解答);
- 写用户文档;
- 写代码。
理解的鸿沟
我们讨论的主题是“以终为始”。那我们第一个问题就是,“终”到底是什么?在前面这个例子里,“终”就是“完成”,可是,小李认为他的活已经做完了,老张却认为他没做完。
弥合差异的方式有很多,有一个最佳实践,它的名字叫 DoD(Definition of Done,完成的定义),从这个概念的名字便不难看出,它就是为了解决软件开发中常见的“完成”问题而生的。
怎样让 DoD 更好地发挥作用。
- DoD 是一个清单,清单是由一个个的检查项组成的,用来检查我们的工作完成情况。DoD 的检查项,就是我们开发产品所需的一系列有价值的活动。比如:编写代码、编写测试代码、通过测试人员验收等。什么样的活动是有价值的,也许每个团队的认识是不同的。但如果你的团队认为除了功能代码,其他都没价值,也许这是个信号,说明你的团队整体上是缺乏职业素养的,在这样的团队工作,前景并不乐观。
- DoD 的检查项应该是实际可检查的。你说代码写好了,代码在哪里;你说测试覆盖率达标了,怎么看到;你说你功能做好了,演示一下。
- DoD 是团队成员间彼此汇报的一种机制。别把“汇报”想复杂了,最简单的汇报就是说一句“这个功能做完了”。当我们有了 DoD,做事只有两种状态,即“做完”和“没做完”。在团队协作中,我们经常会听到有人说“这个事做完了 80%”,对不起,那叫没做完,根本没有 80% 做完的说法。
如果你可以放开思路,会发现 DoD 的思维在工作中用途非常广泛。比如,当我们需要和其他团队合作开发一个接口时,我们都知道第一步就是要把接口定义下来。
**在协作中一旦确立好 DoD,我们甚至可以通过流程把它固化下来,从而更高效高质地完成工作。**当然,我们在工作生活中难免会有一些临时的工作,它们没有复杂到需要一个流程,但是也可以用 DoD 思维来高效地解决。比如:经常会有人过来,让我帮忙做些事。运用 DoD 的思维,我首先会问他我具体要做哪些事,确认好细节(相当于定义好“检查项”),然后我就知道,这个忙我能帮到什么程度。我请别人帮忙的时候,也会很清楚告诉他,哪些事是需要他做的,尽量减少不必要的误解。
DoD 是一个思维模式,是一种尽可能消除不确定性,达成共识的方式。我们本着“以终为始”的方式做事情,DoD 让我们能够在一开始就把“终”清晰地定义出来。
人与人协作中,经常会出现各种问题,根本原因就是,有太多因为理解差异造成的误解,进而浪费了大量的时间,而 DoD 就是一种将容易产生歧义的理念落到实处的方法。
在做任何事之前,先定义完成的标准。
接到需求任务,首先要做什么?
在软件开发中,程序员做什么一般都由需求来定义。我们都知道,需求是软件开发的一个重要组成部分,但你可能并没有仔细想过,不同的需求描述方式,可能会影响我们程序员对需求的理解。
因为信息的传递是会衰减的,你不可能把你理解的信息 100% 传递给另外一个人,而这中间,如何传递,也就是如何描述将直接决定衰减的比例。
很多公司的软件开发模式是基于功能列表的,这个列表“规定”了程序员要做的功能,各个组从产品经理那里领来开发列表,然后“照单抓药”开始写代码。但是,通常这种功能列表只是一些简单的描述,你并不能看到全局。
很多团队的一个状态就是,程序员们都知道要开发的功能是什么,但这个功能是谁在什么样的场景下使用的,很多人却回答不上来。如果你去问他为什么要开发这个功能,他通常会说:这是功能列表里规定的。
这种功能列表式的需求描述方式,将一个完整的需求敲成了碎片。 只有所有功能全部开发完成,对接在一起的时候,才是“破镜重圆”的时刻。
根据这种基于功能列表的需求描述,每个组在安排工作的时候,都会按照自己的理解进行功能排列。所以,当你的组完成了一个功能时,这个功能却可能上不了线,因为你还要依赖于其他组的工作,而这个组不巧,却刚好把相关的功能开发排在了后面。这还只是两个组之间有依赖的情况,如果需要多个组协同,可以想象,状况会多么糟糕。
因此,一些新的需求描述方式也就应运而生,这其中,用户故事(User Story)是我最喜欢的一种方式。它是站在用户的角度来描述了一个用户希望得到的功能,关注用户在系统中完成一个动作需要经过怎样的路径。既然它是“故事”,它就需要是一个完整的场景,可以讲述出来。
如果你的团队采用用户故事的格式进行需求描述固然好,如果不能,在功能列表中,补充验收标准也会极大程度地改善双方协作的效率。
验收标准所给出实现细节应该是业务上的,程序员在这种问题上思考才是真正意义上的浪费时间,我们的发挥空间应该是在技术实现上。
虽然你名义上是程序员,但当拿到一个需求的时候,你要做的事不是立即动手写代码,而是扮演产品经理的角色,分析需求,圈定任务范围。相信我,事前分析绝对比你拿一个写好的系统给老板,而他却告诉你这不是他想要的,好太多了。
如果今天的内容你只能记住一件事,那请记住:在做任何需求或任务之前,先定好验收标准。
精益创业
精益创业提供给我们的是一个做产品的思考框架,我们能够接触到的大多数产品都可以放在这个框架内思考。
有了框架结构,我们的生活就简单了,当产品经理要做一个新产品或是产品的一个新特性,我们就可以用精益创业的这几个概念来检验一下产品经理是否想清楚了。
比如,你要做这个产品特性,你要验证的东西是什么呢?他要验证的目标是否有数据可以度量呢?要解决的这个问题是不是当前最重要的事情,是否还有其他更重要的问题呢?
如果上面的问题都得到肯定的答复,那么验证这个目标是否有更简单的解决方案,是不是一定要通过开发一个产品特性来实现呢?
扩大工作上下文
程序员总喜欢用技术去解决一切问题,但很多令人寝食难安的问题其实根本不是问题。之所以找不出更简单的解决方案,很多时候原因在于程序员被自己的思考局限住了。
不同角色工作真正的差异在于上下文的差异。在一个局部上下文难以解决的问题,换到另外一个上下文甚至是可以不解决的。所以说无论单点有多努力也只是局部优化,很难达到最优的效果。
想把工作做好,就需要不断扩大自己工作的上下文,多了解一下别人的工作逻辑是什么样的,认识软件开发的全生命周期。
扩大自己的上下文,除了能对自己当前的工作效率提高有帮助,对自己的职业生涯也是有好处的。随着你看到的世界越来越宽广,得到的机会也就越来越多。
如果今天的内容你只记住一件事,那请记住:扩大自己工作的上下文,别把自己局限在一个“程序员”的角色上。
通往结果的路径
关于“以终为始”,我们前面讲的内容一直是看到结果,结果是重要的。然而,通向结果的路径才是更重要的。
对比我们的工作,多数情况下,即便目标清晰,路径却是模糊的。所以,不同的人有不同的处理方式。有些人是走到哪算哪,然后再看;有些人则是先推演一下路径,看看能走到什么程度。在我们做软件的过程中,这两种路径所带来的差异,已经在前面的小故事里体现出来了。一种是前期其乐融融,后期手忙脚乱;一种是前面思前想后,后面四平八稳。我个人是推崇后一种做法的。
这个思想并不难理解,我们可以很容易地将它运用在工作中的很多方面。比如:
- 在做一个产品之前,先来推演一下这个产品如何推广,通过什么途径推广给什么样的人;
- 在做技术改进之前,先来考虑一下上线是怎样一个过程,为可能出现的问题准备预案;
- 在设计一个产品特性之前,先来考虑数据由谁提供,完整的流程是什么样的。
让自己的工作成果可以被数字化
迭代0
对比这个清单,大多数新项目都在一项或几项上准备得不够充分。即便你做的不是一个从头开始的项目,对照这个清单,也会发现项目在某些项上的欠缺,可以有针对性地做一些补充。如果今天的内容你只记住一件事,那么请记住:设计你的迭代 0 清单,给自己的项目做体检。
任务分解
好了,和大家分享这两个例子只是为了热热身,说明人类解决问题的方案是差不多的。当一个复杂问题摆在面前时,我们解决问题的一个主要思路是分而治之。
一个大问题,我们都很难给出答案,但回答小问题却是我们擅长的。所以,当我们学会将问题分解,就相当于朝着问题的解决迈进了一大步。
那么,用这种思路解决问题的难点是什么呢?给出一个可执行的分解。
不过,在实际工作中,大多数人都高估了自己可执行粒度,低估任务分解的程度。换句话说,如果你没做过任务分解的练习,你分解出来的大部分任务,粒度都会偏大。
只有能把任务拆分得非常小,你才能对自己的执行能力有一个更清楚地认识,真正的高手都是有很强的分解能力。这个差别就相当于,同样观察一个物品,你用的是眼睛,而高手用的是显微镜。在你看来,高手全是微操作。
如今软件行业都在提倡拥抱变化,而任务分解是我们拥抱变化的前提。
实际上,很多人之所以写不好测试,主要是因为他不懂任务分解。
小事反馈周期短,而大事反馈周期长。小事容易做好,而大事难度则大得多。所以,以这个标准来看,底层的测试才更容易写好。另外,因为涉及到的模块过多,任何一个模块做了调整,都有可能破坏高层测试,所以,高层测试通常是相对比较脆弱的。
测试驱动开发TDD
在测试驱动开发中,重构与测试是相辅相成的:没有测试,你只能是提心吊胆地重构;没有重构,代码的混乱程度是逐步增加的,测试也会变得越来越不好写。因为重构和测试的互相配合,它会驱动着你把代码写得越来越好。这是对“驱动”一词最粗浅的理解。
测试驱动设计
许多人抗拒测试有两个主要原因:
第一,测试需要“额外”的工作量。这里我特意把额外加上引号,因为,你也许本能上认为,测试是额外的工作,但实际上,测试也应该是程序员工作的一部分,这在上一篇文章中我已经讲过。
第二,很多人会觉得代码太多不好测。之所以这些人认为代码不好测,其中暗含了一个假设:代码已经写好了,然后,再写测试来测它。
如果我们把思路反过来,我有一个测试,怎么写代码能通过它。**一旦你先思考测试,设计思路就完全变了:我的代码怎么写才是能测试的,也就是说,我们要编写具有可测试性的代码。**用这个角度,测试是不是就变得简单了呢?
我们举个写代码中最常见的问题:static 方法
Mock 对象的做法面对 static 时行不通了。因为它跳出了对象体系,static 方法是没法继承的,也就是说,没法用一系列面向对象的手法处理它。
你没有办法使用 Mock 对象,也就不好设置对应的方法返回值。要想让这个方法返回相应的值,你必须打开这个 static 方法,了解它的实现细节,精心地按照里面的路径,小心翼翼地设置对应的参数,才有可能让它给出一个你预期的结果。更糟糕的是,因为这个方法是别人维护的,有一天他心血来潮修改了其中的实现,你小心翼翼设置的参数就崩溃了。而要重新进行设置的话,你只能把代码重读一遍。
如此一来,你的工作就退回到原始的状态。更重要的是,它并不是你应该关注的重点,这也不会增加你的 KPI。
显然,你跑偏了。讨论到这里你已经知道了 static 方法对测试而言,并不友好。所以,如果你要想让你的代码更可测,一个好的解决方案是尽量不写 static 方法。
static 是一个方便但邪恶的东西。所以,要限制它的使用。除非你的 static 方法是不涉及任何状态而且行为简单,比如,判断字符串是否为空。否则,不要写 static 方法。你看出来了,这样的 static 方法更适合做库函数。所以,我们日常写应用时,能不用尽量不用。
如果你在自己的代码遇到第三方的 static 方法怎么办,很简单,将第三方代码包装一下,让你的业务代码面对的都是你自己的封装就好了。
如何做好任务分解
最后,我要特别强调一点,所有分解出来的任务,都是独立的。也就是说,每做完一个任务,代码都是可以提交的。只有这样,我们才可能做到真正意义上的小步提交。
如果今天的内容你只能记住一件事,那请记住:按照完整实现一个需求的顺序去安排分解出来的任务。
为什么你的测试不够好?
我看到过很多团队在测试上出现过各种各样的问题,比如:
- 测试不稳定,这次能过,下次过不了;
- 有时候是一个测试要测的东西很简单,测试周边的依赖很多,搭建环境就需要很长的时间;
- 这个测试要运行,必须等到另外一个测试运行结束;……
如果你也在工作中遇到过类似的问题,那你理解的写测试和我理解的写测试可能不是一回事,那问题出在哪呢?为什么你的测试不够好呢?主要是因为这些测试不够简单。
只有将复杂的测试拆分成简单的测试,测试才有可能做好。
测试为什么要简单呢?有一个很有趣的逻辑,不知道你想没想过,测试的作用是什么?显然,它是用来保证代码的正确性。随之而来的一个问题是,谁来保证测试的正确性?
既然无法用写程序的方式保证测试的正确性,我们只有一个办法:**把测试写简单,简单到一目了然,不需要证明它的正确性。**所以,如果你见到哪个测试写得很复杂,它一定不是一个好的测试。
测试坏味道
**很多人总想在一个测试里做很多的事情,比如,出现了几个不同方法的调用。请问,你的代码到底是在测试谁呢?**这个测试一旦出错,就需要把所有相关的几个方法都查看一遍,这无疑是增加了工作的复杂度。
另一个典型“坏味道”的高发区是在断言上,请记住,测试一定要有断言。
还有一种常见的“坏味道”:复杂。最典型的场景是,当你看到测试代码里出现各种判断和循环语句,基本上这个测试就有问题了。举个例子,测试一个函数,你的断言写在一堆 if 语句中,美其名曰,根据条件执行。还是前面提到的那个观点,你怎么保证这个测试函数写的是对的?除非你用调试的手段,否则,你都无法判断你的条件分支是否执行到了。你或许会疑问,我有一大堆不同的数据要测,不用循环不用判断,我怎么办呢?你真正应该做的是,多写几个测试,每个测试覆盖一种场景。
一段旅程(A-TRIP)
怎么样的测试算是好的测试呢?
有人做了一个总结 A-TRIP,这是五个单词的缩写,分别是
- **Automatic,自动化;**把测试尽可能交给机器执行,人工参与的部分越少越好。
- **Thorough,全面的;**应该尽可能用测试覆盖各种场景。理解这一点有两个角度。一个是在写代码之前,要考虑各种场景:正常的、异常的、各种边界条件;另一个角度是,写完代码之后,我们要看测试是否覆盖了所有的代码和所有的分支,这就是各种测试覆盖率工具发挥作用的场景了。
- **Repeatable,可重复的;**这里面有两个角度:某一个测试反复运行,结果应该是一样的,这说的是,每一个测试本身都不应该依赖于任何不在控制之下的环境;还有一个角度,一堆测试反复运行,结果应该是一样的。这说明测试和测试之间没有任何依赖,这也是我们接下来要说的测试的另外一个特点。
- **Independent,独立的;**测试和测试之间不应该有任何依赖,什么叫有依赖?比如,如果测试依赖于外部数据库或是第三方服务,测试 A 在运行时在数据库里写了一些值,测试 B 要用到数据库里的这些值,测试 B 必须在测试 A 之后运行,这就叫有依赖。
- Professional,专业的。
如何砍需求?
以我们用了好多次的登录为例,如果我问你这个需求是什么,大多数人的第一直觉还是用户名密码登录。
基本上,闯入你脑海的需求描述是主题(epic),在敏捷开发中,有人称之为主用户故事(master story)。
如果你对需求的管理粒度就是主题,那好多事情就没法谈了。比如,时间紧迫的时候,我想砍需求,你问产品经理,我不做登录行不行,你就等着被拒绝吧。
但是,如果你说时间比较紧,我能不能把登录验证码放到后面做,或是邮件地址验证的功能放到后面,这种建议产品经理是可以和你谈的。
这其中的差别就在于,后者将需求分解了。
需求要分解
“主题”只是帮你记住大方向,真正用来进行需求管理,还是要靠进一步分解出来的需求。这里的讨论,我们会继续沿用前面专栏文章中已经介绍过的需求描述方式:用户故事,它将是我们这里讨论需求管理的基本单位。
评价用户故事有一个“ INVEST 原则”,这是六个单词的缩写,分别是:
- Independent,独立的。一个用户故事应该完成一个独立的功能,尽可能不依赖于其它用户故事,因为彼此依赖的用户故事会让管理优先级、预估工作量都变得更加困难。如果真的有依赖,一种好的做法是,将依赖部分拆出来,重新调整。
- Negotiable,可协商的。有事大家商量是一起工作的前提,我们无法保证所有的细节都能 100% 落实到用户故事里,这个时候最好的办法是大家商量。它也是满足其它评判标准的前提,就像前面提到的,一个用户故事不独立,需要分解,这也需要大家一起商量的。
- Valuable,有价值的。一个用户故事都应该有其自身价值,这一项应该最容易理解,没有价值的事不做。但正如我们一直在说的那样,做任何一个事情之前,先问问价值所在。
- Estimatable,可估算的。我们会利用用户故事估算的结果安排后续的工作计划。不能估算的用户故事,要么是因为有很多不确定的因素,要么是因为需求还是太大,这样的故事还没有到一个能开发的状态,还需要产品经理进一步分析。
- Small,小。步子大了,不行。不能在一定时间内完成的用户故事只应该有一个结果,拆分。小的用户故事才方便调度,才好安排工作。Testable,可测试的。不能测试谁知道你做得对不对。这个是我们在前面已经强调过的内容,也就是验收标准,你得知道怎样才算是工作完成。
第一个关注点是可协商。作为实现者,我们要问问题。只是被动接受的程序员,价值就少了一半,只要你开始发问,你就会发现很多写需求的人没有想清楚的地方。
在我的职业生涯中,我无数次将需求挡了回去,不是我不合作,而是我不想做一些糊涂的需求。我之所以能问出问题,一方面是出于常识,另一方面就是这里说的用户故事是否有价值。用户故事,之所以是故事,就是要讲,要沟通。
还有一个更重要的关注点,也是这个模块的核心:小。无论是独立性也好,还是可估算的也罢,其前提都是小。只有当用户故事够小了,我们后续的腾挪空间才会大。那接下来就是一个重要的问题,怎么才算小?这就牵扯到用户故事另一个重要方面:估算。
通常情况下,是双方对需求的理解出现了偏差,这时候负责用户故事编写的同事就要站出来,帮助大家澄清需求。所以,一般来说,估算的过程也是大家加深对需求理解的过程。估算还有另外一个重要的作用:发现特别大的用户故事。一般而言,一个用户故事应该在一个迭代内完成。
许多团队真正的困境在于,在开发过程中缺少需求分解的环节。在这种情况下,需求的管理基本单位就是一个主题,既然是基本单位,那就是一个不可分割的整体。团队就被生生绑死在一个巨大的需求上,没有回旋的余地。如果团队可以将需求分解,需求的基本单位就会缩小,每个人看到的就不再是“铁板”一块,才能更方便地进行调整,才会有比较大的腾挪空间。
需求管理
用几个程序员生活中的例子帮你理解一下。让系统不能正常运行的线上故障,就属于重要且紧急事情,不赶紧解决,就影响公司的正常运营。团队对系统升级改造就属于重要不紧急:改造好,性能也好,可维护性也得到提升;不改造,一时半会也能用。一些临时任务都属于紧急不重要,而刷朋友圈则属于既不紧急也不重要。
按照时间管理的理念,重要且紧急的事情要立即做。重要但不紧急的事情应该是我们重点投入精力的地方。紧急但不重要的事情,可以委托别人做。不重要不紧急的事情,尽量少做。这个矩阵带给我们思维上最大的改变是,让人意识到事情和事情不是等价的。如果不把精力放在重要的事情上,到最后可能都变成紧急的事情。
把这个思路带回到我们现实的需求管理中,你会发现,其实团队面临的各种需求所采用的优先级排序方式,基本上都是按照紧急程度排列的,但它们是否真的重要呢?
如果你把这个问题抛给需求的提出者,我几乎可以肯定,他们给你的答案是,他们提出的需求就是重要的。一种可能是,他们也分不清重要和紧急的差别,正如有时候我们也糊涂一样。
试想,两个产品经理出现在你面前,一个告诉你,公司要拓展新方向,这个功能要做;另一个却说,公司要进一步盈利,那个功能必须做。在你看来,他们两个说得都对,听上去都挺重要的。但骨感的现实是,你把两件事都接下来,等着你的是累死都完不成的任务。这个时候,我们能做的是什么呢?跳出这个上下文,到更大的上下文中。你判断不了哪个需求更重要,就请更高一级的老板来判断。
再发散讲几句,为人做事同样要不断扩展自己的上下文,这也就是我们常说的涨见识。很多所谓的人生难题不过是因为见识有限造成的。比如,如果你觉得公司内总有人跟你比技术,莫不如把眼光放得长远一些,把自己放在全行业的水平上去比较。因为你是为自己的职业生涯在工作,而不是一个公司。
需求分解之后,最重要的是,排列需求的优先级。优先级的排列方式有很多,我们可以借鉴时间管理的方法,把事情按照重要和紧急的维度进行划分,得到了四个象限。我们要尽可能把精力放在重要的事情上,而不是把紧急的事情当成优先级排序的方式。
需求分解成一个个小块,其实也分解了原本合一的上下文。如果想要有效地管理需求,尤其是确定事情的重要程度,一种方式是找回丢失的上下文。如果我们自己无法判断上下文,一种好的办法是,引入外部更大的上下文。
如何用最小的代价做产品?
我们的直觉当然是把所有的东西都实现了再去检验,但是世界不会停下来等着我们。事实也一次又一次教育我们,“憋大招”的瀑布式软件开发已经成为不合时宜的“老古董”。那我们的理想怎么实现呢?唯有分解。
我们前面提到,精益创业就是通过不断地尝试在真实世界中验证产品想法,其中一个重要的实践是最小可行产品(Minimum Viable Product,MVP),我们这次就把这个实践展开讨论一下。
先说“最小”。这里的“最小”,指的是最小的代价。怎么叫最小的代价,就是能不做的事情就不做,能简化的事情就简化。
首先,我们必须清楚一件事,我们要做的是验证一个想法的可行性,甚至不是为了开发一个软件,开发软件只是一种验证手段。
很多程序员都会有一个认识上的误区,容易把解决方案当成问题。我们开发软件的目的是为了解决问题,如果不写软件就把问题解决了,岂不是更好。
第一步,我们要验证这样一个想法是否可行。我们做了一个产品文档,就好像我们已经有了这个产品一样,让负责销售的同事拿着这个文档给客户讲讲,看看客户对这个想法的反映。在这个过程中,我们验证了基本的想法,已有设备进行物联网化改造的需求存在,客户看到了这样的一个东西,各种各样的想法和要求就会冒出来。此外,我们还获得了一个额外的收获,我们知道了客户对于这样一个产品能够接受的价格区间,这可以帮助团队给产品进行适当的定价。验证了方向上的想法,我们开始进入到具体的产品设计阶段。这个阶段我们想验证的是,我们给出的产品设计用户是否可以接受。于是,我们决定把这个产品的交互做出来。得益于原型工具的快速发展,我们用一个原型工具做出了相对完整的用户界面,而且把各种交互流都做出来了。在用户看来,这几乎就是完整的软件了。
说完了"最小",我们再来看"可行"。可行是要找到一条路径,给用户一个完整的体验。做程序员出身的人,对软件系统的认识总是一个模块一个模块的,相对比较弱的方面是缺少一个完整的图景。但从产品可行的角度,我们需要转换一下思路,不是一个模块做得有多完整,而一条用户路径是否通畅
当时间有限时,我们需要学会找到一条可行的路径,在完整用户体验和完整系统之间,找到一个平衡。
逐步上线了一个功能相对完整的 P2P 平台。在这个过程中,我们每个阶段都会上线新功能,从用户可见的角度,他看到的始终是一个完整的平台,其中的变化只有站在内部实现者的角度才能看得清楚。(比如还款功能,虽然是完整的生命周期中的一个,但是第一个月没有用户会用到,所以排期可以靠后一点)
想要在实践中运用好最小可行产品的理念,就是要用最小的代价找到一条可行的路径。最小的代价就是能不做的事就不做,能简化的事情就简化。
程序员通常愿意用自己的代码解决问题,而写代码通常是代价非常高的解决方案,它应该成为最后的产品解决方案。可行的路径,是一条完整的用户体验路径,至少在用户眼中是这样的。
我们常常会想给客户一个完整的系统,但在时间有限的情况下,我们必须学会分解。
如果今天的内容你只能记住一件事,那请记住:做好产品开发,最可行的方式是采用 MVP。
在这个模块中,我们学习到了一些最佳实践:
测试金字塔
– 行业中测试组合的最佳实践。
– 多写单元测试是关键。
测试驱动开发
– 测试驱动开发的节奏是:红——绿——重构,重构是测试驱动开发区别于测试先行的关键。
– 有人把测试驱动开发理解成测试驱动设计,它给行业带来的思维改变是,编写可测的代码。
艾森豪威尔矩阵(Eisenhower Matrix)
– 将事情按照重要和紧急进行划分。
– 重要且紧急的事情要立即做。重要但不紧急的事情应该是我们重点投入精力的地方。紧急但不重要的事情,可以委托别人做。不重要不紧急的事情,尽量少做。
最小可行产品
– “刚刚好”满足客户需求的产品。
– 在实践中,要用最小的代价找到一条可行的路径。
另外,我还提到了一些可以直接在工作中应用的做法和评判标准:
- 尽量不写 static 方法;
- 主分支开发模型是一种更好的开发分支模型;
- 好的用户故事应该符合 INVEST 原则;
- 估算是一个加深对需求理解的过程,好的估算是以任务分解为基础的;
- 好的测试应该符合 A-TRIP。
我也带你学习了一些重要的思想,帮你更好地改善自己的开发工作:
- 分而治之,是人类解决问题的基本手段;
- 软件变更成本,它会随着时间和开发阶段逐步增加;
- 测试框架把自动化测试作为一种最佳实践引入到开发过程中,使得测试动作可以通过标准化的手段固定下来;
- 极限编程之所以叫“极限”,它背后的理念就是把好的实践推向极限;
- 大师级程序员的工作秘笈是任务分解,分解到可以进行的微操作;按照完整实现一个需求的顺序安排开发任务。
沟通反馈
我们努力地学习各种知识,为的就是更好地理解这个世界的运作方式,而沟通反馈,就是我们与真实世界互动的最好方式。
当几个人一起讨论问题时,别人往往刚开了个头,他就认为自己已经理解了别人的想法,然后开始表达自己的观点。信息都不全,何谈解码。所以,开发团队的讨论中常常出现一个人高谈阔论,却离题万里的情况。我们要想让自己更好地工作生活,就必须接纳真实世界的反馈,而接纳真实世界的反馈,一是需要我们打开自己的接收器,把信号接纳进来,让反馈进来,这是解码的前提;二是扩展见识,提升自己解码器的效果,更好地理解别人要表达的内容到底是什么。说了编码器和解码器可能出现的问题,我们再来看另外一个可能造成影响的问题:编解码器算法,也就是怎么协调沟通双方更有效地进行沟通。
人生不如意之事,十有八九,之所以很多人有如此多的不如意,很大原因在于我们对真实世界有着很多不切实际的幻想,美好的愿望并不能驱动这个世界,在软件开发中也是如此。虽然人和人生活在一个世界中,但对世界的理解却是千差万别的。
改善编解码,需要从几个角度着手,分别是:编码器,让信息能输出更准确;解码器,减少信号过滤,改善解码能力;还有编解码算法,也就是各种来自行业的“最佳实践”,协调沟通的双方。
用业务的语言去写代码,一来是可读性高,二来是能够识别代码中不变和易变的部分,更好地实践设计模式,做到领域驱动设计
轻量级沟通
开会是为了解决问题,但真实情况却是开了会又没有解决多少问题,这真是一个奇特的矛盾。回想一下,你参加过的会议里面,有没有效果特别好的呢?在我职业生涯中,凡是效果特别好的会议,基本上都是用来做信息同步的。比如,领导宣布一个事情,这种会议几乎不会浪费时间。宣布消息,大家收到消息,结束。那效果不好的会议是什么样呢?几乎都是那些讨论会,你一言我一语,每个会几乎无一例外,都有几个擅长打岔的,这个会基本上都会跑偏,时间就会这样一分一秒地流逝了。
改善会议的第一个行动项是,减少参与讨论的人数。有人会说,我这个讨论有好几个议题,每个议题要不同的人参与,那你要做的是,分别找这几个人专门讨论,而不是把大家放到一起。相比于会议的形式,面对面沟通因为注意力有限,参与的人数不可能太多。也因为参与的人数相对少一些,每个人的投入也会更多一些。
第二个行动项是,如果你要讨论,找人面对面沟通。如果有一个问题需要讨论,我要做的是,分别找到相关人针对关心的主题进行讨论,然后,我把讨论的结果汇总再去征求大家意见。如果大家达成一致了,我才会选择开会。这个时候,开会的目的不再是讨论,而是信息同步:我准备这么干了,相关各方已经同意了,知会大家一下,结束。
多尝试用可视化的方式进行沟通。
持续集成的诞生,就是人们尝试缩短集成周期的结果。为什么要缩短周期呢?因为我们希望尽早得到反馈,知道自己的工作结果是否有效。所以,想要做好持续集成,就需要顺应持续集成的本质:尽快得到工作反馈。
由此,我们便得到持续集成的关键点,你只要记住一句话,快速反馈。
快速反馈,这句分成两个部分,快速和反馈,这也就引出了持续集成的两个重要目标:怎样快速地得到反馈,以及什么样的反馈是有效的。
什么是复盘?复盘,原本是一个围棋术语,就是对弈者下完一盘棋之后,重新把对弈过程摆一遍,看看哪些地方下得好,哪些下得不好,哪些地方可以有不同甚至是更好的下法等等。这种把过程还原,进行研讨与分析的方式,就是复盘。
现如今,复盘的概念已经被人用到了很多方面,比如,股市的复盘、企业管理的复盘,它也成为了许多人最重要的工具,帮助个体和企业不断地提升。这其中最有名的当属联想的创始人柳传志老爷子,他甚至把“复盘”写到了联想的核心价值观里。为什么复盘这么好用呢?在我看来有一个重要的原因,在于客体化。俗话说,当局者迷,旁观者清。以我们的软件开发作为例子,在解决问题的时候,我们的注意力更多是在解决问题本身上,而很少会想这个问题是怎么引起的。**当你复盘时,你会站在另外一个视角,去思考引起这个问题的原因。这个时候,你不再是当事者,而变成了旁观者。**你观察原来那件事的发生过程,就好像是别人在做的一样。你由一个主观的视角,变成了一个客观的视角。用别人的视角看问题,这就是客体化。
你的团队如果能一下洞见到根因固然好,如果不能,那么最好多问一些为什么。具体怎么问,有一个常见的做法是:5 个为什么(5 Whys)。这种做法是丰田集团的创始人丰田佐吉提出的,后来随着丰田生产方式而广为人知。为什么要多问几个为什么?因为初始的提问,你能得到的只是表面原因,只有多问几个为什么,你才有可能找到根本原因。我给你举个例子。服务器经常返回 504,那我们可以采用“5 个为什么”的方式来问一下。
- 为什么会出现 504 呢?因为服务器处理时间比较长,超时了。
- 为什么会超时呢?因为服务器查询后面的 Redis 卡住了。
- 为什么访问 Redis 会卡住呢?因为另外一个更新 Redis 的服务删除了大批量的数据,然后,重新插入,服务器阻塞了。
- 为什么它要大批量的删除数据重新插入呢?因为更新算法设计得不合理。
- 为什么一个设计得不合理的算法就能上线呢?因为这个设计没有按照流程进行评审。
多走近用户,才能明白自己的代码用来做什么了,也能在讨论需求时更加明白哪些需求是有必要的
作为一个程序员,克服技术难题是我们工作的一个重要组成部分,所以,一旦有困难我们会下意识地把自己投入进去。但这真的是最好的做法吗?并不是,不是所有的问题,都是值得解决的技术难题。
写程序有一个重要的原则叫 Fail Fast,这是什么意思呢?就是如果遇到问题,尽早报错。
如果配置文件缺少了一个重要参数,比如,缺少了数据库最大连接数,你打算怎么处理?很多人会选择给一个缺省值,这就不是 Fail Fast 的做法。既然是重要参数,少了就报错,这才叫 Fail Fast。
其实,Fail Fast 也有一些反直觉的味道,很多人以构建健壮系统为由,兼容了很多奇怪的问题,而不是把它暴露出来。反而会把系统中的 Bug 隐藏起来。我们都知道,靠 debug 来定位问题是最为费时费力的一种做法。所以,别怕系统有问题,有问题就早点报出来。
自动化
在我看来,做有价值的事是重要的,这里面的有价值,不仅仅是“做”了什么,通过“不做”节省时间和成本也是有价值的。我的两个同事阻止了客户的浪费,所以,我将这个项目视为成功。
对于开发来说,也遵循同样的道理。程序员这个群体技术能力实在太强,做一个技术方案简直是太符合直觉的做法,我们就是忠实地把一个个需求做出来,把“全世界”都自动化了。**但事实上,这个世界太多的浪费就是做了不该做的东西。**在我们的专栏里,我反复地说,我们要多问问题,目的就是为了不做那些不该做的事。
小心 NIH 综合症
你可以从需求的角度判断哪些工作是可以不做的,但我们也要防止程序员自己“加戏”,我再给你讲一个技术人员普遍存在的问题:NIH 综合症(Not Invented Here Syndrome)。
比如,这种乱象在前端领域也出现了,各种各样的框架,让很多前端程序员哭诉,实在学不动了。再比如,我曾经面试过一个接触 Go 比较早的程序员,他就是恨不得把所有框架都自己写。
说了这么多,无非就是想说明一件事,**写代码之前,先问问自己真的要做吗?能不做就不做,直到你有了足够的理由去做。**对应到 Larry Wall 的说法,你要懒惰,花大力气去规避精力消耗。
一般来说,在构建持续交付的基础设施时,会有下面几个不同的环境。
- 持续集成环境,持续集成是持续交付的前提,这个过程主要是执行基本的检查,打出一个可以发布的包。
- 测试环境(Test),这个环境往往是单机的,主要负责功能验证,这里运行的测试基本上都是验收测试级别的,而一般把单元测试和集成测试等执行比较快的测试放到持续集成环境中执行。
- 预生产环境(Staging),这个环境通常与生产环境配置是相同的,比如,负载均衡,集群之类的都要有,只是机器数量上会少一些,主要负责验证部署环境,比如,可以用来发现由多机并发带来的一些问题。
- 生产环境(Production),这就是真实的线上环境了。
保证自己的代码不要过于混乱:SOLID原则
今天,我们从软件行业的一个段子说起。甲方想要做个电商网站,作为乙方的程序员问:“你要做个什么样的呢?”甲方说:“像淘宝那样就好。”程序员问:“那你打算出多少钱?”甲方想了想,“5 万块钱差不多了吧!”
这当然是个调侃客户不懂需求的段子,但你有没有想过,为什么在甲方看来并不复杂的系统,你却觉得困难重重呢?因为你们想的根本不是一个东西。
在客户看来,我要的不就是一个能买东西的网站吗?只要能上线商品,用户能看到能购买不就好了,5 万块钱差不多了。
而你脑中想的却是,“淘宝啊,那得是多大的技术挑战啊,每年一到‘双 11’,那就得考虑各种并发抢购。淘宝得有多少程序员,5 万块你就想做一个,门都没有。”
做一个新项目的时候,并不需要考虑太多的想象中的需求,最重要的是做好两点
- 分析需求,做好领域划分和抽象(越高层的抽象越稳定,越细节的东西越容易变化),保证架构的可扩展性
- 尽量少引入各种框架,或者保证框架的可插拔,可快速替换
回顾上面的过程,你就可以看到,每次随着业务量的增长,原有技术无法满足需要,于是,就需要用新的技术去解决这个问题。这里的关键点在于:不同的业务量。
一个只服务于几个人的系统,单机就够了,一个刚刚入行的程序员也能很好地实现这个系统。而当业务量到达一台机器抗不住的时候,就需要用多台机器去处理,这个时候就必须考虑分布式系统的问题,可能就要适当地引入中间件。而当系统变成为海量业务提供服务,就没有哪个已经打造好的中间件可以提供帮助了,需要自己从更底层解决问题。虽然在业务上看来,这些系统是一样的,但在技术上看来,在不同的阶段,一个系统面对的问题是不同的,因为它面对业务的量级是不同的。更准确地说,不同量级的系统根本就不是一个系统。
在前面的例子中,淘宝的工程师之所以要改进系统,真实的驱动力不是技术,而是不断攀升的业务量带来的问题复杂度。所以,评估系统当前所处的阶段,采用恰当的技术解决,是我们最应该考虑的问题。
那请记住:用简单技术解决问题,直到问题变复杂。
也许你会说,我做的系统没有那么大的业务量,我还想提高技术怎么办?答案是到有好问题的地方去。现在的 IT 行业提供给程序员的机会很多,找到一个有好问题的地方并不是一件困难的事,当然,前提条件是,你自己得有解决问题的基础能力。
如何做微服务
先回答一个问题,我们为什么要做微服务?对这个问题的标准回答是,相对于整体服务(Monolithic)而言,微服务足够小,代码更容易理解,测试更容易,部署也更简单。这些道理都对,但这是做好了微服务的结果。
怎么才能达到这个状态呢?这里面有一个关键因素,怎么划分微服务,也就是一个庞大的系统按照什么样的方式分解。这是在很多关于微服务的讨论中所最为欠缺的,也是很多团队做“微服务”却死得很难看的根本原因。
不了解这一点,写出的服务,要么是服务之间互相调用,造成整个系统执行效率极低;要么是你需要花大力气解决各个服务之间的数据一致性。
换句话说,服务划分不好,等待团队的就是无穷无尽的偶然复杂度泥潭。只有正确地划分了微服务,它才会是你心目中向往的样子。
那应该怎么划分微服务呢?你需要了解领域驱动设计。
领域驱动设计(Domain Driven Design,DDD)是 Eric Evans 提出的从系统分析到软件建模的一套方法论。它要解决什么问题呢?就是将业务概念和业务规则转换成软件系统中概念和规则,从而降低或隐藏业务复杂性,使系统具有更好的扩展性,以应对复杂多变的现实业务问题。
DDD 到底讲了什么呢?它把你的思考起点,从技术的角度拉到了业务上。
贴近业务,走近客户,我们在这个专栏中已经提到过很多次。但把这件事直接体现在写代码上,恐怕还是很多人不那么习惯的一件事。DDD 最为基础的就是通用语言(Ubiquitous Language),让业务人员和程序员说一样的语言。这一点我在《21 | 你的代码为谁而写?》中已经提到过了。使用通用语言,等于把思考的层次从代码细节中拉到了业务层面。越高层的抽象越稳定,越细节的东西越容易变化。
有了通用语言做基础,然后就要进入到 DDD 的实战环节了。DDD 分为战略设计(Strategic Design)和战术设计(Tactical Design)。
战略设计是高层设计,它帮我们将系统切分成不同的领域,并处理不同领域的关系。我在前面的内容中给你举过“订单”和“用户”的例子。从业务上区分,把不同的概念放到不同的地方,这是从根本上解决问题,否则,无论你的代码写得再好,混乱也是不可避免的。而这种以业务的角度思考问题的方式就是 DDD 战略设计带给我的。战术设计,通常是指在一个领域内,在技术层面上如何组织好不同的领域对象。举个例子,国内的程序员喜欢用 myBatis 做数据访问,而非 JPA,常见的理由是 JPA 在有关联的情况下,性能太差。但真正的原因是没有设计好关联。
说了半天,这和微服务有什么关系呢?微服务真正的难点并非在于技术实现,而是业务划分,而这刚好是 DDD 战略设计中限界上下文(Bounded Context)的强项。
虽然通用语言打通了业务与技术之间的壁垒,但计算机并不擅长处理模糊的人类语言,所以,通用语言必须在特定的上下文中表达,才是清晰的。就像我们说过的“订单”那个例子,交易的“订单”和物流的“订单”是不同的,它们都有着自己的上下文,而这个上下文就是限界上下文。
它限定了通用语言自由使用的边界,一旦出界,含义便无法保证。正是由于边界的存在,一个限界上下文刚好可以成为一个独立的部署单元,而这个部署单元就可以成为一个服务。
所以要做好微服务,第一步应该是识别限界上下文。
你也看出来了,每个限界上下文都应该是独立的,每个上下文之间就不应该存在大量的耦合,困扰很多人的微服务之间大量相互调用,本身就是一个没有划分好边界而带来的伪命题,靠技术解决业务问题,事倍功半。
有了限界上下文就可以做微服务了吧?且慢!
以我拙见,一次性把边界划清楚并不是一件很容易的事。大家在一个进程里,调整起来会容易很多。然后,让不同的限界上下文先各自独立演化。等着它演化到值得独立部署了,再来考虑微服务拆分的事情。到那时,你也学到各种关于微服务的技术,也就该派上用场了!
DDD与精益创业,微服务的划分异曲同工,都需要划分好上下文界限,切用最简单的方式实现当下的需求,再逐步扩大(需要好的设计保证可扩展)精益创业实际上是一种持续验证,验证想法的有效性,获得经过验证的认知(Validated Learning)。
如果你了解了业务,你自己就可以推演出基本的代码结构。但反过来,如果让你看了代码,从中推演出业务,那几乎是不可能的。
事实上,每次了解到一个业务,我都会在脑子中过一下,如果是我做这个业务,我会怎么做。这样一来,我就会先在整体上有一个预判,后面再对应到实际的代码上,就不会那么陌生了。要了解业务,我一般都会请人给我讲一下,这个业务是做什么的,解决什么样的问题,具体的业务流程是什么样子的,等等。
改造遗留系统,我给你几个建议:
- 构建测试防护网,保证新老模块功能一致;
- 分成小块,逐步替换;
- 构建好领域模型;
- 寻找行业中关于系统构建的最新理解。