本文来源:http://r6d.cn/BEUZ
说明:生鲜电商属于一个软件的产品,那么如何做好代码设计呢?代码设计,是程序员做项目时,在coding之前非常重要的一个步骤,可以说关系到整个系统、整个团队的研发质量和效率。一般说代码设计,可能涵盖以下几种:
- 整体设计
- 架构设计
- 领域模型设计
- 数据库设计
- API设计
- 代码实现设计
代码设计的前提是,项目组成员已经完成正式的需求评审,并经过充分思考:
- 这个需求是为什么业务目标服务的?
- 这个需求描述的内容,是否为服务该目标最合适的方式(包括研发性价比、项目周期等)?
- prd本身是否逻辑自洽?
- 是否每个内容点都可实现?
- 实现的技术方案是什么?
- 是否做过类似的功能,合并吗?复用吗?拆解吗?
整体设计与架构设计
“项目的整体设计,有时会涵盖系统架构设计,这里要区分一下,系统架构设计并不完全等同于代码架构设计。 ”
整体设计首先要考虑的,是当前项目是要做一个全新的项目,还是要做原有项目基础上改造、迭代;项目组的积累中,是否有可以复用的地方(模块或成熟方案),是否有可以通过改造以符合新项目需求的可能。
其次再考虑,如果是新起项目,要如何搭建整体实施方案,内容一般包括:
- 硬件部署与资源申请:硬件和资源,是要和业务需求结合来制定的,比如业务最大访问、TPS/QPS等,要切实讨论得出一个数据范围,以确定系统是否做高并发方案。另外还要考虑容灾等问题,以此制定系统高可用的设计。
- 分析项目特别突出的业务、技术难点:如千人千面的UI和查询,或灵活配置的业务模式,类似这种需求的项目,会在模块模型设计上做额外处理,可能是将各种规则单独做一层规则引擎,也可能是在数据建模时增加更多维度;再比如超大的QPS,可能要在整体架构上,添加额外的中间层,异步收集数据等功能;还有就是依赖于审核的迭代上线周期(IOS、微信小程序)等,都要在整体设计中考虑进去。
- 内外部信息交换通讯:整体设计系统,要明确划定项目范围,哪些是本系统的功能,哪些功能、数据依赖其它系统或模块。要明确此次项目对外交互系统的访问关系和访问条件。
- 数据的持久化存储方案,如何选择硬件,如何选择软件,一般有常规解决方案。特别要考虑和需求结合的一点是业务数据的生命周期,这也是数据归档方案等重要依据。
“整体设计中非常重要的一部分是系统架构设计,要在业务边界确定的前提下进行。 ”
首先,从业务视角进行拆解功能,定义系统由哪些模块构成。
其次,根据业务复杂度、模块功能性划分,选择合理的软件架构模式及方案。
比如,业务需求上,有非常复杂的流程处理,但各个业务子模块之间相对独立,则可以选择事件驱动型架构,基本上用状态机、工作流就可以满足功能了;再比如做微服务架构设计上,也不一定都做集中式负载均衡,如果整体功能需求对消息流转要求比较高,则可以考虑中心消息型服务(比如Rocketmq)。
以上两步更多是从需求上分析,偏近于从业务角度设计系统。后面两步要从技术上做架构设计。
下一步要根据模块划分和架构方案选择合适的开发语言和技术框架。生产上一般考虑优选Java语言 Spring框架,但也有许多混搭的成熟方案,有些基于JVM的开源框架中对多语言的支持还是比较好的。
再下一步要将系统拆解,一般考虑划分出展现层、(通讯层)服务层、数据层,再根据需求为每一层设计合理的服务及组件。如展现层上web还是mobile,服务层是用Spring Boot还是Spring Cloud做微服务,数据层使用Mysql还是Oracle等。另外还有项目服务等通用组件及各种业务、技术中间件、及整体项目通用的技术方案选型,如日志(ELK等)、监控告警,访问授权,token认证等。
整体设计要在需求明确(不一定所有细节完全敲定)的前提下尽心,一般形成定案要在正式需求评审会之后。
整体设计由团队leader或项目owner完成。
整体设计文档,优先考虑使用物理部署图、逻辑架构图、业务流程图等描述系统架构。
另:特别建议在需求评审会后,首先由研发lead组织召开设计讨论会议。目的是让项目成员尽早的参与到项目之中,并使用讨论到方式,更好的理解项目及整体设计方案。
一般设计讨论可用头脑风暴方式进行会议。有重大分歧时,可以投票表决。
此讨论会议召开前,需要leader提前设定议题,会议上设有1名主持人(可leader兼任或者team member轮流担任),按照议题集中,轮流发言再集体讨论方式得出结论。
主要议题包括:整体设计方向、项目人员大致分工、待解决疑问和处理人。
领域模型设计与数据库设计
代码语言:javascript复制在整体架构设计完成后,
要针对已经拆分的系统模块做模型设计,
尤其是在项目需求中有重要功能的部分要重点设计。
领域建模要深度结合需求,从业务角度出发,用一种自顶而下的方式来建立模型。
领域建模方法很多,最重要的原则是在项目模型建立之前,要先做概念的统一整理,要对特定概念的名词做专业命名及能力约束。在此基础上,再进行重叠功能的归并和抽象。需要注意的是,此处制定的模型,和业务需求、数据库设计、代码类设计,都是一脉相承的,但并不完全等同。比如需求中有订单Order的概念,在设计订单Order都有哪些元素构成,可以实现什么操作时会发现,要在数据库和代码中,拆分为OrderHeader和OrderDetail。
在领域模型中,还有一个重点,是要标注清楚各抽象概念之间的数据关系和约束。一般会比较关注数据之间是一对一、一对多、多对多等关系,并在此基础上,结合业务流程泳道做系统模块依赖关系图、数据流图等。
数据库一般结合领域模型设计,是领域模型持久化存储的映射转化(ORMapping)。在项目数据库设计中,除了常规关注的范式、mysql约束等(单独写过mysql应用的usage,此文略),额外关注:
1、表字段在整体系统中的规范定义和统一使用。比如相同的概念,在各个表字段中的定义要一致,(在代码应用中也应保持一致)。
2、数据库事务的应用方案。
3、冗余字段与代码简洁实用的平衡。
4、特别说明,数据库一般仅作数据的持久化存储,不要将业务逻辑的处理放到数据库中进行。
5、生产业务数据delete,应该仅作逻辑删除,不做物理删除。
6、表设计要和性能结合起来,特别当DB成为性能瓶颈时,要特别斟酌索引设计的合理性。
模型设计一般为团队leader或项目owner完成,数据库设计一般为leader带领team member一起完成。
数据库设计一般要先于具体功能代码实现,在做此设计后,要针对存储方案和底层数据结构设计,做double check和集中评审,评审内容包括存储介质选型、表结构设计能否满足技术方案、存取性能和存储空间能否满足业务发展、表或字段之间的辩证关系、字段名称、字段类型、索引等。在评审一致通过后,沉淀为文档。
API设计与代码实现设计
代码语言:javascript复制在完成整体设计后,
要将所有功能拆分成独立可调用的API。
这里的设计要考虑系统实现和业务需求的结合。
系统API拆分,一定是从需求实现角度考虑,
基于领域能力做的,且要充分考虑后续需求的变化演进,
尽量使更多后续功能变化在既有规定服务API的框架内继续演化。
此外还要关注非功能性需求,比如安全性、可用性、可扩展性等。
最后,在这个步骤要关注的点,
除了系统主干正向流程外,还有逆向流程、异常流程、
业务边界等方面的接口定义。
API是系统模块对外提供的服务,现行系统基本满足前后端分离的框架使用条件,所以一般可以简单理解为前端和系统交互的入口。但实际系统设计中,API不仅仅提供给前端使用,所以每个API的实现设计是系统模块设计中非常重要的环节。
API的调用方一般会考虑给展示层多端调用(web、mobile等),还要考虑相同的API是否可以给其它系统模块使用,最后一层设计是,是否用相同的API对外提供openAPI服务。设计中应当特别考虑此类API的功能聚合、分离,多场景适用性等。以订单列表查询为例,设计一个订单列表query接口:
- 给前端页面查询,前端分页每次传参。这个场景最大的特点是,查询页面会设计较多分类查询的筛选条件,且此类设计实现经常可能是查询条件直接投射到DB上。
- 在整体系统中,给其它模块使用,如用户模块或报表模块。这个场景的特点是,如果其它模块功能为同步调用,则QPS不可预测,比如用户模块使用了这个订单query接口,那么这个接口的性能就会成为用户模块的瓶颈。还有一种可能是其它模块用此接口异步访问同步数据,就很有可能采用定时任务方式,固定分页,并发调用查询,如每5分钟调用一次,每次调用并发20,每个访问查询500分页数据。
- 给openAPI使用。如果开放给前端的query服务提供给开放平台直接使用或包装后直接访问,则容易出现的场景是,每次调用查询不确定分页,很有可能一个大分页(如十万)就打到DB上,这样即使索引匹配也容易造成数据库缓存区拥堵。遇到这类需求,
3.1 要考虑一个API接口是否可以满足所有需求,是否对数据访问做权限隔离。即,考虑所有的服务都集中到一个API上,还是定向拆分,将一个内部实现core,分别投射到多个API上。
3.2 不同访问端如果有不同的QPS需求,还都考虑到,单个特大QPS接口,可以横向合并,即,不根据业务约束,而是把所有大访问的接口拆出来,给到单独技术架构和硬件部署的服务里。
3.3 是否内部实现上一致,是否使用缓存、中间层方案等。
- 数据库设计尤其是索引设计要和接口设计(尤其是筛选条件)保持一致。
API的设计还有一个维度的考虑,是基于数据交互考虑。当两个系统模块要使用API交互数据时,定义的API要充分考虑使用场景,并做技术选型:
4.1. 数据是推还是拉?
4.2. 同步推送还是异步通知回调?
4.3. 通过接口还是MQ?是否需要削峰?
4.4. 是否需要保证强一致性?
4.5. crontab定时发起还是任务队列发起?需要延时队列、死信队列吗?
API拆分完成后,要做代码实现设计。此设计主要关注每个API的内部实现,将一系列领域模型转换为系统对象的类设计。这里面有3个图可以辅助设计:
1、如果一个API复杂度较高,调用链路上的涉及对象较多,可以使用时序图来表达并且明确各调用环节的输入与输出,以反映对象间的交互与协作关系。时序图对完成设计评审、辅助项目开发有很大作用。
2、如果某个业务对象的状态较多,可以使用状态图来表达并且明确状态变化的各个触发条件。首先明确对象有多少种状态,然后明确两两状态之间是否存在直接转换关系,再明确触发状态转换的条件是什么。状态图对测试用例有很大帮助。
3、如果系统中模型类超过较多,且存在复杂的依赖关系,可以使用类图来表达并且明确类之间的关系。类图对复杂系统设计,尤其是灵活配置、路由映射、设计模式应用等,有一定帮助。
类的设计要充分考虑单一原则。应当优先使用聚合/组合的方式来实现。不得已使用继承的话,要使父类能够出现的地方子类一定能够出现。根据依赖倒置原则,尽量依赖抽象类与接口,有利于系统的扩展与维护。
在设计抽象时,要考虑以下问题:代码直观吗(好的代码自注释性很强),它的编写巧妙吗?实现细节可能隐去了吗?程序编写是立足于问题域而不是计算机科学或语言结构域吗?
程序开发有一个场景比较典型,写第一版需求时,仅仅是一个简单功能,实现也比较简单,但后续功能增加很多,变化很大,每次在原有类定义基础上增加功能,倒置代码冗余,尤其容易造成if-else太多。此时要考虑提前预估功能,做扩展性设计,或者在每次功能迭代中,做小版本重构。比如订单明细查询,在定义查询接口(interface)后,需求要增加一个千人千面功能,不同用户访问返回的内容条目不一样。如果用if-else或switch写,会比较不好管理,代码也容易混乱,这里可以新设计一个接口,做不同内容配置,然后组合使用,或者采用其它设计模式。
设计模式的目的,是辅助程序员更好的实现代码抽象,将现实业务逻辑,映射到抽象维度的代码语言上。一般生产上经常用到工厂抽象工厂、模板方法、策略、状态等。选择合适的设计模式和数据结构,有助于提升代码的清晰简洁度。
这个层次的代码设计一般交给team member完成,并输出接口定义、接口详细设计、包括一些数据库的DDL等。
设计评审与文档中心
在项目实施之前,设计评审是非常重要的环节。研发lead应该组织内部设计讨论,每人的接口设计要通过peer或backup的review,更好的方式是通过集中评审。
因为再好的设计,也要确保项目组所有成员,理解正确且一致。这个不仅仅是lead对团队的灌输,要确保组员之间对同一个业务概念的理解也是正确且一致的。要确保每人的接口设计,都符合整体设计需要。要确保项目级别领域定义符合更上层定义(如公司级别命名),要确保项目级别领域定义统一、代码实现中英文命名统一。大型项目,在dev内部设计通过后,也可以由项目经理组织产品、研发、测试各方展开设计评审,此处尤其要和prd结合,为整理测试用例服务。
接口细节的实现也应当是动手coding之前先做设计,并建议形成文档,研发lead要提前组织好开发文档中心,整组统一设计文档格式。
一般常规内容包括:
•新项目背景、或常规迭代项目里程碑
•项目管理的时间节点(需求评审时间、设计时间、提测时间、上线时间点)
•本期项目概要设计说明
•分工(API、完成人、预估工时、实际工时等)
•详细设计:接口实现设计、DB设计、缓存设计等
•上线计划等等