软件开发是将现实中的业务转化为可运行的代码。在这个过程中,我们往往需要根据用户的描述或资料写出符合要求的代码,即将人类语言转化为编程语言。在《二义性-人类语言与编程语言的重要区别》一文中,我们了解到,人类语言是一种二义性语言,而编程语言是一种非二义性语言(指没有狭义二义性。本文中如无特别说明,二义性均指狭义二义性)。因此,将人类语言转化为编程语言的过程,需要消除二义性。
1 软件开发流程
为了更清楚二义性对软件开发过程的影响,我们先要了解软件开发流程。目前软件开发有客户端与服务端分离的情况,由于服务端开发更具复杂度与代表性,因此我们的讨论以后端开发为基础。
1.1 后端开发的工作环节
不考虑立项及可行性研究,一个软件开发工作的真正开展是从收集需求开始的。不同情况下,收集需求这项工作不一定会有专人承担(可能有产品经理也可能没有)。并且软件开发工作也是持续优化的过程,需求甚至可以来自团队内部的任何人。收集需要的成果可能是一本复杂的需求文档、项目原型图,也可能仅仅是书面或口头的几句话,无论复杂与简单我们都将其称为需求。而后端开发工作是从接收到需求开始的。
如上图,后端开发工作有以下环节:
-
从需求文档到数据模型(需求分析):需求分析的最终目的是创建数据模型(包含建库建表)。数据库文档也是由这一步的分析得出的;
-
从数据模型到算法模型(算法建模):思考过程的必要步骤,产生虚拟的中间结果(即算法模型);
-
从算法模型到代码(代码化):产生最终结果-即项目代码。这个过程其实包括:实现算法模型(狭义上的写代码)、输入的规则校验、输出的异常处理、自测(仅功能性测试)、整理代码。
再加上收集需求,开发过程有4步:收集需求(包含整理工作)、需求分析、算法建模、代码化。
1.2 后端开发工作环节的必要性
1.2.1 需求分析的必要性
只要我们翻开任意一年的软件工程师的考题就能找到最简单的例子来学习什么是需求分析,因为考题的题干部分就包含了简单的需求文档和需求分析。下面以某年的考题为例。
需求文档:
需求分析:
可见,对于一个原生需求(即尚未开发实现的需求。与之相对的是改进需求,即在原来实现的基础上进行优化的需求),后端开发工程师需要进行分析,像考题中那样梳理关系(开发过程中不会像考题那样直接给出需求分析),最终得到的就是数据模型。
根据需求分析得出的对象关系,可以初始数据模型,后续的开发工作要以此为基础。
为什么必需建立初始数据模型?因为数据结构是算法的基础,算法实现需要建立在数据结构之上。之所以说是初始数据模型,是因为算法实现过程中有可能要对原来的数据模型进行修改。
1.2.2 算法建模及其必要性
算法建模环节要产生算法模型。这里的算法模型是指广义上的,是解决问题的数学步骤,不是狭义上的算法工程师开发的算法。
算法模型是计算机领域的称呼,更通用的称呼是数学模型。算法模型是数学模型的子集。
无法产生数学模型的需求,一定无法代码化。例如以下需求:
-
挑出唐诗中最美的一首;(因为美是一个无法定量化的标准,无法用数学步骤描述)
-
挑出在场所有高个子的人。(高个子的人也不是一个明确的集合,同样无法用数学语言描述)
可以产生数学模型的需求,不一定能代码化。由以上例子可以看出,由于算法模型要求有穷性,因此所有无限个解或无穷位数解的问题,虽然是数学可以描述的,但不符合算法模型的要求。例如以下需求:
-
精确解决三体问题(没有通解,只能在特定近似下求特解)
-
精确计算圆周率(结果是无穷的)
通俗地说,一个需求如果不符合数理逻辑,则它一定不能写成代码(如果有人声称可以完成,那么他一定是在运行结果上作了妥协)。
不过算法模型本来就要求时间与空间上的有限性,因此可以写成算法模型的需求就一定可以写成代码。因为算法模型是用来解决实际问题的,所以算法模型还要受到物理规律的节制。例如:对于一个随机过程,精准预测未来的算法就是不可能实现的。
1.2.3 相互影响且有序进行
代码化的必要性是不用置疑的,因为开发工作最终的最重要的成果就是代码。
在真实场景下,我们未必是按严格的先后顺序完成后端开发的三个步骤。因为我们很可能在先建立某张表后就开始写相关的代码了,不一定会等到所有的表都创建完;并且往往也会想到一个步骤的具体实现,写几行代码,而不一定是把所有步骤都想好了才写代码。我们随时有可能在写代码时,返修算法模型,例如:觉得if判断换一种方式写更好;也有可能在算法建模时返修数据模型,例如:觉得添加一个字段或属性实现起来更简单。
但是,我们一定是在有了一部分数据模型后,才构想这部分算法模型;构想出来这一步的算法模型之后,才写的这部分代码。
三个步骤之间是相互影响且有序进行的。
2 开发流程中的二义性
包括收集需求在内的四个流程中,前三个流程收集需求、需求分析、算法建模都是需要消除相关的二义性的。
2.1 收集需求阶段的二义性
收集需求的人(不一定是产品经理,也可能是团队中其他提需求的人)需要明确需求中涉及的名词的含义。明确名词含义,这就是在消除二义性。
收集需求阶段要消除的二义性主要来自于人类语言及行为习惯本身的二义性:
-
例如:一个从小生活在工业地区的人,跟你说“车”这个名词,他很有可能说的是工厂加工用的机床;如果你恰好没有生活在跟他相同的环境下,刚刚听到,大概率会出现理解偏差。
-
又例如:一个需求是语音播报员工的名字,你名单上有一个人的名字是“任我行”;大部分情况下,你一定会认为读作“ren wo xing”;但万一人就叫“ren wo hang”呢?
-
再例如:要求显示设备的采集时间;看上去是一个没有歧义的要求,但实际上“时间”这个词语在现实语境中有可能指时间点,也有可能指时间段;“还不睡,都啥时间了”和“等了挻长时间了吧”,这两个“时间“明显不是一个意思;这时候你再看这个要求,是要采集开始时间呢,还是采集了多长时间?
这些表层的、浅显的,但又不易查觉的二义性,却会直接影响后续的解决问题的算法模型。
收集需求阶段需要解决的二义性大部分是名词的二义性,这些名词占整个开发流程中全部名词的80%。作为一个合格的收集需求的人,应该尽可能多地消除这些名词的二义性。虽然占到了80%,但这并不意味着消除这部分二义性是最困难的。因为人与人之间共同的习惯还是比较多的,大概60%的二义性根本不需要刻意思考,直接根据日常生活经验,下意识地就能消除。也就是说对于大概60%名词,只需要用正式用语转达即可,无需要过多地解释。而对于80%中的另外20%的名词的二义性,一个较好的办法是可以通过列举名词对象的属性和行为来消除。例如:”我是工厂开车的,加工管钢“,由于带上了”加工管钢“这个行为,听者往往可以意识到这里的”车“并不是指交通工具。
2.2 需求分析阶段的二义性
在收集需求阶段的二义性完全解决的情况下,需求分析阶段的二义性主要来自于实现业务逻辑所必需添加的属性和实现细化步骤所必须明确的属性。需求分析阶段消除的也主要是名词的二义性,需要消除整个开发流程中20%的名词的二义性。
虽然只占20%,但这部分名词往往是需要分析才能发现的,并非显而易见。可能有两类:
-
第一类是为了实现业务逻辑而必须额外添加的。如:为实现对象之间的关联关系而设的关联字段、为实现特殊筛选而设的查询字段、为实现记录而设的日期标识、身份标识等;
-
第二类是开发者在实现细化实现步骤时才能发现的。如:属性的具体取值范围、对象的分类方式是哪一种(三种分类方式:经典分类、概念聚集或原型理论。参见《面向对象分析与设计》-Grady Booch-第3版-中英版)...。
这些名词属性并不会出现在需求说明书中,但却需要仔细考虑如何设置、它们的具体含义、如何避免属性之间的含义重叠、属性或对象的生命周期及作用范围。要消除这部分二义性,只能在创建具体数据模型时,并且一定需要仔细分析。
2.3 算法建模阶段的二义性
算法建模阶段的二义性主要来源于对象行为的二义性。行为即是动作,因此这个阶段要消除开发流程中所有动词的二义性。对象的行为在编程中依靠方法来实现,因此编程中对象的行为等价于对象的方法。对于一个无二义性行为(方法),要求责任范围明确、执行步骤明确、产生结果明确。例如:
-
责任范围明确:方法不必考虑任务范围之外的事情。例如:铁锨有”挖“这个行为,但这个”挖“是用来”挖土“的,所以不用考虑”挖饭“的情况。如果所有情况都考虑,无限职责范围的方法是不可能实现的,因为算法要求有穷性,不可能写无穷多个if去排除所有情况;
-
执行步骤明确:例如”在只有一个窗口的情况下设置排除系统,需要考虑残疾人优先、老人优先、军人优先三种情况“。对于一个方法,当这三种优先一起出现时,一定要固定的优先级规则(随机或分时段决定也是一种规则)。如果是人来控制,可以看心情;但如果是使用程序控制,就不能毫无规则地变化。
-
产生结果明确:例如“选出众数”这个操作。当众数不是唯一时,是取一个还是都取,不同情况下有不同要求,不能时而返回一个,时而都返回。或者当一个众数都没有,返回什么,也要有明确的规定。
2.4 代码化阶段不需要消除二义性
编程语言是对算法模型的准确描述语言。一旦算法模型确定,就可以转化为代码了,不会再有需要消除的二义性。因此,代码化阶段不需要消除二义性。
不过正如前面所说,我们通常不会考虑好一个完备的算法模型之后才去代码化。通常我们会考虑好正常情况下的算法实现逻辑之后,先把正常情况代码化,再逐步调试和修改。因此,代码化阶段虽然不需要消除二义性,但同样需要花时间做很多重要的事情(前面列举了一些)。
3 了解这些的现实意义
我们了解这些有什么意义呢?也许了解这些可以为我们带来以下思考。
3.1 什么样的需求才是合理的需求
对于开发人员来说,最无奈的事情莫过于接收到一个二义性很强的需求。并且这些二义性通常还是需求分析阶段的二义性,这些二义性的特点就是取决于用户的理解,无法技术层面去消除。这意味着,如果收集需求的人或用户意识不到,就往往需要开发人员深度思考,再想一种能让收集需求的人或用户听懂的方式描述出来,请他们回复;否则,就需要开发人员根据对这些二义性问题不同选择的后果,再自行决定。因此,团队所有人,及时在需求提出时尽可能评价需求的合理性是很重要的。
什么是需求的合理性?如果先不考虑技术条件的限制,一个可实现的需求至少要能转化成数理逻辑。因为算法模型是代码化的充要条件,算法模型是数学模型的子集,所以符合数理逻辑是最起码的要求。
任何一个需求一定要同时满足以下两个基本条件,才有可能符合数理逻辑:
-
不能有二义性:对于业务所指向的方面,任何名词(包括对象名词及属性名词)的含义是唯一且不变的;
-
必须可定量化:对于业务中用到的所有指标性属性,必须可以用确定的数值或区间描述。
前面我们举过“车”、“时间”的例子,这些都是名词的含义不确定,导致产生了歧义。而“任我行”的例子,就可以看作读音取值不明确,导致无法定量。除些之外,常见的还有“等级评定要求分数范围明确”、“被减数-减数=差,计算任何一个都要求三知其二,并且三个数都属于同一个对象的同一个属性的值”...
3.2 大致估计一个项目的人员配比
一般来说,从需求分析开始到代码化完成为止,整个过程都是由开发人员完成的。这样做是为了降低交流成本。但这也决定了,一个项目团队后端开发人员的比例通常是最高的。
如果按需求分析、算法建模、代码化三个步骤每一步都与收集需求的难度相当,那么显然后端开发人员至少是需求人员的三倍。但实际上,需求分析、算法建模、代码化通常比收集需求更难,尤其是对于改进需求,例如:“将所有车辆的行驶里程按年、月、日分别统计”,需求人员转述这句话时只需要十几秒,但是有人可以在一分钟之内完成吗?
3.3 增加对职业的理解(个人观点)
后端开发人员总共需要完成三个步骤需求分析、算法建模、代码化,三个步骤中哪个最重要呢?个人认为这些环节的分量依次是:算法建模>需求分析>代码化(代码归整>狭义写代码)。
需求分析的重要性在于,如果初始的数据模型创建的好,会很方便后续的代码书写以及数据库管理。这要求个人对于各专业的用语及其语义是敏感的,对数据结构的理解是深刻的。
算法建模是一种利用数学解决实际问题的能力,需要有较强的数学能力和较强的联系实际的能力。
代码化的能力,就是需要学习更多的技术栈、了解更多的技术规则。
其实掌握更多的技术栈及学习更多的技术规则也是很有用的,因为就目前来说,80%到90%的面试问的问题都是关于技术栈的问题(至于为什么会这样,这里不展开),如果我们决定争取一份工作,一定要以正确地态度熟练掌握一些技术栈东西以应对面试。但我觉得从长远发展来看,不提升理解,只强行学习很多技术栈是很没必要的。原因有如下两点:
-
技术栈太多了,记不过来的。比如成百上千条Linux命令,真的有必要背得滚瓜烂熟吗。编程语言有几百种,真的要全部方法都记住吗。只能说一个一个地记是效率最低的方式。
-
真的没必要记,因为可以检索。事实上,如果是因为某个技术栈不会,或想知道需要什么技术解决,这类问题通常都是最好检索的,例如:“java将字符串转为时间的方法”、“ES与mysql之间数据同步的工具”,真接使用合理的关键词检索,像这些问题可能直接AI都可以显示答案,连链接都不用点开。但是关于算法实现的问题,尤其涉及专业领域业务的,如果你不知道如何解决,通常你也搜不到答案,例如:“如何找到路线轨迹上定位错误的点”、“根据数据如何分析人们的浏览偏好”。
为什么是这样?前面我们说过,代码化阶段是对算法模型的实现,只要构思出了算法模型就确定了细化步骤,每一步使用相应的技术栈即可。所以只要在提问关于技术栈的问题时,问题本身就确定了一个较小的范围,致使问题二义性是很小的。但如果是整个算法模型都不会,这个问题的范围是很大的,导致问题本身的二义性很强。而对于二义性强的问题,目前的搜索引擎和人工智能还真未必理解你的问题到底是在说什么。
有些能力是看年限的,有些能力是看水平的。大家都是从事这个行业的,在正常情况下,掌握技术栈的多少就取决于我们见的多不多,大家只是使用偏好不同。没有说哪个技术栈难到一定学不来的,如果想了解,基本上不出三天都能掌握个大概。不过构建算法模型的能力,是基于过去20多年的学习能力培养出来的,可能有天分的就是比我们更能想到好的解决思路,不是说我们今天想超过三天后就能超过的,需要很长时间的刻苦学习。
近年来生成式AI已经能解决不少包含二义性的问题了,像那些二义性很小的技术栈相关问题,真的不太能体现出人类作为最强大脑生物的价值。就问一个问题:要比记忆东西,你还能比过AI不成?因此,我们更多应该将精力放在研究二义性更强的,AI很难解决的问题上。