======== 接上文《软件设计不是CRUD(6):低耦合模块设计实战——组织机构模块(上)》
组织机构功能是应用系统中常见的业务功能之一,但是不同性质、不同行业背景、不同使用场景的应用系统对组织机构功能的要求可能完全不一样。所以使用这样的功能对低耦合模块设计进行示例性的讲解是比较具有代表性的。在后续的几篇文章中,我们会首先进行示例的详细讲解,然后再基于这个示例进行理论讲解。
4、做一个默认实现
如上图所示,由于组织机构模块可以有多种实现方式;例如可以研发一套基于本地关系型数据库的实现,也可以研发一套基于远程调用的实现,还可以研发一套基于ES搜索引擎的实现。这里本文来演示一套基于本地关系型数据库的实现,这套实现作为产品研发团队提供的默认组织机构实现提供给上层项目团队/二次开发团队使用(由于篇幅有限,本文尽可能演示其中的关键代码),二次开发团队如果不满意这套实现,可以基于这套实现进行调整也可以对这套实现进行整体替换,但都不会影响其它模块对该模块的调用——因为其它“可以看到”组织机构的模块,依赖的都是组织机构模块提供的接口。
4.1、对接口的补充说明并建立工程脚手架
如上图所示,组织机构模块基于本地关系型数据库的实现,工程名被定为simple-org-local(pom文件中定义为(simple-org-local-starter),使用者可以通过在应用程序POM文件中设定simple-org-local-starter依赖的方式,将组织机构模块基于本地数据库的实现引入到应用程序中运行。另外要说明的是,笔者在实现过程中针对之前SDK设计考虑不周的问题,对组织机构接口做了细微调整,在进行正式介绍前,需要向读者先说明这些细微调整:
- 在OrganizationModuleRegister接口和UserMappingModuleRegister接口中,增加了返回具体模型类型的方法。这是因为在实现过程中发现如果二次开发团队需要依赖当前的默认实现调整一些模型的字段情况,则需要提供一个指定具体模型类的方法:
// 对OrganizationModuleRegister接口进行的调整
public interface OrganizationModuleRegister <O extends Organization> extends Ordered {
// ......
// 该方法用于返回具体要转换的组织机构class类型
public Class<? extends O> getOrgClass();
// ......
}
// 对UserMappingModuleRegister接口进行的调整
public interface UserMappingModuleRegister <M extends UserMapping> extends Ordered {
// ......
// 该方法用于返回具体要转换的组织机构-用户class类型
public Class<? extends M> getMappingClass();
// ......
}
- 提供支持简单列表结构的组织机构模型。这是因为在实现过程中发现有的项目团队可能不需要组织机构支持树形结构,只需要提供对单纯二维表结构的支持。最简单的方式就是将组织机构模型,分为两个有继承关系的独立接口:
// Organization模型接口不具有树形结构的描述特点
public interface Organization {
// 组织机构类型
public String getType();
// 在组织机构类型下,唯一的组织机构业务编码
public String getCode();
// 组织机构的中文名
public String getName();
}
// ...................
// 继承他的TreeOrganization模型接口,才具有描述树形结构的要求
public interface TreeOrganization extends Organization {
// 组织机构携带的下级组织机构信息(组织机构特性字段)
public <O extends Organization> List<O> getKids();
public void setKids(List<? extends Organization> kids);
// 组织机构直接携带的人员绑定信息(组织机构特性字段)
public <M extends UserMapping> List<M> getUsers();
public void setUsers(List<? extends UserMapping> users);
// 获取当前节点可能的父级节点类型(注意:可能没有)
public String getParentType();
// 获取当前节点可能的父级节点编号(注意:可能没有)
public String getParentCode();
}
以上调整都是对上文介绍的组织机构接口的细微调整,都不会影响读者对于实现的理解,也不会影响本示例所要体现出的低耦合设计思想。接着我们就可以进入正题了,首先给出这个本地数据库实现的具体脚手架(以及脚手架中重要的实现类),然后再对脚手架中重要的实现类进行详细介绍。
其它一些关注度不会太高的代码包还有基于JPA的数据库模型entity包,以及数据库操作接口repository包,还有工具包utils(实际工作中,这些工具包会统一放置在一些更下层的模块中)等等。读者可以在作者的下载空间中,对这些详细代码进行下载。
TODO 代码还没有上传
注意:spring.factories文件的设置方式是spring-boot 2.7 之前版本的设置方式,在spring-boot 2.7及后续版本推荐/要求使用org.springframework.boot.autoconfigure.AutoConfiguration.imports文件的设置方式。读者自行进行脚手架验证时,需要注意这个细节。
4.2、建立和注册具体的业务模型
上文已经多次提到,在组织机构模块的接口定义中,关于“组织机构具体的模型结构”这件事情,只是进行了一个抽象描述,既是:只要具有类型、类型下唯一编号的模型,就可以是一种组织机构(实现Organization接口)。如果这种组织机构需要支持树形接口,则另外需要规定父级组织机构的信息和下级组织机构的信息(实现TreeOrganization接口)。
那么在具体的实现中就需要对组织机构的模型结构进行详细描述了,由于在这个默认实现中,组织机构类型是需要支持树形结构的,所以需要实现TreeOrganization接口:
DefaultOrg类的详细代码,如下所示:
/**
* 这是标准产品提供的默认组织机构实现,其中默认可以关联各种类型的下级组织机构和各种类型的用户
* 为了简化代码,这里使用了lombok组件,以降低不必要的代码
* @author yinwenjie
*/
@Getter
@Setter
public class DefaultOrg implements TreeOrganization {
public static final String DEFAULT_ORG = "default";
private String id;
// 组织机构类型
private String type = DEFAULT_ORG;
// 业务编号
private String code;
// 组织机构上级组织机构编号
private String parentCode;
// 组织机构上级组织机构类型
private String parentType;
// 中文名
private String name;
// 组织机构完整称呼(组织机构特性字段)
private String mainName;
// 组织机构邮件地址(组织机构特性字段)
private String mail;
// 组织机构电话信息(组织机构特性字段)
private String phone;
// 组织机构携带的下级组织机构信息(组织机构特性字段)
private List<? extends Organization> kids;
//组织机构直接携带的人员绑定信息(组织机构特性字段)
private List<? extends UserMapping> users;
}
当然,除了默认组织机构的详细结构描述外,组织机构的默认实现中还实现了一个默认的用户关联信息。该关联信息的数据结构,实现了UserMapping接口,代码如下所示:
/**
* 这是默认的组织机构、用户关联信息
* (实际上生产系统的情况下,这个关联用户结构应该由上层用户模块进行定义)
* @author yinwenjie
*/
@Getter
@Setter
public class DefaultMappingUser implements UserMapping {
public static final String DEFAULT_MAPPING_ORG = "default";
// 唯一的用户账号信息
private String account;
// 用户中文姓名信息
private String describer;
// 用户昵称信息
private String name;
// 用户年龄(特异性业务字段)
private Integer age;
// 用户检查次数信息(特异性业务字段)
private BigDecimal checkPoint;
// 业务申请次数(特异性业务字段)
private Integer requestNum;
}
注意事项:类似的这种用户关联信息,在实际工作基本不会放置在本模块的实现中,而是直接放置在需要和组织机构信息建立关联的上层模块中(例如上层的管理员用户模块、监控员用户模块),这样做有几个原因:
-
重用上层模块的模型结构代码:上层模块中需要和组织机构信息建立关联的信息结构,只要直接实现UserMapping接口,就可以将模型结构关联到组织机构模块中,不需要上层模块做额外的代码增加,也不会发生因为上层模块需要将信息关联到组织机构,而改变自身模型结构的情况。
-
将关联关系的具体实现交给上层模块,保证在上层模块对下层模块透明的情况下,支持更多关联模块的扩展:本系列文章中多次提到,为了保证系统中模块分层的稳定性,上层模块是对下层模块透明的。换句话说下层模块是不知道其上层有什么模块,所以下层模块当然无法知晓上层有哪些模块需要建立和组织机构的关联。
以下代码是具体的组织机构模型和用户关联模型的注册实现(DefaultOrg和DefaultMappingUser两个模型):
// 注册组织机构模型(默认组织机构)
public class DefaultOrganizationModuleRegister implements OrganizationModuleRegister<DefaultOrg> {
// 默认的组织机构类型,排序优先度很高
@Override
public int getOrder() {
return 1;
}
@Override
public String type() {
return DefaultOrg.DEFAULT_ORG;
}
// 这里描述json结构到具体DefaultOrg模型的转换
@Override
public DefaultOrg transform(JSONObject json) {
// ......
}
}
// =============
// 注册示例中需要的组织-用户关联模型
public class DefaultUserMappingModuleRegister implements UserMappingModuleRegister<DefaultMappingUser> {
// 默认这个模型注册器所转换的对象模型,排序优先级最高
@Override
public int getOrder() {
return 0;
}
// 具体描述json结构和用户关联信息的转换
@Override
public List<DefaultMappingUser> transform(JSONArray users) {
// 只有要求转换的用户关联信息,其type都为default的时候,才进行转换,否则报错
// ......
}
}
4.3、建立具体的业务行为
建立了具体的业务模型后,现在需要进行具体的业务行为描述。业务行为在本示例中可以简单理解为对业务模型的增、删、改、查操作。这就是组织机构模块中,OrganizationStrategy接口和UserMappingStrategy接口存在的原因。
注意:以上两个策略接口中的行为只是需要接入组织机构模块统一控制的各种模型行为,不包括某种组织机构类型、某种组织机构-用户关联类型特有的行为。例如,作为组织机构模型来说,一定存在创建、修改和层次结构查询的要求,这些属于对于组织机构模型最基本的要求。另外,不同的组织机构类型还有定制化的行为,例如“组织机构类型A”还允许进行临时禁用操作。而这些针对不同组织机构类型的定制化行为,并不属于OrganizationStrategy接口和UserMappingStrategy接口的控制范围,需要二次开发团队根据自己的需要,自行定义并实现(一般在Service层定义新的接口,且这些接口和实现只存在于项目使用的范围)。
- 首先是默认组织机构的业务逻辑描述
public class DefaultOrganizationStrategy implements OrganizationStrategy<DefaultOrg> {
// 默认实现内部使用的数据持久层功能
@Autowired
private DefaultOrgRepository orgRepository;
@Override
public String type() {
return DefaultOrg.DEFAULT_ORG;
}
// 创建默认组织机构信息
@Override
public DefaultOrg create(DefaultOrg org) {
/*
* 这里进行默认组织机构的实际创建,过程为:
* 1、进行和业务相关的边界校验
* 2、进行数据层的保存
* 注意,这里不进行监听器的激活,因为它不属于实际的创建过程
* */
// 1、=============
// 验证和业务相关的字段
String mail = org.getMail();
Validate.notBlank(mail , "创建时,邮件信息必须填写");
// TODO 还能进行邮件字段的其它业务相关性验证,例如邮件是否合规,是否被使用等
String name = org.getName();
Validate.notBlank(name , "创建时,姓名信息必须填写");
String phone = org.getPhone();
Validate.notBlank(phone , "创建时,电话信息必须填写");
// 2、=============
// 转换成默认实现内部使用的数据持久层对象,并进行入库操作
DefaultOrgEntity orgEntity = this.transform(org);
this.orgRepository.save(orgEntity);
// 保存后,数据层会生成一个id
org.setId(orgEntity.getId());
return org;
}
// ...... 其它方法实现过程略
// 包括必要的查询、修改操作等
}
- 然后是默认组织机构-用户映射方式的业务逻辑描述
public class DefaultUserMappingStrategy implements UserMappingStrategy<DefaultMappingUser> {
// 默认实现内部的数据层功能
@Autowired
private DefaultMappingUserRepository defaultMappingUserRepository;
@Override
public int getOrder() {
return 1;
}
@Override
public String type() {
return DefaultMappingUser.DEFAULT_MAPPING_ORG;
}
// 创建新的组织机构-用户映射信息
@Override
public DefaultMappingUser create(DefaultMappingUser userMapping) {
// 1、========== 边界校验
Integer age = userMapping.getAge();
Validate.notNull(age , "创建关联信息时,人员年龄信息必须填写");
// TODO 当然还可以进行更多的正确性验证
Integer requestNum = userMapping.getRequestNum();
Validate.notNull(requestNum , "创建关联信息时,人员申请次数必须填写");
// 2、========== 进行数据层模型转换
DefaultMappingUserEntity userMappingEntity = this.transform(userMapping);
this.defaultMappingUserRepository.save(userMappingEntity);
userMapping.setId(userMappingEntity.getId());
return userMapping;
}
// ...... 这里有一些私有方法和其他业务行为
@Override
public Collection<DefaultMappingUser> queryUsers(String parentType, String parentCode) {
// 进行边界校验后,就进行数据库查询
if(StringUtils.isAnyBlank(parentType , parentCode)) {
return Lists.newArrayList();
}
List<DefaultMappingUserEntity> userMappingEntities = this.defaultMappingUserRepository.findByOrgTypeAndOrgCode(parentType, parentCode);
if(CollectionUtils.isEmpty(userMappingEntities)) {
return Lists.newArrayList();
}
// 如果存在查询结果,则转换为对应的业务模型,然后进行输出
List<DefaultMappingUser> mappingUsers = this.transform(userMappingEntities);
return mappingUsers;
}
}
注意:和UserMapping接口、UserMappingModuleRegister接口类似,在实际工作中对于UserMappingStrategy接口的实现也会交给应用系统中处于组织机构模块上层的,需要接入组织机构模块建立组织机构-用户关联的模块进行实现。出现这种情况原因在上文中已经介绍过了,这里不再赘述。
4.4、将业务行为进行间接耦合
在《软件设计不是CRUD(5):耦合度的强弱(下)》文章的描述中,我们对间接耦合的概念进行了详细描述。总结来说,就是模块内部没有业务过程,只负责按照一定的逻辑将业务过程组装起来,且处理逻辑本身是可以变化。
为了使模块的耦合强度下降到间接耦合,我们需要在模块中设计一个描述业务过程组装方式的中间层,让每个业务逻辑能够按照一定的工作顺序运行起来(称为处理逻辑)。在本示例中,我们选择传统编程结构中的Service层作为这个处理逻辑的描述层。
public class OrgServiceImpl implements OrgTreeService {
// 这里是系统中已经注册的组织机构行为
@Autowired(required = false)
private List<? extends OrganizationStrategy<? super Organization>> organizationStrategies;
// 这里是系统中已注册的组织机构-用户关联行为
@Autowired(required = false)
private List<? extends UserMappingStrategy<? super UserMapping>> userMappingStrategies;
// 这里是系统中已注册的组织机构模型具体的类型描述
@Autowired(required = false)
private List<? extends OrganizationModuleRegister<? super Organization>> organizationModuleRegisters;
// 这里是系统中已注册的需要监听组织机构模块事件变化的具体监听器
@Autowired(required = false)
private List<? extends OrgListener> orgListeners;
@Override
@Transactional
public Organization create(Organization org) {
/*
* 在进行新的组织机构查询时,该service方法中并没有具体的业务逻辑过程
* 而是描述了一个控制过程,具体的处理逻辑在OrganizationStrategy接口的某个具体实现中进行实现
*
* 1、进行边界校验,只对必要的类型信息进行校验,因为处理类型信息以外,本控制过程不需要关注其它业务字段的校验问题
* 2、通过类型信息找到对应的处理策略,并检查节点是否已经存在,然后进行正式创建操作
* 3、进行了组织机构基本信息的添加后,再根据情况确认是否要进行携带的用户关联信息的添加
* 4、创建成功后,本处理逻辑会触发事件监听,将创建成功后的状态通知到上层需要知晓该模块数据变化的模块
* */
// 1、================
Validate.notNull(org , "创建时,需要传入必要的组织机构信息");
String type = org.getType();
Validate.notBlank(type , "创建时,必须传入组织机构类型(type)信息");
String code = org.getCode();
Validate.notBlank(code , "创建时,必须传入组织机构编号(code)信息");
Validate.isTrue(!CollectionUtils.isEmpty(organizationStrategies) , "创建时,未发现任何已注册的处理方式");
// 检验可能存在的父级节点信息
if(org instanceof TreeOrganization) {
TreeOrganization treeOrg = (TreeOrganization)org;
String parentCode = treeOrg.getParentCode();
String parentType = treeOrg.getParentType();
OrganizationStrategy<? super Organization> parentOrganizationStrategy = this.findOrganizationStrategy(parentType);
Validate.isTrue(!StringUtils.isAllBlank(parentCode , parentType) && !StringUtils.isAnyBlank(parentCode , parentType)
, "创建时,要么父级信息全部完整填写;要么都不填写");
// 只有两者都完整,才任务当前节点设定了父级节点,如果有父节点,则要确认父节点
if(!StringUtils.isAnyBlank(parentCode , parentType)) {
Organization parentOrganization = parentOrganizationStrategy.queryByCode(parentType, parentCode);
Validate.notNull(parentOrganization , "无法找到指定的父级节点,请检查");
}
}
// 2、================
// 找到正确的操作策略器,并进行正式的创建处理
OrganizationStrategy<? super Organization> currentOrganizationStrategy = this.findOrganizationStrategy(type);
Validate.notNull(currentOrganizationStrategy , "创建时,未发现正确的已注册的处理方式(%s)" , type);
// 验证要添加的节点,是否已经存在
Organization exsitOrg = currentOrganizationStrategy.queryByCode(type, code);
Validate.isTrue(exsitOrg == null , "创建时,发现指定类型下的组织机构编号已经被使用,请检查");
Organization result = currentOrganizationStrategy.create(org);
Validate.notNull(result , "创建成功后的结果不能为空");
// 3、================
if(org instanceof TreeOrganization) {
TreeOrganization treeOrg = (TreeOrganization)org;
List<? extends UserMapping> users = treeOrg.getUsers();
if(!CollectionUtils.isEmpty(users)) {
for (UserMapping userMapping : users) {
String userMappingType = userMapping.getType();
Validate.notBlank(userMappingType , "创建时,未传入用户映射类型信息");
UserMappingStrategy<? super UserMapping> currentUserMappingStrategy = this.findUserMapping(userMappingType);
Validate.notNull(currentUserMappingStrategy , "创建时,未找到正确的用户映射类型处理策略");
currentUserMappingStrategy.create(userMapping);
}
}
}
// 4、================
// 一旦创建成功,则事件会被触发
if(!CollectionUtils.isEmpty(orgListeners)) {
for (OrgListener orgListener : orgListeners) {
orgListener.onCreated(result);
}
}
return result;
}
// ......
// 还实现了诸如树形结构查询的方法,请下载代码自行阅读
// ......
}
从以上的实现代码来看,代码中不是描述某一种具体的组织机构类型应该怎么进行业务属性相关的边界校验、新增入库或者字段修改,而是描述了一种处理过程:先进行业务无关的边界校验,再寻找正确的组织机构处理策略并调用具体的创建过程,然后再调用正确的组织机构-用户映射创建过程,最后触发事件监听。这样一来,定义不同的类型的组织机构和不同类型的组织机构-用户映射关系,就可以基于两个不相交的“组织机构类型”维度和“组织机构-用户映射类型”维度形成无数多条业务逻辑分支。
另外需要注意的是,在本示例中我们将控制逻辑放置在组织机构本地数据库实现(的脚手架)中,主要原因是为了方便讲解顺序。实际的工作中,类似这种控制逻辑的具体实现类(OrgServiceImpl)一般会被放置在接口定义的脚手架中(这里就是org-sdk),因为这种控制逻辑和技术人员做那种具体的实现是没有关系的。
-
为了便于读者理解这种控制耦合,上图将组织机构中两个不相交的维度映射成X-Y轴的平面坐标系,并进行控制逻辑的相交表述——但实际上这并不是“维度相交”的定义,只是为了便于读者进行阅读理解才这样进行坐标系映射。后文中,我们将详细描述什么叫做业务维度相交、
-
因为间接耦合在本示例中是通过一套固定的逻辑过程被关联上的,所以有一种可能是由于这套逻辑过程相对于使用方出现了很大差异,使用方需要直接改变这套固定的处理逻辑过程。这种情况当然是可能出现的,解决办法也只有推荐使用方重新替换service层的实现(只是逻辑过程无法满足的情况),或者直接替换整个组织机构模块的实现(逻辑过程、业务维度都无法满足的情况)。这种场景一般认为组织机构的业务屈服度未达到设计期望,关于业务屈服度的介绍将在后文进行展开。
上图我们汇总了目前组织机构模块的接口定义和本地数据库默认实现。后文我们将介绍,这个组织机构默认的本地数据库实现,如果部署到具体的应用中。二次开发团队怎么针对组织机构模块提供的接口定义,进行二次开发工作。