软件设计不是CRUD(6):低耦合模块设计实战——组织机构模块(上)

组织机构功能是应用系统中常见的业务功能之一,但是不同性质、不同行业背景、不同使用场景的应用系统对组织机构功能的要求可能完全不一样。所以使用这样的功能对低耦合模块设计进行示例性的讲解是比较具有代表性的。在后续的几篇文章中,我们会首先进行示例的详细讲解,然后再基于这个示例进行理论讲解。

1、组织机构模块需求

目前X公司研发的标准产品中有一个组织机构功能模块,这个模块提供一个最基础的组织机构功能实现:这个组织机构中的每个组织机构节点都有一些基本信息(编号、中文名、排序号、创建时间、创建者、修改时间、修改者、国家编码等信息),每个节点除了可以关联用户信息,还可以关联下级组织机构节点。整个组织机构的数据可以组成一棵无限极的树结构,如下图所示:
在这里插入图片描述
不止是组织机构对于下级组织机构的关联问题,客户还明确要求由于客户所在企业有许多种不同类型的人员(例如企业领导、普通职工、外聘人员等),所以如果有可能还要求在这个无限极的组织机构树结构中,能够设定不同的组织机构类型可以关联的人员类型。不过这个需求点客户也明确告知,不要求本期迭代中一定包括对此需求的实现,因为客户方本身也没有梳理清楚目前的用户类型怎么划分比较符合自身业务情况。但客户方明确要求研发团队一定把扩展点预留出来,以便确认需求后能够快速加不同类型的组织机构和不同类型用户的关联。

这个无限级的树结构在查询的时候还需要支持正向查询和逆向查询。正向查询很好理解,就是给定一个节点,然后查询这个节点直接关联的用户信息和直接关联的下级组织机构节点信息;逆向查询的功能是说给定一个(或多个)用户或者一个(或多个)组织机构,查询这些信息直接和间接关联的上级组织机构信息直到根节点,最后反向形成一个完整的树形结构。

另外,为了加快查询速度,组织机构的树形结构还支持编码降维处理。例如子公司的编号为 51,部门的编号为01,团队编码为06,这样三级组织机构的完整编码为510106。这种编码方式可以进行快速查询,例如查询某个部门下的所有直接和间接关联的下级组织机构,并用列表进行显示。
在这里插入图片描述
这样的组织机构功能基本上适应于大部分的业务场景,但是也不能排除将标准产品提供的这个组织机构功能应用到具体项目时,项目的客户对组织机构功能提出新的需求(需求变化诉求):

  • 组织机构需要有类型的标识,组织机构不同的类型,字段完全不一样,有的组织机构类型甚至不能包括用户信息。不过组织机构都有业务编号和显示用的中文名。不同的组织机构类型可以关联的下级组织机构类型不一样,例如A类型的组织机构只能关联B、C两种类型的组织机构作为下级组织机构。

  • 有的项目团队,需要屏蔽一些不需要使用的组织机构类型。例如标准产品中的这种最基础的组织机构类型,但是这种组织机构类型在一些特定项目中基本不适用,所以需要去掉。

  • 有的项目团队,因为有特别的查询要求,所以需要新增组织机构的查询功能。例如有的项目团队自己新增的组织机构类型中,有一个和特定业务相关的属性叫做“出口目的地”的字段,项目团队有需求根据这个字段进行特定组织机构类型的查询(查询结果需要包括直接关联的子级组织机构的信息)。

  • 有的项目团队开发的应用系统,由于某种组织机构类型的数据特别多,所以这种组织机构所使用的两位编码不够用,需要将组织机构的编码位数从两位提高到三位甚至四位。

  • 还有其它未知的变化——由于项目客户的问题,一些组织机构功能的详细需求变化点还不能确认,但是这些变化点也只会集中在两个方面:对不同组织机构类型的模型字段进行调整,以及对不同组织机构类型的查询功能进行调整。

2、一般的设计开发过程

针对以上的组织机构模块的需求,以及目前了解到的项目团队对该模块的变动诉求,我们来看一个可能出现的功能模块设计方案。这个设计方案也是目前绝大多数应用系统在进行功能模块设计时所使用的设计方式。

2.1、构建脚手架

为了保证模块开发的顺利进行,开发人员会首先搭建一个开发脚手架,这里我们以普遍使用的Spring Boot + Maven的方式给出示例脚手架(笔者在实际工作中更喜欢使用gradle):
在这里插入图片描述
以上是一个读者再熟悉不过的简要的脚手架工程了。我们先不急着讨论这样的脚手架规划有什么问题,但其优点是显而易见的,就是简单、便于理解。其中一些包中的代码本文会在后续的内容中逐渐进行填充。

  • entity:这个包中,我们用来放置数据库对应的持久层模型
  • mapper:如果开发人员使用MyBatis进行数据持久层的实现,那么推荐专门用一个mapper包放置XML映射信息和持久层接口定义,如果使用JPA则不需要(推荐使用JPA,虽然使用门槛要高一些)。
  • repository:模块内对数据持久层的功能调用,放置在这个包中。
  • service:这个包中,我们用来放置专门提供给外部调用者使用的调用功能
  • po:这个包中,我们用来放置业务模型,由于业务模型和数据模型在复杂功能模块中往往存在差异,所以数据模型和业务模型在这个示例中分开进行设计(实际工作中也推荐模块内不同层面的模型分开设计)。
  • config:spring-boot的配置信息,环境配置信息等。
  • 其它包的创建和使用,将随着内容的深入逐渐增加。

2.2、建立数据表

收到需求,并有了进行功能模块开发的先决条件(搭好了脚手架)后,开发人员首先会进行数据表结构的设计。一般来说开发人员会使用关系型数据库,当然也可能是非关系型数据库,不过使用什么样的数据库对我们讨论模块设计没有实质性影响。以下是一种可能的数据表设计方式:

字段名字段中文名字段类型是否必填备注
id组织机构技术编号字符串/整数全系统唯一
type组织机构类型字符串组织机构类型
name组织机构中文名字符串必须填写的中文名
parentId组织机构的直接上级组织机构字符串如果当前组织机构是一个根节点,则该字段没有值
field1业务字段1整数当type类型为A的时候,才需要填写业务字段1
field2业务字段2字符串当type类型为A的时候,才需要填写业务字段2
field3业务字段3整数当type类型为B的时候,才需要填写业务字段3
field4业务字段4字符串当type类型为B的时候,才需要填写业务字段4
fieldOther其它业务字段字符串/数字还有更多业务性质的字段这里省略

建立数据表的时候,由于开发人员没办法知道后续的应用系统会增加或者删减哪些类型的组织机构,所以这张数据表中和组织机构业务相关的字段会无限制的扩展。另外,由于不清楚特定的数据表中会使用哪些字段作为查询条件,所以数据表中这些业务相关的字段也没法提前进行索引优化。

注意:这里的数据表是一种正常的数据表,有的开发人员为了适应不断增加的组织机构类型和业务属性,也会采用一种“垂直表”的方式(这里就不展开介绍了)。这种“垂直表”涉及到查询的问题,可能引起的设计、稳定性、性能问题会更多。

数据表完成设计以后,我们需要为这个数据库建立对应的持久层模型(示例模块采用Java进行开发,但实际上大多数高级语言,都需要编写建立模型的代码)。这个持久层模型可能如下代码所示:

  • 组织机构的数据持久层模型
// 组织机构标准的数据持久层模型(基于JPA)
@Entity
@Table(name="simple_org")
public class OrgEntity {
  @Id
  private String id;
  // 组织机构类型
  @Column(name = "type" , length = 128 , nullable = false)
  private String type;
  // 组织机构名称
  @Column(name = "name" , length = 512 , nullable = false)
  private String name;
  // 如果当前组织机构是一个根节点,则该字段没有值
  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "parent_id" , nullable = true)
  private OrgEntity parent;
  // 该组织机构已经关联的用户信息
  @OneToMany(fetch = FetchType.LAZY , mappedBy = "org")
  private Set<OrgUserMappingEntity> users;
  // 当type类型为A的时候,才需要填写业务字段1
  @Column(name = "field1" , nullable = true)
  private Integer field1;
  // 当type类型为A的时候,才需要填写业务字段2
  @Column(name = "field2" , length = 256 , nullable = true)
  private String field2;
  // 还有更多业务性质的字段这里省略
  @Column(name = "field_other" , length = 512 , nullable = true)
  private String fieldOther;
  
  // ......
  // getter和setter忽略
  // ......
}
  • 组织机构关联的用户信息
// 组织机构和用户的直接关联信息
@Entity
@Table(name="org_user_mapping" , indexes = @Index(columnList = "org_id,account" , unique = false))
public class OrgUserMappingEntity {
  @Id
  private String id;
  /**
   * 用户直接关联的组织机构信息</br>
   * 在标准产品中,一个用户可以关联多个组织机构。
   */
  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "org_id" , nullable = false)
  private OrgEntity org;
  // 账户信息
  @Column(name="account" , length = 255 , nullable = false)
  private String account;
  // 是否是当前用户主要的组织机构
  @Column(name="principal" , nullable = false)
  private Boolean principal;
  
  // ......
  // getter和setter忽略
  // ......
}

在基于Java语言的Spring开发框架中,大多数情况我们都会采用MyBatis或JPA(当然两者可以进行融合)来建立具体的持久层模型,这种具体的持久层模型的问题是,一旦字段需要发生变化(例如应用程序新增了一种组织机构类型,且新的这种组织机构类型会有一些特定的业务字段)那么这个模型的代码也需要进行调整(有可能是在模型的代码本身新增一段,也可能继承原有对象建立一种新的对象)。

2.3、定义专门的调用层并进行实现

模块中存在专门的功能调用层,其中有一个OrgService接口定义了多种组织机构基本信息供外部调用的功能(包括增删改查)。另外,为了保证持久层的模型不对外泄露,保证模块内数据库中数据表的变化,尽可能不影响外部调用者,这个专门的调用层创建了一个专门的业务模型。代码如下所示:

  • 业务模型
/**
 * 组织机构的业务对象
 * @author yinwenjie
 */
public class Organization {
  private String id;
  // 组织机构类型
  private String type;
  // 组织机构名称
  private String name;
  // 如果当前组织机构是一个根节点,则该字段没有值
  private String parentId;
  // 当type类型为A的时候,才需要填写业务字段1
  private Integer field1;
  // 当type类型为A的时候,才需要填写业务字段2
  private String field2;
  // 还有更多业务性质的字段这里省略
  private String fieldOther;
  // ......
  // getter和setter忽略
  // ......
}
  • 专门的功能调用层的接口定义
public interface OrgService {
  /**
   * 创建一个新的组织机构。新增成功后,最新的组织机构信息将被返回
   * @param org 需要新增的组织机构信息
   * @return
   */
  public Organization create(Organization org);
}

虽然我们为调用层创建了专门的业务模型,但这个业务模型对屏蔽数据库设计细节的作用不会太明显。原因是这个业务模型中的业务字段需要与具体的某一张数据表有映射关系。所以一旦数据表中的字段发生了变化(新增字段、修改字段类型等),数据持久层的模型和业务模型都可能发生变化。

服务层实现的问题也比较明显。就以以上代码描述的新增功能来说,不同类型的组织机构新增时,需要验证的信息不一样、新增的处理逻辑也不一样。一旦二开团队需要创建一种新类型的组织机构,那么新增功能中的逻辑就会发生变化——在代码中新增一条处理逻辑分支。

// 调用接口的实现使用了一些spring boot常见的注解。
@Service
public class OrgServiceImpl implements OrgService {1
  @Autowired
  private OrgRepository orgRepository;
  @Override
  public Organization create(Organization org) {
    // 首先进行边界校验
    // ......
    
    // 然后将业务模型Organization转换成数据持久层模型,进行保存
    OrgEntity orgEntity = transform(org);
    this.orgRepository.save(orgEntity);
    org.setId(orgEntity.getId()); 
    // 返回最终创建成功的组织机构业务对象
    return org;
  }
  // ......
  // 这里还有一些针对修改操作、查询操作的功能实现
}

以下是做了进一步代码开发的组织机构模块的开发脚手架:
在这里插入图片描述
在这样的模块设计结构下,如果二次开发团队需要根据自己的业务情况增加一种组织机构类型,那么组织机构模块现有的处理逻辑就需要做出修改,下面以“组织机构创建”功能代码进行示例:

// 调用接口的实现使用了一些spring boot常见的注解。
@Service
public class OrgServiceImpl implements OrgService {
  @Autowired
  private OrgRepository orgRepository;
  @Override
  public Organization create(Organization org) {
    Validate.notNull(org , "必须传入要新增的组织机构信息");
    String type = org.getType();
    Validate.notBlank(type , "组织机构类型必须传入");
    
    OrgEntity orgEntity = null;
    // 不同的组织机构类型,使用的验证和转换逻辑不一样的
    // 随着不同的二开团队新增不同的组织机构类型,这里的if...... eles if ......分支代码会更多。
    if(StringUtils.equals(type, "A")) {
      validateForTypeA(org);
      orgEntity = transformForTypeA(org);
    } else if(StringUtils.equals(type, "B")) {
      validateForTypeB(org);
      orgEntity = transformForTypeB(org);
    } else {
      throw new UnsupportedOperationException("不支持的组织机构类型");
    }
    // 然后将业务模型Organization转换成数据持久层模型,进行保存
    this.orgRepository.save(orgEntity);
    // 返回最终创建成功的组织机构业务对象
    return org;
  }

  // ......
  // 其它关于组织机构写操作的代码基本也会出现变化
}

很难想象如果二次开发团队需要去掉一些组织机构类型,又需要新增几种组织机构类型,会如何通过“if…else if…”或者类似的方式改造原始代码。实际上上文提到的改变模型的字段结构的问题、改变模块服务层的处理逻辑的问题、改变模块服务层的查询逻辑的问题,都并不是最大的问题——需求变化带来的代码调整大概率都是必要的。更大的问题是,这些修改必须在标准产品原有模块的基础上进行,也就是说一旦需要修改就只有两种方式:

  • 方法一: 将源代码提供给二开团队,然后二开团队在理解了目前的业务逻辑的前提下,进行源代码的修改。修改后形成一条新的代码分支并进行管理。可以这样想象:如果Spring Boot框架采用这种方式进行功能的扩展使用,那么二开团队的学习成本、能力要求都会被提高——绝大部分开发人员没有时间、没有精力去阅读Spring Boot的源代码,然后在理解这些源代码的逻辑后,对这些源代码进行修改。修改后的逻辑和原始逻辑大概率也是无法兼容的,于是只有形成一个新的源代码分支(一个二开团队/项目一个分支),进行管理。

  • 方法二: 二开团队给组织机构模块的原始开发者提出修改需求,然后由原始开发者进行修改后再提供给二开团队。这种维护方式带来的问题更严重:首先如果二开团队的数量超过一个,那么工作效率瓶颈就会出现,多个二开团队只能按优先级等待原模块的开发者依次完成功功能修改。另外,这些具有特异性的功能,本身就不应该增加到原始模块中,这是因为这些逻辑不具有逻辑共性,不能为其他二开团队/项目所用,只能徒增原始模块中的技术债务。最后,这种修改方式给原模块的开发者带来了更大的难度,原模块的开发者需要充分考虑同一种逻辑在多个不同项目场景中的运行兼容性。

2.4、设计方式的问题总结

目前组织机构模块的设计除了存在当需求发生变化时原有代码都要进行修改的问题、存在只能在原有代码上修改处理逻辑的问题、存在二开团队无法有效扩展模块逻辑的问题外,还存在容易产生循环依赖的问题。最后一个问题是很严重的,因为一旦产生循环依赖,该模块和产生循环依赖的模块客观上只能作为一个大粒度的模块向外提供服务——无论这些模块的开发者主观上如何理解、如何划分这些模块。产生这个问题的原因,主要集中在这个专用的调用层的设计和实现:

  • 调用层的接口定义只有正向接口和实现,没有提供反向接口:
    根据我们前文介绍的内容,为了保证不出现循环依赖,最好的办法就是处于应用系统下层的模块完全“看不见”处于应用系统上层的模块。或者换句话说,下层模块根本不知道上层有哪些模块。那么由于该模块专门的调用层只有正向接口,那么要满足以上描述的系统模块分层的定义,这个模块就只能处于应用系统的最下层。

    但是类似于这种包含业务功能的组织机构模块,往往会在具体的逻辑中对其它业务功能模块进行调用:例如调用用户模块、调用角色模块、调用信息发送模块等等。所以如果将这个组织机构模块定位成最底层的模块,而该模块又只有正向调用接口,就会
    更容易产生循环依赖。如下图所示:

在这里插入图片描述

  • 上层模块无法干预组织机构的内部工作:

    这实际上也是由于组织机构模块没有提供用于上层模块实现的反向接口,造成上层模块无法监控、干预、参与组织机构模块内部的逻辑处理过程。为什么无法提供呢?主要是因为组织机构模块的核心业务逻辑在调用层的实现中已经被“写死”,没有向上层模块提供任何干预这个“写死”逻辑的可能性。

  • 一些客户要求预留的扩展点,没有在本次设计中得到很好的体现:

    这主要就是指客户在需求提出之初就要求设计团队预留的**“不同组织机构类型可设定自己所允许关联的用户类型”**,原因是客户自己还没有梳理清楚在系统中怎样划分用户类型比较合理。但不代表后续的研发迭代中,客户会放弃这个需求。这就是一个比较尴尬的问题了,由于目前设计中并没有预留为这个后续需求准备的扩展方式,可能带来的后果就是一旦客户梳理清楚用户类型如何划分后,组织机构模块涉及的业已工作的代码可能需要做比较大范围的修改。

  • 模块的模型结构定义抽象度不够:

    模型结构的抽象可以一句话概述为:如何以最小的属性规模定义模型的业务性

    这可能不好理解,举例说明:只要模型有一个技术编号、一个类型、一个同种类型下唯一的编号、一个中文名,那么这个模型就可以作为一个组织机构模型。再来一个例子:只要模型有一个技术编号、一个全系统唯一的账号,那么这个模型就可以作为用户信息模型。

    具体到这里的组织机构模块的设计实例中。组织机构模型的抽象度不够就是说:目前组织机构模块中对组织机构信息进行建模时,由于没有找到业务的核心诉求,所以加入了很多特异性的业务字段A、B、C,从客观上定义了组织机构信息必须符合这些特异性的业务特征才能被定义成“组织机构”。例如定义“组织机构必须有类型、有编号、有中文名、有负责人、有所属公司、有销售目标”才叫做组织机构。当更多的组织机构类型加入进来、当更多的特异性业务字段加入进来,就已经不再是抽象度够不够的问题,而是能不能进行抽象的问题了。

    组织机构模型结构的抽象度不够,还有一个更最直接的影响:虽然组织机构模块分开定义了持久层的数据模型和调用层的业务模型,但还是不能保证不出现某种类型的组织机构信息要进行修改时,数据模型和业务模型都要进行修改的情况。从根本上来说,这种从数据模型“翻译”过来的业务模型就是数据层的“傀儡”,无法达到隔离数据结构变化的目的。另外多个不同的组织机构类型,共用一个业务模型/数据模型,也会在模型上体现这些不同的组织机构类型所有的特异性业务属性,这种被共用的模型结构也会在增加更多组织机构类型的情况下,变得更加臃肿、更难控制。

总的来说,目前整个组织机构模块的耦合强度,如果按照前文《软件设计不是CRUD(4):耦合度的强弱》提到的耦合强度定义,那么只能算是一种“控制依赖”的耦合强度。而“控制依赖”是一种可以优化设计的耦合强度。下面我们来介绍一下如何降低这个模块的耦合强度。

3、将设计降低到间接耦合

要将模块的耦合强度降低到间接耦合,一种可靠的方式就是在需求理解和设计落地之间加入一个业务抽象的步骤。本文先不讲解什么是业务抽象,以及怎么做业务抽象,先将业务抽象的过程带入示例,并演示基于业务抽象的思想所进行的模块设计,其耦合强度是如何降低的。

3.1、进行需求分析

我们仔细分析上文中的需求描述,可以看出对于组织机构的变化主要分为对组织机构模型变化的要求和对组织机构行为的变化要求,我们先用一张图总结一下这些需求,特别是需求的变化:
在这里插入图片描述
经过对需求进行总结,我们可以发现所有的组织机构功能的需求,都是按照一个“类型”维度进行变化量和不变量的区分。另外,组织机构模块还有一个业务维度“用户类型”,但“用户类型”这个维度的需求并没有确认,且从经验来看“用户类型”这个维度的业务边界并不会在组织机构功能内,所以在组织机构功能中“用户类型”这个业务维度只能算一个辅助维度。

不同的类型针对模型的特殊要求特异性关联针对行为的特殊要求
组织机构类型A(标准产品默认组织机构)特异性属性:创建时间、修改人、统一信用编号、认证编号原本只能关联类型A的组织机构,但是项目X有要求,需要能够支持关联组织机构B、组织机构C,且不能关联任何人员信息删除时,如果有人员或者下级组织机构已经直接或者间接关联这个组织机构了,就不允许删除
组织机构类型B(项目X自定义的组织机构)特异性属性:修理时间、轮查费用、预算形式、拨付有效性可以关联组织机构B、组织机构C;创建时,如果当前创建的组织机构没有传入下级组织机构,则不允许创建;修改的时候,不能修改预算形式
组织机构类型C(项目X自定义的组织机构)特异性属性:部门领导人、销售金额、绩效评级、是否外销管理不可以关联任何下级组织机构创建时,如果当前创建者没有指定部门负责人,则不允许创建;修改的时候,绩效评级信息不能进行修改

本示例展示了在单一业务维度且维度不变化的情况下,如何进行模块中模型和行为的抽象分析。在后续文章详细讲解业务抽象的概念和理论后,还会介绍在多业务维度且维度变化的情况下,如何进行抽象分析。经过上述分析后,我们基本上确定了组织机构功能模块的业务要素,包括业务维度有哪些、基于业务维度的业务模型哪些是变化的哪些是不变的、基于业务维度的业务行为哪些是变化的哪些是不变的。接下来我们可以进行设计落地了。

3.2、进行接口设计

3.2.1、重新设计的抽象模型

  • 显然我们需要一种接口,以便描述不同组织机构类型的模型特征,并进行模型转换工作。这个问题很好理解,由于不同组织机构类型都有自己特有的业务字段信息,为了在需求变化时互不干预模型结构,就需要对模型结构进行分开定义。另外,由于字段不同,所以模型的转换方式也不同。
// 只要实现了该接口的业务模型,都被认为是组织机构
public interface Organization {
  // 组织机构类型
  public String getType();
  // 在组织机构类型下,唯一的组织机构业务编码
  public String getCode();
  // 组织机构的中文名
  public String getName();
}

无论是哪种组织机构类型,只需要满足组织机构模型最基本的要求,就能够被组织机构模块看成是一种组织机构模型——只要实现了以上接口的模型,都是组织机构的业务模型。因为新的设计中组织机构功能模块可以按照组织机构类型支持多个业务模型了,所以我们需要一种注册说明接口,对每种组织机构类型的模型转换和具体业务模型定义进行明确。

// 不同的组织机构类型的业务模型,都需要这个类进行明确具体的模型定义和转换方式
// 每一种组织机构类型的模型,都需要实现该接口,以便描述自己的模型特性
// 不同的组织机构类型还只支持排序,保证某个节点下有多种子级组织机构时,排序顺序不会乱
public interface OrganizationModuleRegister <O extends Organization> extends Ordered {
  /**
   * 某一种具体的组织机构类型,适配的模型特性描述</br>
   * 这里的type返回值应该和具体模型定义的type属性,要保证一致
   */
  public String type();
  /**
   * 该方法描述一个json结构应该如何转换成特定的组织机构模型
   */
  public O transform(JSONObject json);
    /**
   * 由于不同的组织机构类型涉及的降维编码长度不一样,所以这里需要进行设置
   * 默认长度为2;
   */
  public default int reducedLength() {
    return 2;
  }
  /**
   * 可直接关联的下级组织机构类型
   * @return 返回为null,就表示任何组织机构类型都可以作为下级组织机构进行关联;
   * 返回空数组,就表示当前组织机构类型下,不能再关联任何的组织机构类型
   * 如果设定了一个或者多个组织机构类型,就表示只有设定的组织机构类型可以作为下级组织机构被关联
   */
  public default String[] enableOrgTypes() {
    return null;
  }
  /**
   * 可直接关联的下级用户类型
   * @return 返回为null,就表示任何用户类型都可以进行关联;
   * 返回空数组,就表示当前组织机构类型下,不能直接关联任何用户类型
   * 如果设定了一个或者多个用户类型,就表示只有设定的用户类型可以被关联
   */
  public default String[] enableUserTypes() {
    return null;
  }
}

由于在应用系统的整体设计中,用户模块被设置在了组织机构模块的上层,也就是说组织机构是看不到用户模块的——无论将来的设计中用户模块是一个模块然后用“类型”区分不同的用户,还是“不同类型的用户”建立不同的用户模块。反正按照模块层次稳定的要求,这个或者这些用户模块都是在组织机构模块的上层。但是组织机构模块又存在关联用户的诉求,这些被关联的用户都需要返回给调用者进行显示、判定、查询等操作。又考虑到上层用户模块可能需要重用模型结构,所以在组织机构模块中,我们需要定义一个关联用户的接口。如下代码所示:

/**
 * 由于在本示例中,用户模块处于组织机构模块的上层
 * (实际上正式设计中,一般不会这样设计,因为用户模块变动的可能性一般高于组织机构),
 * 所以在组织机构不清楚上层模块会如何设计具体的用户信息时,
 * 只需要定义一个用户信息的接口满足组织机构中最简单的用户显示需求就行。
 * @author yinwenjie
 */
public interface UserMapping {
  // 和组织机构关联的用户类型(由于用户类型在标准产品中只有一个,所以默认为default)
  public default String type() {
    return "default";
  }
  // 系统唯一的用户账号信息
  public String account();
  // 用户信息中的用户中文名(也可能是昵称)
  public String describer();
  // 用户真实姓名
  public String name();
}

3.2.2、稳定系统分层的前提下,重新设计模块行为

上文已经多次提到,为了保证模块分层的稳定性,组织机构是不允许依赖用户模块、角色模块、信息发送模块等上层模块的。但在某个具体项目中有一个特异性需求,明确要求如果新的组织机构创建时,所关联的部门领导人不具有“部门领导”角色,则不允许添加。那么就需要由组织机构模块提供接口,以便组织机构的创建事件能够通知到上层的角色模块(这里我们使用监听器方式)。

在这里插入图片描述
以下是监听器的接口定义:

// 组织机构模块的事件监听器,上层模块通过实现这些接口,可以监控、干预组织机构模块中对应的写操作过程
public interface OrgListener {
  // 当组织机构模块中,成功创建新的组织机构信息,则该事件将被触发
  // @param org 新创建的组织机构将被传递给具体的实现者
  public void onCreated(Organization org);
  /**
   * 当组织机构模块中,成功修改已有的组织机构信息,则该事件被触发
   * @param before 修改前的组织机构信息
   * @param after 修改后的组织机构信息
   */
  public void onUpdated(Organization before , Organization after);
  /**
   * 当组织机构模块中,成功删除已有的组织机构信息,则该事件被触发
   * @param org 已经被删除的组织机构信息将被传递给具体的实现者
   */
  public void onDeleted(Organization org);
}
  • 另外,由于不同组织机构类型对组织机构添加、修改的要求都不一样,所以需要不同的策略逻辑对特异性的处理逻辑进行隔离,保证它们的现行工作和后续变动互不干扰且可以扩展新的策略逻辑。以下代码示例了一种策略接口定义,可以将不同组织机构类型的写操作(添加、修改、删除等操作)隔离开:
// 不同的组织机构类型的写操作方法,都需要实现一个该接口。用于隔离不同组织机构类型的不同处理逻辑和模型结构
public interface OrganizationStrategy <O extends Organization> {
  /**
   * 某一种具体的组织机构类型,适配的模型特性描述</br>
   * 这里的type返回值应该和具体模型定义的type属性,要保证一致
   */
  public String type();
  /**
   * 对某种具体的组织机构类型进行创建的实际操作。
   * @param org 由外部请求者传入,并成功转换成具体组织机构信息,最后再进行传入的将要进行创建的组织机构信息
   * @return 添加成功后的完整组织机构信息,需要被返回。返回的对象中必须带有组织机构类型、编号、名称信息
   */
  public O create(O org);
  /**
   * 对某种具体的组织机构类型进行修改的实际操作
   * @param org 由外部请求者传入的,并成功转换成具体组织机构信息,最后再进行传入的将要进行修改的组织机构信息
   * 注意其中的code、type信息必须传入,且这两个信息无论哪种组织机构类型的具体修改操作,都不允许修改
   * @return 修改成功后的完整组织机构信息,需要被返回
   */
  public O update(O org);
  /**
   * 对某种具体的组织机构类型进行删除的实际操作
   * @param org 由外部请求者传入的,并成功转换成具体组织机构信息,最后再进行传入的将要进行修改的组织机构信息
   * 注意其中的code、type信息必须传入
   */
  public void delete(O org);
  /**
   * 按照组织机构节点的类型和编号,查询组织机构的基本信息(不一定包括关联信息,看具体的实现)
   * @param type 查询时传入的组织机构类型信息
   * @param code 查询使用的业务编号
   * @return 如果查询得到结果,则进行返回;其他情况返回null
   */
  public O queryByCode(String type , String code);
  /**
   * 按照指定节点(通过传入的type和code,指定的节点),查询这个节点下,在这个组织机构类型中绑定的所有下级组织机构信息
   * @param type 查询的组织机构类型
   * @param code 查询的组织机构类型下的组织机构业务编号信息
   * @return 如果查询到结果,则进行返回,其它情况返回null或者空集合;
   * 注意,一个指定节点下,可能存在多种组织机构类型的子级节点,这里只负责查询本类型下子级节点
   */
  public Collection<O> queryChilds(String type , String code);
}

接着,由于还有多种存在于上方的用户模块,可能关联组织机构信息(就是用户信息和组织机构信息产生关联),所以还需要定义一种关联接口,用于实现一种或者多种用户模块关联组织机构信息:

/**
 * 一旦组织机构模块的上层用户模块(一种用户模块或者多种用户模块)需要关联组织机构时
 * 就需要实现该策略接口,以便隔离不同的用户类型对组织机构的关联逻辑.
 * 该接口的不同实现(不同的用户和组织机构的关联),还可以进行排序,保证用户的显示顺序
 */
public interface UserMappingStrategy <M extends UserMapping> extends Ordered {
  /**
   * 指定组织机构节点,查询这个组织机构节点所关联的某一种用户信息集合
   * @param type 查询的组织机构类型
   * @param code 查询的组织机构类型下的组织机构业务编号信息
   * @return 如果查询到关联的某种特定用户信息(集合),则进行返回,其它情况返回null或者空集合;
   */
  public Collection<M> queryUsers(String type , String code);
}

3.2.3、设计的关键接口总结

我们来总结本节内容中对组织机构功能的需求进行分析后的输出,首先我们从需求中提炼出了几个要素的部分:

  • 关于业务维度:从需求中我们发现系统支持不同类型的组织机构,而不同类型的组织机构其模型结构和读写操作的逻辑过程都不一样。组织机构模块的开发团队和二次开发团队都可能扩展不同类型的组织机构,所以组织机构类型显然是一个关键的业务维度;我们还分析出一个非关键的业务维度,即是客户还没有完全梳理清楚的用户类型维度。这个业务维度不在组织机构模块功能边界内,原因是我们从需求中并没有发现根据用户类型的维度,组织机构的模型和行为会发生根本性变化。已知的需求只是:不同的组织机构类型可以关联的用户类型是不一样的。

  • 关于模型:我们并没有从需求中提取出所有类型组织机构的所有详细业务属性,这是因为不同的组织机构类型涉及的业务属性大部分不一样,且二开团队需要能够自定义新的组织机构类型。从需求中我们只能提取出一些共性特点就是,无论哪种类型的组织机构都存在类型、业务编号、显示中文名这些信息。也就是说只要有这些关键字段,就可以认定这个模型是某种类型的组织机构

  • 关于行为:组织机构模块的工作逻辑同样受到组织机构类型的影响,不同类型的组织机构新增、修改、删除逻辑可能完全不一样,且查询功能可能也有差异。但不同组织机构类型的查询特性都必须支持组织机构树的构建——这里所说的构建包括两类,第一种是正向查询子级以便形成一棵无限极的树结构;第二种是逆向查询父级直到树结构的根节点。而我们从需求中分析得到的非关键维度(用户类型)在组织机构模块中只控制了一个行为,就是“如何设定和查询组织机构关联的用户信息”。
    在这里插入图片描述
    接着我们基于已知的业务维度来进行接口设计,这一步是设计的关键,核心思路在于设计业务维度如何控制模型和行为。由于本示例场景中有两个业务维度,这两个业务维经过分析后我们发现并没有交集:要扩展用户类型的上层模块/应用和要扩展组织机构类型的上层模块/应用不存在交集。诸如处于组织机构上层和用户管理相关,并需要关联组织机构的模块,才会扩展新的用户类型;处于组织机构上层由二开团队开发的应用程序,才会扩展新的组织机构类型。所以需要将基于这两个业务维度所设计的模型和行为接口分开定义,不能定义成一个接口。
    在这里插入图片描述
    然后,由于需要考虑其它上层模块需要关注组织机构本身的数据变化,以便在这些数据发生变化时,上层业务模块可以根据这些变动而发生变动、可以验证变动的合规性或者可以干预组织机构模块内部的处理过程。所以我们还需要一种事件机制的接口接口。这里,本示例选择了最简单的监听器接口。这不是一个设计的核心,只是为了保证各模块边界的清晰和模块层次的稳定。后续设计实战中,我们将看到更复杂的监控、干预机制。

最后,我们将组织机构的接口层专门提取出来形成一个独立的脚手架,这样的话程序员可以根据这套接口(最后形成的jar包),制作多套实现。整个脚手架如下图所示:
在这里插入图片描述
上图中几乎所有的接口都在本节中进行了描述,一个比较特殊的是OrgService接口,这个接口是主要给模块功能的调用者准备的,这个接口的任意版本实现都要求没有任何具体的业务逻辑,而是对控制逻辑进行描述。接下来我们看一下针对这套接口设计的一个默认实现。默认实现可以由产品团队进行开发,项目团队或者其他二开团队觉得默认实现不好用,可以直接进行替换、继承重写或者重新实现。

另外,读者还可以发现,在这套组织机构模块的接口设计中并不包括数据持久层部分的接口。这是因为数据持久层的模型结构和数据持久层的功能在这样的设计中已经不属于组织机构模块向外暴露的范围。换句话说,外部调用者已经不再允许直接调用数据持久层的模型和行为。

最后需要注意的是目前设计出来的这些接口,有的接口是在组织机构的具体实现中必须实现的接口;有的接口则是需要依赖组织机构模块的上层模块根据情况来实现的,例如OrgListener这个接口就是上层模块需要监控、干预组织机构内部的写操作过程时,才需要实现的接口。在下一篇文章中,我们将描述这套根据业务抽象结果设计的组织机构模块的接口,如何完成一个默认实现。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/184300.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

linux磁盘清理

目录 排查过程1、查看磁盘占用情况2. 按照占用大小进行倒排-当前目录及其子目录3.当前目录磁盘占用情况 清理命令 排查过程 1、查看磁盘占用情况 df -hdf -h 命令用于显示磁盘空间的使用情况&#xff0c;以人类可读的方式呈现&#xff0c;其中&#xff1a;df 是 “disk free”…

ROS2编译Python节点来发布和订阅的实践《2》

通过熟悉&#xff1a;ROS2对比ROS1的一些变化与优势&#xff08;全新安装ROS2以及编译错误处理&#xff09;《1》 我们大概了解到了ROS2的重新设计带来的巨大优势&#xff0c;最核心的就是去掉了roscore&#xff0c;这样就避免了因为节点管理器崩溃而使整个系统都崩溃的场景出现…

机器学习/sklearn 笔记:K-means,kmeans++,MiniBatchKMeans,二分Kmeans

1 K-means介绍 1.0 方法介绍 KMeans算法通过尝试将样本分成n个方差相等的组来聚类&#xff0c;该算法要求指定群集的数量。它适用于大量样本&#xff0c;并已在许多不同领域的广泛应用领域中使用。KMeans算法将一组样本分成不相交的簇&#xff0c;每个簇由簇中样本的平均值描…

【ChatGLM2-6B】Docker下部署及微调

【ChatGLM2-6B】小白入门及Docker下部署 一、简介1、ChatGLM2是什么2、组成部分3、相关地址 二、基于Docker安装部署1、前提2、CentOS7安装NVIDIA显卡驱动1&#xff09;查看服务器版本及显卡信息2&#xff09;相关依赖安装3&#xff09;显卡驱动安装 2、 CentOS7安装NVIDIA-Doc…

idea 问题合集

调试按钮失效&#xff1a; 依次点击&#xff1a;Modules-web-src-Sources&#xff0c;重启IDEA即可&#xff08;网上看到的方法&#xff0c;原因呢未明&#xff09;

Modbus故障码速查手册(故障码含义、分析原因、详细解读)

Modbus故障码速查手册 文章目录 Modbus故障码速查手册引言故障码表故障详解0x01 IllegalFunction0x02 IllegalDataAddress0x03 IllegalDataValue0x04 SlaveDeviceFailure0x05 Acknowledge0x06 SlaveDeviceBusy0x08 MemoryParityError0x0A GatewayPathUnavailable0x0B GatewayTa…

java spring-boot 修改打包的jar包名称

修改pom文件 <finalName>lzwd</finalName><build><finalName>lzwd</finalName><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plu…

IP地址定位的误差问题及解析

随着互联网的普及&#xff0c;IP地址定位成为了数字时代中不可或缺的一部分&#xff0c;被广泛应用于各种场景&#xff0c;从位置服务到网络安全。然而&#xff0c;尽管IP地址定位提供了便利&#xff0c;但其准确性仍然受到多种因素的影响&#xff0c;存在一定的误差。本文将深…

【AI考证笔记】NO.1人工智能的基础概念

以下部分内容来自于百度智能云人才认证培训讲义&#xff0c;腾讯等也有人工智能类似的讲义&#xff0c;限时免费&#xff0c;也就是不报考&#xff0c;也能系统学习&#xff0c;课程做的都是不错的。有感兴趣的朋友&#xff0c;可以去检索学习。 本系列是学习笔记&#xff0c;…

thinkphp6生成PDF自动换行

composer安装 composer require tecnickcom/tcpdf 示例 use TCPDF;public function info($university,$performance,$grade,$major){//获取到当前域名$domain request()->domain();//实例化$pdf new TCPDF(P, mm, A4, true, UTF-8, false);// 设置文档信息$pdf->SetCr…

短视频账号矩阵系统saas化批量管理部署搭建/技术

一、短视频矩阵系统建模----技术api接口--获取用户授权 技术文档分享&#xff1a; 本系统采用MySQL数据库进行存储&#xff0c;数据库设计如下&#xff1a; 1.用户表&#xff08;user&#xff09;&#xff1a; - 用户ID&#xff08;user_id&#xff09; - 用户名&#xff08;…

AIOps探索 | 应急处置中排障的降本增效方法探索(下)

文章来源&#xff1a;公众号ID-布博士&#xff08;擎创科技资深产品专家&#xff09; 哈喽~上期内容我们分享了传统调用链系统与CMDB系统的缺陷、服务所有权模型是什么、服务所有权模型分类。这期我们来说一说如何落地服务所有权模型&#xff0c;以及好用的模型推荐&#xff0…

H5(uniapp)中使用echarts

1,安装echarts npm install echarts 2&#xff0c;具体页面 <template><view class"container notice-list"><view><view class"aa" id"main" style"width: 500px; height: 400px;"></view></v…

将form表单中的省市区的3个el-select下拉框的样式调成统一的间隔距离和长度,vue3项目iot->供应商管理

省市区是用3个el-select组成的 在表单中用el-col&#xff0c;会导致3个下拉的距离不统一&#xff0c;市和区的前面也是不需要文字label的 如何解决:用vue3的:deep()进行样式穿透&#xff0c;由于el-form-item标签都是一样的&#xff0c;为了能准确的找到市的el-form-item&…

C语言众数问题(ZZULIOJ1201:众数问题)

题目描述 给定含有n个元素的多重集合S&#xff0c;每个元素在S中出现的次数称为该元素的重数。多重集S中重数最大的元素称为众数。 例如&#xff0c;S{1&#xff0c;2&#xff0c;2&#xff0c;2&#xff0c;3&#xff0c;5}。多重集S的众数是2&#xff0c;其重数为3。 编程任务…

部署系列六基于nndeploy的深度学习 图像降噪unet部署

文章目录 1.直接在源代码demo中修改2. 如何修改呢&#xff1f; https://github.com/DeployAI/nndeploy https://nndeploy-zh.readthedocs.io/zh/latest/introduction/index.html 1.直接在源代码demo中修改 如果你想运行yolo5: onnxruntime:115ms ./install/lib/demo_nndeploy_…

【华为数通HCIP | 网络工程师】821-IGP高频题、易错题之OSPF(5)

个人名片&#xff1a; &#x1f43c;作者简介&#xff1a;一名大三在校生&#xff0c;喜欢AI编程&#x1f38b; &#x1f43b;‍❄️个人主页&#x1f947;&#xff1a;落798. &#x1f43c;个人WeChat&#xff1a;hmmwx53 &#x1f54a;️系列专栏&#xff1a;&#x1f5bc;️…

Android 提示框代码 java语言

在Android中&#xff0c;你可以使用 AlertDialog 类来创建提示框。以下是一个简单的Java代码示例&#xff0c;演示如何创建和显示一个基本的提示框&#xff1a; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; im…

EXIT外部中断 HAL库+cubeMX

一.cubeMX外部中断配置 1.系统内核 2.中断管理 3.选择抢占优先级和响应优先级&#xff0c;共有5个等级&#xff0c;在这里就使用库函数编写代码时最常用的2位抢占优先级2位响应优先级。 4.勾选使能选项&#xff0c;后面的两个零&#xff0c;第一个代表抢占优先级的等级&#xf…

怎么申请IP地址证书?

IP地址证书&#xff0c;也称为SSL证书&#xff0c;是一种数字证书&#xff0c;用于在网络传输过程中对IP地址进行加密和解密。它是由受信任的证书颁发机构&#xff08;CA&#xff09;颁发的&#xff0c;用于证明网站所有者身份的真实性和合法性。 一、选择证书颁发机构。首先需…