小架构step系列24:功能模块
1 概述
开发一个业务系统的时候,功能可能非常多,需要对功能进行分组,这里引入“模块”的概念,它就代表着一组功能。对于程序员来说,“模块”这个概念可能容易混淆,比如python语言里也有Module这个概念,翻译为中文也是“模块”,在python中一个模块就是一个代码文件。所以为了有别于其他“模块”,这里专门指功能分组的“功能模块”。如果觉得“模块”这个词不好理解,也可以换成“应用”,本文采用“模块”来说明。
2 模块的规则
2.1 如何分模块
仅“功能模块”这个概念并不好理解,虽然把它定义为一组功能,但“功能”这个词也比较泛,不同人的理解不一样。这里讲几个分模块的原则,增进一点对模块的理解。
首先是业务分离。比如“物业”这个概念,在一个小区里物业管理包含安防巡逻、设备报修维护、清洁保洁等基础业务,即业内常说的“三保”业务。不同的业务不宜合并在一起,合并在一起就互相受影响,不好维护。
然后是角色分离。一般业务系统都至少有三类角色:服务提供方、服务使用方、系统管理员。既然是业务系统,那就需要提供服务,提供服务的那方就是服务提供方,使用系统所提供服务的那一方就是服务使用方。系统管理员负责系统的初始化配置和基础运维,通常代表系统的所有者(如系统的采购方)。当然角色可能还有更多,比如对于一个SAAS平台,可能还有个平台管理方,它管理着所有的服务提供方和服务使用方,系统也带了这方需要使用的功能。
对于同一类功能,如果涉及到这些角色的,建议按角色分成不同模块。比如物业报修,物业接受维修申请、提供维修服务等,属于服务提供方。看到走道的灯坏了向物业方提交一个说明灯坏了的信息(报修),并享受物业维修灯之后走道有灯光的服务,这方就是服务使用方。在物业方内可能还分两个角色,一个是提供维修服务的业务部门,一个是对维修功能进行授权或者初始配置的部门,前者对应前面说的服务提供方角色,后者则是管理员角色。这个时候可以分为“物业报修”、“报修管理”、“报修配置”三个模块,分别对应不同角色。
从这个角度看,模块之所以按功能分组,其实隐含着一个“权限”在后面,不同的功能使用者不一样,对应的权限不一样,分成不同的模块的好处之一是方便授权。如果上面物业报修例子中只有一个模块,那么分权限的时候就必须在模块下进行细化分配权限,当细化权限过多的时候分配起来就比较困难,而分模块设计后,实践中通常直接按模块分配权限即可。
2.2 统一模块
功能模块是针对整个系统的功能进行分组,天然地有一个“全局”统一的要求。模块最重要的信息有两个:ID和名称。ID是功能模块的唯一标识,这个标识需要全局唯一,需要在整个系统中统一分配。也就是模块的ID需要硬编码,在系统外部统一维护,ID需具备业务语义,能明确映射到具体业务域,在代码中可能需要拿来做逻辑的。
如果业务公司里有产品设计,最好是由产品统一维护,每设计一个功能,就需要明确这个功能属于什么模块。这样比较方便传递到后面的开发和测试,使得大家概念统一。
每个业务系统可以有自己的ID定义。这里建议模块ID用四位数字来表示,前1000个留给系统内部使用,主要是应用于一些不对外的模块,比如发短信发邮件,如果这个发短信或发邮件没有界面、也没有相关管理,就是发一下和记录一下,此时分一个模块感觉有点多余,不分模块可能又无法满足后面的一些权限、错误码等要用模块ID的要求,可以定义一个通用模块“通知服务”,这种模块产品也不关心,就由开发和产品商量使用前1000个ID来定义。
后面1001到9999还有将近9000个ID供业务使用,如果觉得系统足够大不够用,那么要么增加id位数,要么换其它方案。过大的系统就不符合本文“小系统”的范畴了,在此略过。选四位来表示ID,是因为后面会用模块ID来作为错误码的一部分,不宜过长。太短则不够用,所以选一个折中的长度。
2.3 功能粒度
可以认为模块是在代码层面,系统管理功能的最小粒度。比如服务A看到服务B的时候,看到的是服务B里的模块,这个在微服务体系里更加明显。有了这个关联关系,就比较容易识别一些信息,比如某操作来源于某模块等。以发短信为例,发短信需要定义一个模板,这个模板属于什么业务的,应该有个模块对应;短信最终是由什么业务发的,应该对应一个模块。
上面说“代码层面”,这个是站在写代码如何组织功能对应代码来说的。如果是从运行层面,还有可能一套代码对应多种业务的情况,比如有些业务的流程太像了,如列一些课程供选择,选择后提交申请,这个流程和列一些活动供选择然后提交申请的流程几乎是一样的,如果把代码写成一套,那么模块应该就只有一个,但在功能上应该是两个功能,此时需要在模块的基础上再分一下,此时功能管理的最小单位不再是模块,需进一步拆分为子模块或功能点。这种情况不多,也不建议用这种方式,上面两种功能虽然初始的时候看起来好像“流程”一样,但如果它们真的是一种服务,就随着时间的延长,很可能会发展出更多独特的功能,到那个时候可能就“后悔”把这么小的“相似”抽象成一类功能,在扩展和维护上都非常困难。在这个角度上,更偏向分开,虽然初始的代码“拷贝”了一遍多了一点代价,但后续维护和扩展的成本显著降低。
2.4 模块实现
功能模块类是一个不变类,一经初始化就不能再修改,只提供getter,不提供setter。
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Value;@Value
@Schema(description = "功能模块")
public class FunModule {@Schema(description = "模块ID", requiredMode = Schema.RequiredMode.REQUIRED, minimum = "1")Long id;@Schema(description = "模块默认名称", requiredMode = Schema.RequiredMode.REQUIRED)String name;public FunModule(Long id, String name) {this.id = id;this.name = name;}
}
3 架构一小步
统一概念“功能模块”,统一维护,统一分配唯一标识。