博客

  • 如何理解高内聚、低耦合

    首先需要理解高内聚、低耦合各自的意义。注意在不同的领域它们的意义是不同的。本文中主要描述的是它们在软件工程领域中的意义。

    耦合:简单来讲可以理解为元素之间的依赖度。 这个依赖度往往决定于元素间进行依赖的方式。 如: 排课服务,排课时需要依赖老师的一些信息(职级,育龄等), 是通过教师服务接口依赖,还是公用库依赖,其耦合度是不一样的。

    耦合越强,说明元素间关系越紧密, 元素之间越不能离开其依赖元素而存在或运行, 且替换其依赖越困难。另外耦合也是因存在依赖而存在, 所以软件工程中会说降低耦合,而不是去耦合。这里的元素可以是系统,服务、 组件, 模块,类,甚至是方法。软件工程中不同角色关注的元素不一样。如架构师主要关心系统、组件间耦合度、 初级开发主要关心类、方法的耦合。因此,耦合情况可以是评价软件工程各个角色工作质量的一个重要指标。耦合按从弱到强的顺序可分为以下几种类型

    • 非直接耦合:两模块间没有直接依赖关系,依赖完全是通过主控制模块对依赖模块调用来实现的。面向抽象编程, 能做到具体实现的变更,不影响主模块。
    • 数据耦合:一个模块访问另一模块,彼此间通过简单数据参数来交换输入、输出信息。这里的简单数据参数不同于控制参数、公共数据结构或外部变量。如Java 中方法参数为值时
    • 标记耦合:如一组模块通过参数表传递记录信息,就是标记耦合。这个记录是某一数据结构的子结构,不是简单变量。 如Java 中方法参数为类引用时。 
    • 控制耦合:一个模块通过传递开关、标志、名字等控制信息,明显的控制选择另一模块的功能。常见的有直接使用其他模块标志字段做逻辑控制。
    • 外部耦合:一组模块都访问同一全局简单变量而不是同一全局数据结构,而且不是通过参数传递该全局变量的信息。
    • 公共耦合:一组模块都访问同一个公共数据环境。该公共数据环境可以是全局数据结构、共享的通信区、内存的公共覆盖区等。 公共配置就是常见的公共耦合。 如Nacos 公共配置, 这个配置建立目的就是建设重复配置,简化各项目配置项,但当多个模块或多个服务都使用到该配置时,配置变更就需要关注各使用模块。如默认的数据库连接池配置,默认的注册中心配置等。
    • 内容耦合:一个模块直接修改另一个模块的数据,或直接转入另一个模块。最常见的就数据库表共用,如排课服务直接调用教师服务库中教师信息表来获取教师职级信息。这会导致教师服务变更需要考虑到排课服务,排课服务也可能要因为教师服务变更而变更。

    在设计或编码过程,会发现要降低耦合,经常会增加实现的复杂度。如内容耦合,要降低耦合,就需要在模块A中提供符合模块B的业务能力的接口。所以在实际的编码和设计过程中,还是需要更加项目实际情况,选择适当耦合方式。

    内聚: 可以简单理解为模块内各元素间是强耦合的, 其对外提供的能力是单一的,即职责单一。

    单一,不是说一定是只有一个功能,也可以是一类。如: 一个排课服务,其提供排课申请,排查查询,排课变更等一类功能接口。

    与耦合不同,内聚有“模块内”这个前提。内聚程度越高,说明模块内各元素关联度越高,元素间关系越强,说明模块划分的越合理,这里的模块同样可以是系统,服务、 组件, 模块,类,甚至是方法。内聚也有按程度低到高几种分类:

    • 偶然内聚: 指一个模块内的各处理元素之间没有任何联系。
    • 逻辑内聚: 指模块内执行几个逻辑上相似的功能,通过参数确定该模块完成哪一个功能。
    • 时间内聚: 把需要同时执行的动作组合在一起形成的模块为时间内聚模块。
    • 通信内聚: 指模块内所有处理元素都在同一个数据结构上操作(有时称之为信息内聚),或者指各处理使用相同的输入数据或者产生相同的输出数据。
    • 顺序内聚: 指一个模块中各个处理元素都密切相关于同一功能且必须顺序执行,前一功能元素输出就是下一功能元素的输入。
    • 功能内聚: 最强的内聚,指模块内所有元素共同完成一个功能,缺一不可。与其他模块的耦合是最弱的。

    从分类可以看出,内聚是评估我们划分服务,模块,类甚至方法是否合理的重要指标。高内聚可以提升模块的鲁棒性,可靠性,可重用性,可读性等优点。

    所以我们在说高内聚低耦合时,实际指的是模块间耦合性低, 但模块内的内聚度高。从软件生命周期可知,很多软件的维护期是远远高于其他周期的,而系统的维护的难易程度很大程度决定于系统的可扩展性,可重用性,及可测试性。而符合高内聚低耦合的系统,直观表现上有模块划分合理,模块本地的重用性好, 模块间耦合度低,替换难度低, 易扩展,易测试。

    另外,需注意的一点从耦合、内聚定义可知,做好系统的高内聚低耦合,不是仅取决于系统架构师, 而是所有参与系统设计、研发编码的所有人员有关, 只是架构师或系统设计人员的影响会更大点而已。 那如何做好高内聚低耦合呢, 就技术本身没有固定方法,往往需要根据实际需求将高内聚低耦合作为目标,选择或组合适当的设计模式来实现。 对于开发来说,学习设计模式比较重要,但熟悉设计模式六大原则,然后根据设计原则灵活组合设计模式更重要。另外对于实际项目来说,内聚耦合不仅依赖于开发或架构技术水平, 还于项目的周期长短,可投入资源多少,项目性质有关系, 这也是为什么要求设计人员有实际项目经验的原因。

  • 基于SpringBoot Actuator存活就绪检查

    什么是存活就绪检查?

    存活检查: 检查一个服务是否存活, 如果不存活,需要启动新的服务进行补充替换。对于JAVA 服务,如在Java 进程异常退出、 Java 进程假死等情况等不可自动恢复的情况,就任务是不存活的。

    就绪检查: 检查一个服务是否准备好,对外提供服务。 主要应用在新应用启动后,不立刻提供对外服务,而需要通过就绪检查确定提供的服务的资源都准备好后才提供对外的服务。

    为什么需要存活、就绪检查?

    对于一个被线上使用的服务来说,降低服务的可用性的原因(不考虑编码层面问题)主要有:

    • 发布不平滑。 发布时不能平滑替换服务,导致部分请求报错,降低可用性。
    • 服务故障修复不及时。 一般是服务过载情况下, 服务缺失故障自动修复能力,或自动扩展能力不足,导致服务不可用。
    • 没有备份。 单机或单区运行,在服务器异常或区域性依赖异常时,导致服务不可用。

    就绪检查主要会用在保障发布时保障服务可用时再提供对外服务使用,是平滑发布保障的关键一环。

    存活检查主要用在服务过程中,服务发生不可恢复故障时,发布工具能够自动及时的进行补救。

    如何实现存活就绪检查?

    根据存活就绪检查目的, 实现方法一般会对应用依赖重要组件进行检查,汇总后提供检查结果输出。 可以看出来这是比较适合进行封装的动作,SpringBoot Actuator 就这样一个包,虽然存活就绪只是其功能之一。如果需要自己实现,SpringBoot Actuator 是一个很好的参考。

    SpringBoot Actuator 做为存活就绪检查步骤

    SpringBoot Actuator提供的健康检查端点 /actuator/health ,虽然也可以做为存活和就绪检查使用,通过对存活和就绪作用及原理的了解,知道其满足不了存活就绪分离检测。 不过在Spring Boot 2.3及以上版本,Actuator新增了分组功能,通过对健康端点的各组件进行分组,通过分组自定义组内包含的检测组件,实现不同作用的检测。如可以设置liveness和readiness 两个分组, 然后通过/actuator/health/liveness和/actuator/health/readiness来作为检测接口。

    使用SpringBoot Actuator 基本上包含大部分组件的检查端点实现, 通过配置可以灵活的配置需要使用的端点。配置示例如下:

    management:
      endpoints:
        web:
          exposure:
            include: "health,info,env"  #开放访问的接口
          base-path: /actuator     #自定义基础路径, 默认/actuator
      endpoint:
        health:
          enabled: true            #是否开启,默认true
          show-details: ALWAYS     #health 接口是否返回详情信息,方便排查一般返回
          group:
            readiness:               #就绪检查组,访问地址 /health/readiness
              include: "ping"   #就绪组检查的组件
            liveness:                #存活检查组,访问地址 /health/liveness
              include: "ping"        #存活组要检查的组件
      health:
        defaults.enabled: false      #关闭自动启用组件检查,改由手动启用
        ping.enabled: true           #defaults.enabled 为false 情况下, 手动指定开启的检查端点
        mysql.enabled: true          

    如何确定哪些组件需要加入存活或就绪检测

    一般项目不复杂,引入组件很少或没有重要影响系统运行的三方组件情况下,可以使用

    Spring Boot Actuator提供的默认设置即可满足需求。但因为默认设置会默认加载所有引入组件包中带的Actuator 健康监测端点,而不管这个端点检测的组件是否使用,是否重要。为防止一些无效的包或不重要的组件检测故障导致存活或就绪检查失败,推荐自定义配置来定义设置线上项目的health 端点, 即把defaults.enabled 设置为false, 然后手工开启需要进行检测的组件。

    具体项目中包含哪些可用端点呢, 可以通过设置defaults.enabled 设置为true,

    开启actuator 健康检查,将health.defaults.enabled 设置为true的情况下,访问/actuator/health 就能查看到当前项目所有检查的端点。

    如果项目没有需要检测的组件,或组件不适合作为就绪或存活检查项, 可以启用ping 组件做空白验证。对于有重要组件,但没有自带的检测端点,也可以自实现端点接口HealthIndicator。 示例:

    public class MemHealthIndicator implements HealthIndicator {
        private final Logger LOGGER = LoggerFactory.getLogger(MemHealthIndicator.class);
        @Override
        public Health health() {
            MemoryMXBean memoryMbean = ManagementFactory.getMemoryMXBean();
            MemoryUsage usage = memoryMbean.getHeapMemoryUsage();
            Long max = usage.getMax();
            Long used = usage.getUsed();
            double useRate = used.doubleValue()/max.doubleValue();
            String useInfo = toString(usage);
            if(useRate > 0.9){
                LOGGER.info("jvm 内存使用超过90%,使用率{}",useInfo);
                return Health.down().withDetail("内存使用率",usage).build();
            }
            LOGGER.info("jvm 内存使用{}",useInfo);
            return Health.up().withDetail("内存使用率",usage).build();
        }
    }
    //其他配置类中
    ...
        @Bean
        @ConditionalOnEnabledHealthIndicator("mem") // 定义端点名称,配置中可以使用mem.enable:true 启用
        public HealthIndicator memHealthIndicator(){
            return new MemHealthIndicator();
        }
    ....