正式工作也快一年了,希望能总结一些工作方式,还有一些自己的理解。这个文章主要在讲日常生活中我是如何完成一个需求/项目的,也有很多内容其实是希望能告诉一年前的自己的,所以对于团队新人来说也许能窥探出一些避免踩坑的方式。
本文并不会探讨“如何科学高效的进行软件开发”这种问题。我只是罗列一些现状,至于能否改进和如何改进,可能还需要我后面慢慢体会。
分析需求
做一个需求第一步就是分析需求了,这一步也是开发流程中最重要的一步。记住,这里说的分析需求不是产品经理分析的需求,而是我们开发拿到“产品需求”之后做的二次分析,也叫需求把控。
开发为什么要分析需求
先定基调:产品经理和用户一样不可靠,技术一定要再做一次需求分析。这里不是对产品经理能力的否认,而是对技术人员的一种自我保护。原因如下:
- 第一个方面,在一个复杂的业务系统中,我们往往会发现,开发和测试(尤其是测试)比产品经理更了解这个系统,产品有时需要开发的帮助。
- 第二个方面,产品经理也是人,很少有人能思维极其缜密的考虑问题,人的记忆总是会有遗漏,思维总会有疏忽。
- 第三个方面,在业务部门里,开发人员对于产品的发展也有很大责任,一个糟糕的需求下来,最终遭殃的一定是开发人员。(血的教训就不一一列举了)
以上列出的方方面面,从软件开发的角度,归根到底,就是在需求阶段认清楚需求风险,避免后面的需求变更。
读到这里你可能有很多疑问,为什么需求变更要开发来买单呢?为什么不用敏捷开发来应对需求风险呢?这些可能和我们部门长期的开发方式有关:看似有章法,实际无章法。结果导向、业务导向的后果就是业务方先定下来发布时间,这个时间之前把这件事情搞定。在这种模式下,没有人会给你时间搞小周期迭代,更没有人会帮你挡住需求变更。大家会把赌注都压在项目一气呵成的假设上。也可以说,开发需要有能力识别出项目的全部风险。
- 举个例子,需求范围评估不全,导致开发后期发现需求影响到了某个原有功能,但是影响的结果却未定义(产品经理也没考虑到),这种事情领导最不愿意看到,而又总是将责任推给开发人员。尽管这种事情我认为也算是一种需求变更,但是从来不会有人给你延期帮你讲情(结果导向)。
长期演变过来的结果,就是要求开发要有很高的业务理解能力,其实主要就是需求分析能力,甚至这种能力成为了你晋升最重要的因素之一。因为按照上面的开发模式,最理想的办法,就是在需求分析阶段指出需求缺陷,发现项目风险,使得后面的开发过程一马平川的走过来。当然这就需要我们的开发具备这项能力了。
开发如何分析需求
- 这个需求解决了什么现实问题?
此时一定要着重考虑现实问题,从现实问题入手和理解,一连串为什么要问到底,抓到问题的本质。 - 这个需求真的能解决这个问题吗?
对问题有了理解之后,就要对当前的需求方案进行评估,看一下这个需求是否能真的解决这个问题,对于边界情况和特殊情况是否也会生效。如果这个需求只是个临时方案,那还要去考虑最终方案是什么样的,临时方案是否值得去做。
另一方面,还要考虑需求会不会留下新的问题,不能出现“捡了芝麻丢了西瓜”的需求。 - 需求描述是否足够清晰?
和传统的软件开发不同,我们缺乏严谨的需求规格说明书,因此产品提交的需求文档很可能有疏漏。我们需要检查它的一致性(是否存在自相矛盾),二义性(是否存在语义不明的表述)。每个用例的前置条件是什么,不同状态下的输出是什么(需要考虑系统的大量状态,很可能是笛卡尔乘积),边界情况下的输出是什么。
现实情况是,需求文档会有大量不清晰的表述,也是俗称的“一句话需求”,例如:查询结果可导出。(导出哪些字段,是否需要分页,导出的文件格式和模板是什么,非功能性需求有哪些) - 这个问题是不是只能通过这个需求来解决,有没有更好的办法?
一个需求提过来的可能是很具象的,比如在哪里哪里加一个按钮,在哪里新增一个输入框,这种需求提过来很有可能会对开发造成误导,低下头来直接去做了。开发此时要考虑的是,这个需求侵入到这个领域模型里是否合理,用户的操作归属在这个页面上是否合理,相似的需求已经有过类似的实现能否复用等。
如果到了这一步开发有不同的见解,就要做好讨(si)论(bi)的准备了。职场上如何说服一个人,还是有很深的方法和套路的。可以寻求上级的支持,也可以用一些技术上的借口来旁敲侧击- -。 - 需求的范围是否有遗漏?
需求的范围决定了项目的范围,项目的范围影响着项目的时间和质量。因此需求范围的改变对项目的影响是巨大的,可惜的是我们在需求阶段总是会遗漏一些范围,为项目后期埋下隐患。
评估一个需求的范围,可以从系统角度来看待,每个模块的改造都需要考虑到直接依赖它的模块是否需要改造,包括接口的变动,接口实现的变动,数据的变动。如果需要,还要一层一层的分析下去,因此每个模块的改造的影响范围可能是一连串的模块。(由此可见模块的解耦多么重要) - 历史数据是否兼容,如何做数据迁移?
当存在关系模型变动时(例如一对多变成了一对一),尤其要考虑历史数据如何处理,如何兼容的问题。同样还有新增了一些关键的属性,但是历史数据都没有这些属性时,是否需要初始化和特殊处理。
码农的注意事项
千万不要因为自己想搞技术而忽略需求分析,程序员的意义在于对现实问题抽象建模,用计算机的手段来解决现实问题,也就是充当着现实和计算机之前的桥梁,因此桥梁的两边都要顾及和熟悉。尤其是身处业务部门时,业务能力要远比技术能力重要,此时不去积极参与需求讨论和分析,就是舍本逐末。
技术设计
业务开发做技术设计的核心,在于模型、接口和数据库表结构设计,在于业务逻辑的抽象能力。少数情况下可能涉及到技术选型的问题,这一点就不在此做概括了。得益于公司大中台小前台的组织结构,前台业务并不需要关心技术问题的具体实现,包括但不限于数据的读写性能、网站的并发能力、搜索引擎的数据同步、异地多活等等技术问题,使得业务开发完全投入到业(ban)务(zhuan)中。
设计像是一门艺术,他不一定有对与错,但是良好的设计确实可以对软件开发带来极大的积极作用,也是一个工程师综合能力的体现。
模型/接口设计
模型是我们为了解决业务问题进行的系统建模,代表了一个模块的职责。
- 系统模型与业务模型
系统模型(也可以说是领域模型)就是业务服务层暴露出的模型。系统模型的设计离不开业务模型,应该在业务模型的设计基础上推导分析得出。而业务模型应该在需求分析阶段输出。因此,需求分析阶段的业务模型极其重要,在一个业务系统架构上,很多模块之间的耦合关系不是技术设计得出的,而是产品设计时就诞生了。当业务模型设计出来之后,我们同样可以用软件工程的角度来评估这个模型和模块的合理性,例如职责是否明确(可以表现在这个页面内的动作职责是否内聚,每个用例与业务模型的关系是否合理)、业务模块之间的依赖能否减少。尤其是业务模块之间的依赖关系,我们希望的能减少就减少,能单向依赖就不要双向依赖。 - 模型设计追求简单
因此在技术设计时,我们可能需要反思模型的合理性,当一个设计变得越来越复杂时,一定要停下来反思,业务模型是否合理,系统模型是否合理,是否有更简单有效的做法。一个优秀的开发工程师,应该将复杂问题抽象化和简单化,而不是将简单问题复杂化。我见过一些简单问题复杂化的例子,甚至我也做过这样的事情,工程师总是喜欢面向未来去做一些设计,做一些框架,但结果总是有好有坏。我认为如果一个复杂设计解决的问题是已知的、普遍的,那就可以放手去做;如果一个复杂设计解决的问题是不确定的、面向未来的,那么存在过度设计的风险,我倾向于用简单的方案。 - 职责明确、单一
模型设计的核心就是明确职责,如果一个模型能够保持一份明确的职责,那么在软件迭代开发中,维护和设计会变得更清晰可靠。职责这个词说起来容易,具体分析时却很困难,很难说有一个明确的划分方式,通常来说,逻辑和数据越内聚的地方,职责就越明确。 - 关于接口设计
接口设计我一直在考虑一个问题,接口的粒度应该尽量细化,还是创建一个大而全的粗粒度接口呢。按照设计原则,接口(方法)应该细化,这样一来职责明确,参数也明确。但是实践中,我们的服务化接口很多是大而全的接口,修改接口都是整个模型扔进去都可以改,这样的好处在于逻辑收敛,接口太细就会有太多接口和方法,维护起来相当困难。总的来说,每个应用越往上层(展示层),接口就要越细化,职责就要越单一;越往下层(业务逻辑层),接口的粒度越粗,通用性也越强。最终应该呈现出一个倒三角形的接口层级。 - 通用性
对于一些业务相关度不高的模块,设计的时候要考虑通用性。例如:打标、快照等模型,设计的时候尽量不要和当前业务有太多的耦合,直接通过out_id+out_type去关联外部业务模型即可。这样设计出的模块更容易复用,更容易诞生出平台化的解决方案。
除了上述,开发人员对于技术模型的设计也是有很大空间的,例如,一个业务模型是否需要拆分成多个系统模型,系统模型的设计是否具有可复用性和扩展性,模型是否清晰易懂?这样看来,模型的设计也是很讲究经验和感觉的,只有不断的尝试和总结,才能摸索出自己的方式。
存储模型设计
系统模型设计完成之后,才可以进行存储模型的设计。相比于存储模型,系统模型才是设计的核心,因此千万不要有先设计数据库再设计领域模型的想法。对于存储模型我们指的都是关系型数据库的表结构,因为事实证明,电商的大多数情况下离不开事务,也离不开关系型数据库。
这里要提一下现在常见的分布式数据库,现在的OLTP数据库为了实现分布式,很多采用的都是分库分表的方式。分库分表的方式缩减了数据库的查询和关联能力,从某种程度上来说变成一种弱关系型数据库。因此在进行表结构设计时,可能需要作出一些反范式的设计。
存储模型大多由系统模型推导出,也可以认为是系统模型的持久化层实现方案。但是存储模型的设计不代表不重要,不同的存储设计带来的维护成本可能差别很大。
- 范式与反范式
- 范式与反范式也可以理解为减少冗余和提高性能之间的权衡。
- 范式可以减少冗余,这对于维护数据的一致性来讲是一个大好消息,毕竟冗余总是有可能带来数据不一致,从而影响系统功能的。这种可能性通常来自于代码缺陷,但是这种缺陷又是很难避免的:因为如果要在代码里维护各个表之间的冗余存储,很容易就把存储逻辑与业务逻辑紧密的耦合在了一起,这种代码会变得复杂+难以维护+容易出错。
- 反范式可以提高查询性能,减少表关联和查询次数。如果完全按照范式设计我们需要多建很多表结构,这些表结构的维护和查询成本也对程序有很大影响。现实中我们会看到很多反范式的表结构,这些可能是为了克服分库分表带来的查询成本,也可能是为了存储模型简化、减少表关联等。
- 存储设计也没有绝对的对错,但是我倾向于用范式设计来减少冗余,用搜索引擎来提高查询能力,用反范式来做模型简化。冗余我认为在大多数情况下都要尽量避免的,至少我遇到冗余带来的痛苦比带来的好处要多很多;相对应的查询能力的削弱,可以用搜索引擎来解决,我相信每个业务都会有接入搜索引擎的经历;
- 最后一点“用反范式来做模型简化”我解释一下,范式设计会带来很多表,例如N:M的实体关系通常会建立一张关系表。但是如果拆分出的表可以用其他简单的方式代替,例如原表加字段,甚至存json格式的文本,那么这种方案也可以列入考虑范围。严格的关系模型设计的确会带来复杂度,在一些简单且不需要考虑扩展性的场景下,还是需要用一些方式来简化我们的表结构的。
- 可扩展性
不要以为存储模型的设计会简单一些,存储模型的设计依然需要带着业务的思考来看待。具体表现就是存储模型需要有优良的可扩展性,迎合业务发展方向。毕竟模型变更只需要改代码,存储结构变更就可能要做数据迁移了。例如两个实体的关系,究竟是1:1还是1:N甚至是N:M呢,这一点上很有可能在业务上发生变化,而每次这种变动对于底层数据来说都是一项大工程。类似的,哪些系统模型需要拆成多个表结构,哪些系统模型可以合并在一张表里,这些决定很可能不是因为上面讲的范式设计,而是开发者对未来发展方向的考量。不过我还是保持上面的观点,除非已经了解到了未来的业务规划和业务需求,面向未来的事情做起来还是谨慎为好,难以确定的时候尽量选择简单的方案。
其他
- 代码结构
模型定下来之后,其他的设计内容通常不会有太大的困难和变数。当然,对于一些复杂的业务模块,代码结构的设计也很重要。 这里不需要给出详细设计文档一样的东西,其实想清楚大概要用的设计模式和大体的结构即可。毕竟我相信好的代码结构是不断重构出来的。 - 配置化
业务逻辑复杂起来以后,配置化是一个值得考虑的方向:例如利用流程引擎去解决复杂的流程分支,或者利用元数据驱动去提高相似产品的开发效率。代码毕竟还是属于比较具体、接近于机器的逻辑表述,如果上述两种方式能够提高代码的可维护性,你又有能力设计出一套适合当前业务场景的框架,那么就大胆的尝试吧。
结语
软件开发的流程很长,但是关键的节点往往都是在前期的需求和设计阶段,毕竟这些都是在项目初期决定着项目未来方向的重要事项。工程师的价值不仅仅在于编码实现,更应该在于设计和分析。想清楚再做,往往可以避免很多无用的体力开发工作量。