checkstyle中类的分散复杂度详解

属性信息
文档名称类的分散复杂度详解
版本号V1.1.0.0

文档修改日志

修改时间修改人修改描述版本号
2020-01-21 09:10宋全恒新建类的分散复杂度详解V1.0.0.0
2020-03-10 15:22宋全恒加入checkstyle的安装V1.1.0.0

1 简介

在使用checkstyle进行代码静态扫描时,有如下几种量度
在这里插入图片描述
这些代码质量衡量标准可以在Checkstyle4.3 中文手册中有定义,本文聚焦于类的分散复杂度,之前并未使用过checkstyle去衡量代码的优劣,由于28的要求,因此在使用了28的规则文件基础上,去统一代码的风格,checkstyle插件的安装并不复杂,上手也比较简单,在团队的共同努力下,代码的质量确实上升不少,最后只剩下了圈复杂度和***类的分散复杂度***两个问题,圈复杂度的理解在这个文档中不做详细的说明。

2 checkstyle安装

2.1 测试环境

因为笔者采用IDEA开发,因此在此介绍checkstyle插件在IDEA中的安装。笔者IDEA
在这里插入图片描述
使用快捷键Ctrl + Shift + A,键入Plugins选择
在这里插入图片描述
2.2 配置
在使用checkstyle之前,需要首先给出一份规则配置文件,该规则文件为xml文件。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC "-//Puppy Crawl//DTD Check Configuration 1.3//EN" "http://www.puppycrawl.com/dtds/configuration_1_3.dtd">

<!--
    This configuration file was written by the eclipse-cs plugin configuration editor
-->
<!--
    Checkstyle-Configuration: Copy of cetc28_regular
    Description: none
-->

<!--修改说明:
20180626:修改方法名长度15为提示。删除javadoc注释要求以.!?结尾的规则
-->
<module name="Checker">
    <property name="severity" value="error"/>
    <property name="charset" value="UTF-8"/>
    <module name="TreeWalker">
        <property name="tabWidth" value="4"/>

        <!-- 类的分散复杂度:一个类依靠的其他类的个数。这个数的平方也会被显示出来以表示最少需要的修改个数。-->
        <module name="ClassFanOutComplexity">
            <property name="max" value="7"/>
            <property name="severity" value="info"/>
        </module>

    </module>

    <!--建议5 -->
    <module name="FileLength">
        <property name="severity" value="info"/>
        <property name="max" value="1000"/>
    </module>

    <!--建议26-->
    <module name="JavadocPackage">
        <property name="severity" value="info"/>
    </module>

    <!-- 建议65 -->
    <!-- 部分checkstyle用不了此规则,用不了则注释掉。 5.7可用 -->
    <!-- <module name="StrictDuplicateCode">
    <property name="severity" value="info"/>
  </module> -->
</module>

事先准备好该配置,然后在IDEA运行界面快捷键Ctrl + Shift + A,键入checkstyle settings选择
在这里插入图片描述
然后安装插件到IDEA即可。
在这里插入图片描述

2.3 运行checkstyle扫描

Ctrl + Shift + A,键入checkstyle选择
在这里插入图片描述
在IDEA下部可以看到***CheckStyle面板***。
在这里插入图片描述
从上面可以看到生效的规则(Rules)规则文件,class_fanout为之前为规则文件添加的描述性文字,主题面板当前为当前扫描的结果。从上面的结果可以看到
ScheduledTaskService.java文件中
类的分散复杂度为14个,而规则文件中配置的上限为7。

3 类的分散复杂度定义

3.1 ClassFanOutComplexity定义

The number of other classes a given class relies on. Also the square of this has been shown to indicate the amount of maintenance required in functional programs on a file basis at least

一个类依靠的其他类的个数。这个数字的平方也会被显示出来,以表示最少需要的修改个数。

3.2 使用

如果想要使用类的分散复杂度去量度代码,则需要在checkstyle规则文件中添加如下的model。

<!-- 类的分散复杂度:一个类依靠的其他类的个数。这个数的平方也会被显示出来以表示最少需要的修改个数。-->
<module name="ClassFanOutComplexity">
    <property name="max" value="20"/>
    <property name="severity" value="info"/>
</module>

3.3 计算过程

 类分散复杂度困惑了我很久的时间, 原因是因为自己不会正确的计算出类的分散复杂度的值,当时真的是尤其想要解决这个问题,或许是因为程序员的强迫症的缘故,因此记得在回到住处时看了许多的材料试图去理解类分散复杂度的含义,终于被自己弄明白了,知其然也知其所以然。
在印象笔记中251-编程规范下有一篇笔记,名字叫做ClassFanOutComplexityCheck,可以方便的进行检索。
翻译8.28API中类分散复杂度的计算过程如下所示:

  1. Iterates over all tokens that might contain type reference.
  1. If a class was imported with direct import(i.e. import java.math.BigDecimal), or the class was referenced with the package name(i.e. java.math.BigDecimal value) and the package was added to the excludedPackages parameter, the class does note increase complexity.
  1. If a class name was added to the excludedClasses paramter, the class doesnot increase complexity.

具体理解如下图所示:
在这里插入图片描述
其中常见需要忽略的类包括如下:
在这里插入图片描述

4 理解实践

4.1 理解

想要理解类的分散复杂度,如何理解上述XMind中的概念一共有两个关键概念,如下所示:

  • token的含义

token指的是在一个Java文件中出现的变量

token:
  • 类中的公有成员、私有成员、受保护成员
  • 方法中参数、定义的局部变量

If a class was imported with direct import(i.e. import java.math.BigDecimal), or the class was referenced with the package name(i.e. java.math.BigDecimal value) and the package was added to the excludedPackages parameter, the class does note increase complexity.

上述是理解计算的关键,用大白话翻译一下:

理解上述语句的关键是or和and,and是且的关系。就是说如果你在checkstyle的配置文件中,通过excludedPackages配置了一个排除的包,即上述and后面的the package指java.math。表示如果java.math包被配置到了规则文件的excludedPackages,那么这个BigDecimal类是不会增加分散复杂度的。

注意:分散复杂度增加与否,是由token所属的类型有关的,即倘若在代码中出现了如下两个token

@Resource
private TaskService taskService;
private TaskService ttService;

虽然taskService和ttService均为token,但由于两个token所属的类型均为TaskService,那么这两个token只会导致类的分散复杂度增加1次。

4.2 实践

package com.camp.service;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.camp.common.Constant;
import com.camp.common.ServiceStatusEnum;
import com.camp.common.TaskStatus;
import com.camp.config.SystemConfig;
import com.camp.domain.Task;
import com.camp.domain.TaskFactory;
import com.camp.domain.entity.ServiceEntity;
import com.camp.domain.entity.TaskEntity;
import com.camp.mapper.ServiceManageMapper;
import com.camp.util.CommonReturn;
import com.camp.util.GeneralUtil;
import com.camp.util.HikException;
import com.camp.util.HttpUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Optional;


/**
 * 定时任务
 *
 * 2019-12-10 13:58
 */
@Service
@Slf4j
public class ScheduledTaskService {

    private final Integer querySuccess = 200;


    @Resource
    private TaskService taskService;

    @Resource
    private SystemConfig systemConfig;

    @Resource
    private ServiceManageMapper serviceManageMapper;

    @Resource
    private InferEngineService inferEngineService;

    @Value("${keepPicDays}")
    private Integer keepPicDays;

    @Value("${pic.real.path}")
    private String picRealPath;

    private static final String GET_ALL_INFERENGINE_INFO = "/api/inferengine/interfaces/infolist";


    public static final int SCHEDULED_TIME_LENGTH = 1 * 1000 * 60;

    /**
     * 60秒钟执行一次
     */
    @Scheduled(fixedRate = SCHEDULED_TIME_LENGTH)
    public void updateServiceTaskStatus() {
        // updateServiceStatus();
        updateTaskStatus();
    }

    /**
     * 每天凌晨1点钟清理失效图片
     */

    @Scheduled(cron = "0 0 1 * * ?")
    public void removeOutDatePic() {
        // 获取指定天数前的文件名称(带有日期的模糊文件名)
        Calendar date = Calendar.getInstance();
        date.add(Calendar.DATE, -keepPicDays);
        SimpleDateFormat sd = new SimpleDateFormat("yyyy-MM-dd");
        String beforeDate = sd.format(date.getTime());
        String pic = "\"" + beforeDate + "-*.jpg\"";

        // 调用shell命令删除过期失效图片文件
        StringBuilder command = new StringBuilder("find " + picRealPath + " -name ");
        command.append(pic + " | xargs rm -rf");
        log.info("命令:" + command.toString());
        Boolean removePicResult = GeneralUtil.executePipeLineShellCommand(command.toString());
        if (removePicResult) {
            log.info("定时删除失效图片文件成功!");
        } else {
            log.info("定时删除失效图片文件失败!");
        }
    }

    /**
     * 更新服务状态
     */
    public void updateServiceStatus() {
        log.info("开始定时更新服务的状态");

        // 查看数据库正在启动的服务列表信息
        List<ServiceEntity> recordRunningServices = serviceManageMapper.selectAllRunningService();

        Optional<JSONObject> result = HttpUtil.getRequest(systemConfig.getInferEngineUrl() + GET_ALL_INFERENGINE_INFO);

        if (!result.isPresent()) {
            log.error("调用算法服务未能获取服务列表信息,定时更新服务状态失败!!!");
            return;
        }
        // 调用服务推理引擎接口获取正在运行服务列表信息
        JSONObject actualServicesInfo = result.get();
        // 将数据库正在启动的服务信息和推理引擎记录的正在运行服务进行比较处理
        JSONArray runningServices = null;
        if (actualServicesInfo.getInteger("status").equals(querySuccess)) {
            runningServices = actualServicesInfo.getJSONArray("data");
            for (int i = 0; i < recordRunningServices.size(); i++) {
                // 标识该条服务状态信息是否需要变更,默认不需要
                Boolean needChangeStatus = false;
                ServiceEntity recordRunningService = recordRunningServices.get(i);
                for (int j = 0; j < runningServices.size(); j++) {
                    JSONObject runningService = runningServices.getJSONObject(j);
                    if (recordRunningService.getServerIp().equals(runningService.getString("server_ip"))
                            && recordRunningService.getServerPort().equals(runningService.getString("server_port"))) {
                        if (!runningService.getString("service_state").equals(recordRunningService.getServiceState())) {
                            needChangeStatus = true;
                        }
                        break;
                    } else if (j == (runningServices.size() - 1)) {
                        // 数据库中的服务信息不在正在运行的服务列表里,需要标记下并拉起服务
                        needChangeStatus = true;
                    }
                }
                // 尝试一次拉起异常挂了的服务
                /*if (needChangeStatus) {
                    pullUpService(recordRunningService);
                }*/
            }
            log.info("完成定时更新服务的状态");
        }

    }

    /**
     * 拉起服务
     *
     * @param recordRunningService 记录正在运行的服务信息
     */
    public void pullUpService(ServiceEntity recordRunningService) {
        // 根据数据表记录中服务类型重新创建服务
        JSONObject recoveryParam = new JSONObject();
        String serviceName = recordRunningService.getServiceName();
        recoveryParam.put("serviceName", serviceName);
        String serverIp = null;
        JSONObject result = inferEngineService.inferengineCreate(serviceName, serverIp);

        // 根据创建服务返回结果修改数据表信息
        if (result != null && querySuccess.equals(result.getInteger("status"))) {
            log.info(serviceName + "服务恢复成功!");
            JSONObject backInfo = result.getJSONObject("data");
            recordRunningService.setServiceId(backInfo.getString("service_id"));
            recordRunningService.setServerIp(backInfo.getString("server_ip"));
            recordRunningService.setServerPort(backInfo.getString("server_port"));
            recordRunningService.setServiceState(ServiceStatusEnum.RUNNING.getValue());
            recordRunningService.setTaskState(TaskStatus.IDLE.getValue());
            recordRunningService.setUpdateTime(new Date());
            serviceManageMapper.updateServiceStateById(recordRunningService);
        } else {
            log.error(serviceName + "服务恢复失败!");
            recordRunningService.setServiceState(ServiceStatusEnum.OFFLINE.getValue());
            recordRunningService.setTaskState(TaskStatus.IDLE.getValue());
            recordRunningService.setUpdateTime(new Date());
            serviceManageMapper.updateServiceStateById(recordRunningService);
        }

    }

    /**
     * 更新任务状态信息
     */
    public void updateTaskStatus() {
        List<TaskEntity> tasks = taskService.selectAll();
        tasks.stream().forEach(taskEntity -> handleTaskRecord(taskEntity));
    }

    /**
     * 处理任务记录信息
     *
     * @param taskEntity 任务信息
     */
    private void handleTaskRecord(TaskEntity taskEntity) {
        if (TaskStatus.CANNOT_GET_TASK_STATE.getValue().equalsIgnoreCase(taskEntity.getTaskStatus())
                || Constant.STOPSTATUS.equals(taskEntity.getTaskStatus())) {
            return;
        }

        Task task = TaskFactory.create(taskEntity);
        JSONObject result = new JSONObject();
        // 默认返回任务状态是失败
        result.put(Constant.STATUS, CommonReturn.FAILURE);
        try {
            result = task.status();
        } catch (HikException e) {
            log.error(e.getMessage());
        }
        if (GeneralUtil.isRequestFailed(result)) {
            // 如果上一次检测时,数据库中的任务状态为Running,而本次测试无法获取响应
            if (TaskStatus.RUNNING.getValue().equals(taskEntity.getTaskStatus())) {

                taskEntity.setTaskStatus(TaskStatus.CANNOT_GET_TASK_STATE.getValue());
                taskService.updateByBean(taskEntity);
                updateServiceTaskStateToIdle(taskEntity, task);
            }

        } else {
            JSONObject data = result.getJSONObject(Constant.DATA);
            String state = data.getString(Constant.STATE);
            // 数据表中任务状态与实际状态不一致时修改任务状态
            if (!taskEntity.getTaskStatus().equals(state)) {
                taskEntity.setTaskStatus(state);
                taskService.updateByBean(taskEntity);
                // 当任务的实际状态为成功停止时,需更新服务的状态(基于文件的算法分析会出现这种情况)
                if (TaskStatus.SUCCESSFULLY_STOPPED.getValue().equals(state)) {
                    updateServiceTaskStateToIdle(taskEntity, task);
                }
            }
        }
    }

    /**
     * 更新服务、任务状态至idle
     *
     * @param aTask 任务信息
     * @param task  任务实体类
     */
    private void updateServiceTaskStateToIdle(TaskEntity aTask, Task task) {
        log.info("ScheduledTaskService, 上次检测为Running, 而这次检测无法获取算法组返回的状态信息");
        log.info("更新任务对应的服务信息记录中的任务状态为Idle");
        String serviceName = task.getServiceName();
        String serviceIp = aTask.getIp();
        String servicePort = aTask.getPort();
        ServiceEntity serviceEntity = serviceManageMapper.getServiceInfo(serviceName, serviceIp, servicePort);
        serviceEntity.setTaskState(TaskStatus.IDLE.getValue());
        serviceEntity.setUpdateTime(new Date());
        serviceManageMapper.updateServiceStateById(serviceEntity);
    }

}

4.2.1 分析

通过运行checkstyle扫描可以看到当前类的分散复杂度为14
在这里插入图片描述
那么该14是如何产生的呢?

4.2.1.1 提炼tokens

在上述文件中可以提炼出文件中的token包括如下的内容
注意:可以通过Notepad++频繁使用正则表达式来进行文本处理,不然想要得到如下的结果,还是挺麻烦的。

Integer querySuccess
TaskService taskService;
SystemConfig systemConfig;
ServiceManageMapper serviceManageMapper;
InferEngineService inferEngineService;
Integer keepPicDays;
String picRealPath;
String GET_ALL_INFERENGINE_INFO
int SCHEDULED_TIME_LENGTH
Calendar date
SimpleDateFormat sd
String beforeDate
String pic
StringBuilder command
Boolean removePicResult
List<ServiceEntity> recordRunningServices
Optional<JSONObject> result
JSONObject actualServicesInfo
JSONArray runningServices
int i
Boolean needChangeStatus
ServiceEntity recordRunningService
int j
JSONObject runningService
ServiceEntity recordRunningService
JSONObject recoveryParam
String serviceName
String serverIp
JSONObject result
JSONObject backInfo
List<TaskEntity> tasks
TaskEntity taskEntity
Task task
JSONObject result
HikException e
JSONObject data
String state
String serviceName
String serviceIp
String servicePort
ServiceEntity serviceEntity
4.2.1.2 排序

排序可以通过Linux的命令来做,Notepad++工具也提供了按照数字或者字母进行整行排序的功能。

Boolean needChangeStatus
Boolean removePicResult
Calendar date
HikException e
InferEngineService inferEngineService;
Integer keepPicDays;
Integer querySuccess
JSONArray runningServices
JSONObject actualServicesInfo
JSONObject backInfo
JSONObject data
JSONObject recoveryParam
JSONObject result
JSONObject result
JSONObject runningService
List<ServiceEntity> recordRunningServices
List<TaskEntity> tasks
Optional<JSONObject> result
ServiceEntity recordRunningService
ServiceEntity recordRunningService
ServiceEntity serviceEntity
ServiceManageMapper serviceManageMapper;
SimpleDateFormat sd
String GET_ALL_INFERENGINE_INFO
String beforeDate
String pic
String picRealPath;
String serverIp
String serviceIp
String serviceName
String serviceName
String servicePort
String state
StringBuilder command
SystemConfig systemConfig;
Task task
TaskEntity taskEntity
TaskService taskService;
int SCHEDULED_TIME_LENGTH
int i
int j

移除token的具体名称,只保存类型

Boolean
Boolean
Calendar
HikException
InferEngineService
Integer
Integer
JSONArray
JSONObject
JSONObject
JSONObject
JSONObject
JSONObject
JSONObject
JSONObject
List<ServiceEntity>
List<TaskEntity>
Optional<JSONObject>
ServiceEntity
ServiceEntity
ServiceEntity
ServiceManageMapper
SimpleDateFormat
String GET_ALL_INFERENGINE_INFO
String
String
String
String
String
String
String
String
String
StringBuilder
SystemConfig
Task
TaskEntity
TaskService
int SCHEDULED_TIME_LENGTH
int
int
4.2.1.3 移除重复行

由于在计算类的分散复杂度的时候,计算的是token所属的类型,因此,此时把重复的类型
可以通过Linux Shell命令行sort –u

sort –u

或者uniq

uniq

去除重复行
把上述结果保存为a.java

cat a.java | sort -u
cat a.java | uniq
Boolean
Calendar
HikException
InferEngineService
int
int
Integer
JSONArray
JSONObject
List<ServiceEntity>
List<TaskEntity>
Optional<JSONObject>
ServiceEntity
ServiceManageMapper
SimpleDateFormat
String
StringBuilder
SystemConfig
Task
TaskEntity
TaskService
4.2.1.4 删除excludedClasses中包含的类型
Calendar
HikException
InferEngineService
JSONArray
JSONObject
List<ServiceEntity>
List<TaskEntity>
Optional<JSONObject>
ServiceEntity
ServiceManageMapper
SimpleDateFormat
SystemConfig
Task
TaskEntity
TaskService

注意: 由于List和List中使用了Java泛型,但实际的类型均为List,因此移除1个,即可得到最终的结果:

4.2.1.5 结果
Calendar
HikException
InferEngineService
JSONArray
JSONObject
List
Optional
ServiceEntity
ServiceManageMapper
SimpleDateFormat
SystemConfig
Task
TaskEntity
TaskService

上述需要计算的class的个数正好为14,即为
在这里插入图片描述
扫描的结果。

4.2.2 自定义

<!-- 类的分散复杂度:一个类依靠的其他类的个数。这个数的平方也会被显示出来以表示最少需要的修改个数。-->
<module name="ClassFanOutComplexity">
    <property name="max" value="7"/>
<property name="severity" value="info"/>
<property name="excludedClasses" value="info"/>
<property name="excludedPackages" value="java.util"/>
</module>

可以通过配置excludedClasses或者excludedPackages来控制扫描过程中分散复杂度的计算。
注意在配置excludedPackages时,需要使用合法的标识符(valid identifier)

  • java.util 这表示排除在java.util包下的所有类,但不排除该包下subpackages中的类。
  • java.util.
  • java.util.*

5 总结

该文档详细描述了类分散复杂度的定义、概念,并且通过分散复杂度的计算过程中详细描述了分散复杂度的由来,通过降低类的分散复杂度可以增强类型的内聚性,增强代码质量。

														202032217:04:16 于马塍路36
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值