【问题标题】:Reduce the Cyclomatic complexity of a function降低函数的圈复杂度
【发布时间】:2017-12-28 09:33:46
【问题描述】:

我有一个函数,它使用 JPA Criteria API 根据我传递给它的一些参数从数据库中检索数据。 参数使用gridParams 对象传递,如下:

public List<MyObjectDTO> find(final GridParamsDTO gridParams) {

        final CriteriaBuilder builder = getCriteriaBuilder();
        final List<Predicate> predicates = new ArrayList<>();

        final CriteriaQuery<MyObjectDTO> criteriaQuery = builder.createQuery(MyObjectDTO.class);

        final Root<MyObject> from = criteriaQuery.from(MyObject.class);

        Join<MyObject, AnotherObject> join = from.join(MyObject_.anotherObject, JoinType.LEFT);

        if ((gridParams.getFilter() != null) && (gridParams.getFilter().length > 0)) {

            for (final GridFilterDTO filter : gridParams.getFilter()) {
                if ("fieldX".equals(filter.getField())) {
                    final List<Predicate> sqPredicates = new ArrayList<>();
                    Subquery<Long> sq = criteriaQuery.subquery(Long.class);
                    Root<ObjectX> sqFrom = sq.from(ObjectX.class);
                    sqPredicates
                            .add(builder.equal(sqFrom.get(ObjectX_.organismeIDE), filter.getFilterSingleValue()));
                    sq.select(sqFrom.get(ObjectX_.referentielID).get(MyObject_.objectX));
                    sq.where(builder.and(sqPredicates.toArray(new Predicate[sqPredicates.size()])));
                    predicates.add(builder.in(from.get(MyObject_.objectX)).value(sq));
                }
                if ("fieldA".equals(filter.getField()) || "fieldB".equals(filter.getField())) {
                    final GridFilterOperatorEnum gridFilterEnumValues = GridFilterOperatorEnum.values()[filter.getOperator().ordinal()];
                    switch (gridFilterEnumValues) {
                    case TEXT_CONTAINS:
                        predicates.add(builder.like(builder.upper(from.get(filter.getField())), "%" + filter.getFilterSingleValue().toUpperCase() + "%"));
                        break;
                    case TEXT_START_WITH:
                        predicates.add(builder.like(builder.upper(from.get(filter.getField())), filter.getFilterSingleValue().toUpperCase() + "%"));
                        break;
                    case TEXT_DIFFERENT:
                        predicates.add(builder.notEqual(from.get(filter.getField()), filter.getFilterSingleValue()));
                        break;
                    case TEXT_END_WITH:
                        predicates.add(builder.like(builder.upper(from.get(filter.getField())), "%" + filter.getFilterSingleValue().toUpperCase()));
                        break;
                    case TEXT_EQUALS:
                        predicates.add(builder.equal(builder.upper(from.get(filter.getField())), filter.getFilterSingleValue().toUpperCase()));
                        break;
                    default:
                        break;
                    }
                }
                if ("dateField1".equals(filter.getField()) || "dateField2".equals(filter.getField())) {
                    final GridFilterOperatorEnum gridFilterEnumValues = GridFilterOperatorEnum.values()[filter.getOperator().ordinal()];
                    switch (gridFilterEnumValues) {
                    case DATE_EQUALS:
                        predicates.add(builder.equal(from.<LocalDate> get(filter.getField()), LocalDate.parse(filter.getFilterSingleValue())));
                        break;
                    case DATE_BEFORE:
                        predicates.add(builder.lessThan(from.<LocalDate> get(filter.getField()), LocalDate.parse(filter.getFilterSingleValue())));
                        break;
                    case DATE_AFTER:
                        predicates.add(builder.greaterThan(from.<LocalDate> get(filter.getField()), LocalDate.parse(filter.getFilterSingleValue())));
                        break;
                    case DATE_BETWEEN:
                        predicates.add(
                                builder.between(from.<LocalDate> get(filter.getField()), LocalDate.parse(filter.getFilterSetValues()[0]),
                                        LocalDate.parse(filter.getFilterSetValues()[1])));
                        break;
                    default:
                        break;
                    }
                }
                if ("anotherField".equals(filter.getField())) {
                    if (filter.getFilterSetValues().length > 0) {
                        predicates
                                .add(builder.isTrue(join.get(AnotherObject_.lib).in((Object[]) filter.getFilterSetValues())));
                    } else {
                        predicates.add(join.get(AnotherObject_.lib).isNull());
                    }

                }
            }

        }

        if (gridParams.hasSort()) {
            List<Order> orderList = new ArrayList<>();
            for (IGridSort gridSort : gridParams.getSort()) {
                if (ArrayUtils.contains(new String[] { "fieldA", "dateField1", "dateField2", "fieldB" }, gridSort.getField())) {
                    if (gridSort.getSort().equalsIgnoreCase(GridSortDTO.ASC)) {
                        orderList.add(builder.asc(from.get(gridSort.getField())));
                    } else if (gridSort.getSort().equalsIgnoreCase(GridSortDTO.DESC)) {
                        orderList.add(builder.desc(from.get(gridSort.getField())));
                    }
                }
                if (gridSort.getField().equalsIgnoreCase("anotherField")) {
                    if (gridSort.getSort().equalsIgnoreCase(GridSortDTO.ASC)) {
                        orderList.add(builder.asc(join.get(AnotherObject_.lib)));
                    } else if (gridSort.getSort().equalsIgnoreCase(GridSortDTO.DESC)) {
                        orderList.add(builder.desc(join.get(AnotherObject_.lib)));
                    }
                }
            }
            if (!orderList.isEmpty()) {
                criteriaQuery.orderBy(orderList);
            }
        }

        predicates.add(builder.between(builder.literal(LocalDate.now()), from.get(MyObject_.dateDebut),
                from.get(MyObject_.dateFin)));

        criteriaQuery.multiselect(from.get(MyObject_.objectX), from.get(MyObject_.dateDebut), from.get(MyObject_.dateFin),
                from.get(MyObject_.lib), from.get(MyObject_.coleur), join,
                from.get(MyObject_.refExterne));
        criteriaQuery.where(builder.and(predicates.toArray(new Predicate[predicates.size()])));
        criteriaQuery.distinct(true);

        final TypedQuery<MyObjectDTO> typedQuery = getEntityManager().createQuery(criteriaQuery);

        if (gridParams.hasStart()) {
            typedQuery.setFirstResult(gridParams.getStartRow());
        }
        if (gridParams.hasEnd()) {
            typedQuery.setMaxResults(gridParams.getEndRow() - Math.max(0, gridParams.getStartRow()));
        }

        return typedQuery.getResultList();
    }
}

问题是SonarQube抱怨这个方法“find”的圈复杂度是31,大于授权的30,我做了一些研究,我发现我必须重构我的函数以降低圈复杂度。

在这个函数中,我必须测试gridParams 是否定义了 4 个字段:(开始、结束、排序和过滤),排序和过滤是对象数组,所以我必须遍历它们。

最终我需要检查很多字段,如果没有大量检查,我看不到如何做到这一点:/

我该如何解决这个问题?

【问题讨论】:

  • 乍一看,你需要摆脱深层的if-else 层次结构才能快速修复它。
  • 这个方法很臃肿,所以为了摆脱声纳问题,只需将方法拆分成更小的方法

标签: java refactoring cyclomatic-complexity


【解决方案1】:

您可能希望从查看 Refactoring Catalog 中的一些重构开始。

如果你看看你的方法,你有很多部分:

  • 编译谓词列表的部分
  • 编译订单列表的部分
  • 创建查询的部分
  • 运行查询的部分

这些是使用提取方法重构重构为单独方法的良好候选者。一旦将这些部分提取到新方法中,就可以对每种方法重复该过程,直到您认为自己已经尽了最大努力。然后,您可以查看您创建的新方法,并确定那些操作相同数据的方法。他们可能希望被移动到一个单独的类,例如,一个查询类。

switch 语句看起来也很适合用多态性重构替换条件。

如有疑问,请从小处着手,一次进行一项简单的重构。

【讨论】:

    【解决方案2】:

    以下是提取方法的方法:

    public List<MyObjectDTO> find(final GridParamsDTO gridParams) {
        final CriteriaBuilder builder = getCriteriaBuilder();
    
        final CriteriaQuery<MyObjectDTO> criteriaQuery = builder.createQuery(MyObjectDTO.class);
        final Root<MyObject> from = criteriaQuery.from(MyObject.class);
        final Join<MyObject, AnotherObject> join = from.join(MyObject_.anotherObject, JoinType.LEFT);
    
        final List<Order> orderList = buildOrderList(gridParams, builder, from, join);
        if (!orderList.isEmpty()) {
            criteriaQuery.orderBy(orderList);
        }
    
        criteriaQuery.multiselect(from.get(MyObject_.objectX),
                                  from.get(MyObject_.dateDebut),
                                  from.get(MyObject_.dateFin),
                                  from.get(MyObject_.lib),
                                  from.get(MyObject_.coleur),
                                  join,
                                  from.get(MyObject_.refExterne));
        criteriaQuery.where(builder.and(buildPredicates(gridParams, builder, criteriaQuery, from, join)));
        criteriaQuery.distinct(true);
    
        final TypedQuery<MyObjectDTO> typedQuery = getEntityManager().createQuery(criteriaQuery);
        if (gridParams.hasStart()) {
            typedQuery.setFirstResult(gridParams.getStartRow());
        }
        if (gridParams.hasEnd()) {
            typedQuery.setMaxResults(gridParams.getEndRow() - Math.max(0, gridParams.getStartRow()));
        }
        return typedQuery.getResultList();
    }
    

     

    private Predicate[] buildPredicates(final GridParamsDTO gridParams, final CriteriaBuilder builder,
            final CriteriaQuery<MyObjectDTO> criteriaQuery, final Root<MyObject> from,
            Join<MyObject, AnotherObject> join) {
        final List<Predicate> predicates = new ArrayList<>();
        final GridFilterDTO[] filters = gridParams.getFilter();
        if (filters != null && filters.length > 0) {
            for (final GridFilterDTO filter : filters) {
                if ("fieldX".equals(filter.getField())) {
                    addSubQuery(builder, predicates, criteriaQuery, from, filter);
                }
                if ("fieldA".equals(filter.getField()) || "fieldB".equals(filter.getField())) {
                    addTextPredicates(builder, predicates, from, filter);
                }
                if ("dateField1".equals(filter.getField()) || "dateField2".equals(filter.getField())) {
                    addDatePredicates(builder, predicates, from, filter);
                }
                if ("anotherField".equals(filter.getField())) {
                    if (filter.getFilterSetValues().length > 0) {
                        predicates
                                .add(builder.isTrue(join.get(AnotherObject_.lib).in((Object[]) filter.getFilterSetValues())));
                    } else {
                        predicates.add(join.get(AnotherObject_.lib).isNull());
                    }
                }
            }
        }
        predicates.add(builder.between(builder.literal(LocalDate.now()), from.get(MyObject_.dateDebut),
                from.get(MyObject_.dateFin)));
        return predicates.toArray(new Predicate[predicates.size()]);
    }
    

     

    private void addSubQuery(final CriteriaBuilder builder, final List<Predicate> predicates,
            final CriteriaQuery<MyObjectDTO> criteriaQuery, final Root<MyObject> from, final GridFilterDTO filter) {
        final List<Predicate> sqPredicates = new ArrayList<>();
        Subquery<Long> sq = criteriaQuery.subquery(Long.class);
        Root<ObjectX> sqFrom = sq.from(ObjectX.class);
        sqPredicates.add(builder.equal(sqFrom.get(ObjectX_.organismeIDE), filter.getFilterSingleValue()));
        sq.select(sqFrom.get(ObjectX_.referentielID).get(MyObject_.objectX));
        sq.where(builder.and(sqPredicates.toArray(new Predicate[sqPredicates.size()])));
        predicates.add(builder.in(from.get(MyObject_.objectX)).value(sq));
    }
    

     

    private void addTextPredicates(final CriteriaBuilder builder, final List<Predicate> predicates,
            final Root<MyObject> from, final GridFilterDTO filter) {
        final GridFilterOperatorEnum gridFilterEnumValues = GridFilterOperatorEnum.values()[filter.getOperator().ordinal()];
        switch (gridFilterEnumValues) {
        case TEXT_CONTAINS:
            predicates.add(builder.like(builder.upper(from.get(filter.getField())), "%" + filter.getFilterSingleValue().toUpperCase() + "%"));
            break;
        case TEXT_START_WITH:
            predicates.add(builder.like(builder.upper(from.get(filter.getField())), filter.getFilterSingleValue().toUpperCase() + "%"));
            break;
        case TEXT_DIFFERENT:
            predicates.add(builder.notEqual(from.get(filter.getField()), filter.getFilterSingleValue()));
            break;
        case TEXT_END_WITH:
            predicates.add(builder.like(builder.upper(from.get(filter.getField())), "%" + filter.getFilterSingleValue().toUpperCase()));
            break;
        case TEXT_EQUALS:
            predicates.add(builder.equal(builder.upper(from.get(filter.getField())), filter.getFilterSingleValue().toUpperCase()));
            break;
        default:
            break;
        }
    }
    

     

    private void addDatePredicates(final CriteriaBuilder builder, final List<Predicate> predicates,
            final Root<MyObject> from, final GridFilterDTO filter) {
        final GridFilterOperatorEnum gridFilterEnumValues = GridFilterOperatorEnum.values()[filter.getOperator().ordinal()];
        switch (gridFilterEnumValues) {
        case DATE_EQUALS:
            predicates.add(builder.equal(from.<LocalDate> get(filter.getField()), LocalDate.parse(filter.getFilterSingleValue())));
            break;
        case DATE_BEFORE:
            predicates.add(builder.lessThan(from.<LocalDate> get(filter.getField()), LocalDate.parse(filter.getFilterSingleValue())));
            break;
        case DATE_AFTER:
            predicates.add(builder.greaterThan(from.<LocalDate> get(filter.getField()), LocalDate.parse(filter.getFilterSingleValue())));
            break;
        case DATE_BETWEEN:
            predicates.add(
                    builder.between(from.<LocalDate> get(filter.getField()), LocalDate.parse(filter.getFilterSetValues()[0]),
                            LocalDate.parse(filter.getFilterSetValues()[1])));
            break;
        default:
            break;
        }
    }
    

     

    private List<Order> buildOrderList(final GridParamsDTO gridParams, final CriteriaBuilder builder,
            final Root<MyObject> from, Join<MyObject, AnotherObject> join) {
        if (gridParams.hasSort()) {
            return Collections.emptyList();
        }
        List<Order> orderList = new ArrayList<>();
        for (IGridSort gridSort : gridParams.getSort()) {
            if (ArrayUtils.contains(new String[] { "fieldA", "dateField1", "dateField2", "fieldB" }, gridSort
                    .getField())) {
                if (gridSort.getSort().equalsIgnoreCase(GridSortDTO.ASC)) {
                    orderList.add(builder.asc(from.get(gridSort.getField())));
                } else if (gridSort.getSort().equalsIgnoreCase(GridSortDTO.DESC)) {
                    orderList.add(builder.desc(from.get(gridSort.getField())));
                }
            }
            if (gridSort.getField().equalsIgnoreCase("anotherField")) {
                if (gridSort.getSort().equalsIgnoreCase(GridSortDTO.ASC)) {
                    orderList.add(builder.asc(join.get(AnotherObject_.lib)));
                } else if (gridSort.getSort().equalsIgnoreCase(GridSortDTO.DESC)) {
                    orderList.add(builder.desc(join.get(AnotherObject_.lib)));
                }
            }
        }
        return orderList;
    }
    

    现在由您决定这是否更好。 你甚至可以提取更多的方法和更多的局部变量来避免这段代码中过多的重复。

    【讨论】:

      【解决方案3】:

      你可以试试这样的:

      public List<MyObjectDTO> find(final GridParamsDTO gridParams) {
      
          CriteriaBuilder builder = getCriteriaBuilder();
          List<Predicate> predicates = new ArrayList<>();
      
          CriteriaQuery<MyObjectDTO> criteriaQuery = builder.createQuery(MyObjectDTO.class);
      
          Root<MyObject> from = criteriaQuery.from(MyObject.class);
      
          Join<MyObject, AnotherObject> join = from.join(MyObject_.anotherObject, JoinType.LEFT);
      
          predicates = subMethod1(gridParams, builder, predicates, criteriaQuery, from, join);
      
          criteriaQuery = subMethod2(gridParams, builder, criteriaQuery, from, join);
      
          predicates.add(builder.between(builder.literal(LocalDate.now()), from.get(MyObject_.dateDebut),
                  from.get(MyObject_.dateFin)));
      
          criteriaQuery.multiselect(from.get(MyObject_.objectX), from.get(MyObject_.dateDebut), from.get(MyObject_.dateFin),
                  from.get(MyObject_.lib), from.get(MyObject_.coleur), join,
                  from.get(MyObject_.refExterne));
          criteriaQuery.where(builder.and(predicates.toArray(new Predicate[predicates.size()])));
          criteriaQuery.distinct(true);
      
          final TypedQuery<MyObjectDTO> typedQuery = getEntityManager().createQuery(criteriaQuery);
      
          if (gridParams.hasStart()) {
              typedQuery.setFirstResult(gridParams.getStartRow());
          }
          if (gridParams.hasEnd()) {
              typedQuery.setMaxResults(gridParams.getEndRow() - Math.max(0, gridParams.getStartRow()));
          }
      
          return typedQuery.getResultList();
      }
      
      private CriteriaQuery<MyObjectDTO> subMethod2(GridParamsDTO gridParams, CriteriaBuilder builder,
              CriteriaQuery<MyObjectDTO> criteriaQuery, Root<MyObject> from,
              Join<MyObject, AnotherObject> join) {
          if (gridParams.hasSort()) {
              List<Order> orderList = new ArrayList<>();
              for (IGridSort gridSort : gridParams.getSort()) {
                  if (ArrayUtils.contains(new String[] { "fieldA", "dateField1", "dateField2", "fieldB" }, gridSort.getField())) {
                      if (gridSort.getSort().equalsIgnoreCase(GridSortDTO.ASC)) {
                          orderList.add(builder.asc(from.get(gridSort.getField())));
                      } else if (gridSort.getSort().equalsIgnoreCase(GridSortDTO.DESC)) {
                          orderList.add(builder.desc(from.get(gridSort.getField())));
                      }
                  }
                  if (gridSort.getField().equalsIgnoreCase("anotherField")) {
                      if (gridSort.getSort().equalsIgnoreCase(GridSortDTO.ASC)) {
                          orderList.add(builder.asc(join.get(AnotherObject_.lib)));
                      } else if (gridSort.getSort().equalsIgnoreCase(GridSortDTO.DESC)) {
                          orderList.add(builder.desc(join.get(AnotherObject_.lib)));
                      }
                  }
              }
              if (!orderList.isEmpty()) {
                  criteriaQuery.orderBy(orderList);
              }
          }
          return criteriaQuery;
      }
      
      private List<Predicate> subMethod1(GridParamsDTO gridParams, CriteriaBuilder builder,
              List<Predicate> predicates, CriteriaQuery<MyObjectDTO> criteriaQuery, Root<MyObject> from,
              Join<MyObject, AnotherObject> join) {
          if ((gridParams.getFilter() != null) && (gridParams.getFilter().length > 0)) {
      
              for (final GridFilterDTO filter : gridParams.getFilter()) {
                  if ("fieldX".equals(filter.getField())) {
                      final List<Predicate> sqPredicates = new ArrayList<>();
                      Subquery<Long> sq = criteriaQuery.subquery(Long.class);
                      Root<ObjectX> sqFrom = sq.from(ObjectX.class);
                      sqPredicates
                              .add(builder.equal(sqFrom.get(ObjectX_.organismeIDE), filter.getFilterSingleValue()));
                      sq.select(sqFrom.get(ObjectX_.referentielID).get(MyObject_.objectX));
                      sq.where(builder.and(sqPredicates.toArray(new Predicate[sqPredicates.size()])));
                      predicates.add(builder.in(from.get(MyObject_.objectX)).value(sq));
                  } else if ("fieldA".equals(filter.getField()) || "fieldB".equals(filter.getField())) {
                      final GridFilterOperatorEnum gridFilterEnumValues = GridFilterOperatorEnum.values()[filter.getOperator().ordinal()];
                      switch (gridFilterEnumValues) {
                      case TEXT_CONTAINS:
                          predicates.add(builder.like(builder.upper(from.get(filter.getField())), "%" + filter.getFilterSingleValue().toUpperCase() + "%"));
                          break;
                      case TEXT_START_WITH:
                          predicates.add(builder.like(builder.upper(from.get(filter.getField())), filter.getFilterSingleValue().toUpperCase() + "%"));
                          break;
                      case TEXT_DIFFERENT:
                          predicates.add(builder.notEqual(from.get(filter.getField()), filter.getFilterSingleValue()));
                          break;
                      case TEXT_END_WITH:
                          predicates.add(builder.like(builder.upper(from.get(filter.getField())), "%" + filter.getFilterSingleValue().toUpperCase()));
                          break;
                      case TEXT_EQUALS:
                          predicates.add(builder.equal(builder.upper(from.get(filter.getField())), filter.getFilterSingleValue().toUpperCase()));
                          break;
                      default:
                          break;
                      }
                  } else if ("dateField1".equals(filter.getField()) || "dateField2".equals(filter.getField())) {
                      final GridFilterOperatorEnum gridFilterEnumValues = GridFilterOperatorEnum.values()[filter.getOperator().ordinal()];
                      switch (gridFilterEnumValues) {
                      case DATE_EQUALS:
                          predicates.add(builder.equal(from.<LocalDate> get(filter.getField()), LocalDate.parse(filter.getFilterSingleValue())));
                          break;
                      case DATE_BEFORE:
                          predicates.add(builder.lessThan(from.<LocalDate> get(filter.getField()), LocalDate.parse(filter.getFilterSingleValue())));
                          break;
                      case DATE_AFTER:
                          predicates.add(builder.greaterThan(from.<LocalDate> get(filter.getField()), LocalDate.parse(filter.getFilterSingleValue())));
                          break;
                      case DATE_BETWEEN:
                          predicates.add(
                                  builder.between(from.<LocalDate> get(filter.getField()), LocalDate.parse(filter.getFilterSetValues()[0]),
                                          LocalDate.parse(filter.getFilterSetValues()[1])));
                          break;
                      default:
                          break;
                      }
                  } else if ("anotherField".equals(filter.getField())) {
                      if (filter.getFilterSetValues().length > 0) {
                          predicates
                                  .add(builder.isTrue(join.get(AnotherObject_.lib).in((Object[]) filter.getFilterSetValues())));
                      } else {
                          predicates.add(join.get(AnotherObject_.lib).isNull());
                      }
      
                  }
              }
      
          }
          return predicates;
      }
      

      【讨论】:

      • 有什么意义?这段代码的可读性比以前更差了。
      • 圈复杂度是每个方法的复杂度,并计算 if/else、循环、运算符、异常等的数量。因此,如果您拆分方法的逻辑,您可以有一个更干净代码,也许您可​​以将该代码重用于其他方法,当然复杂性会降低。
      • 我知道圈复杂度是什么,也知道它的缺点(见“认知复杂度”)。授予新代码有效地降低了圈复杂度。但代码质量不仅仅是原始数字。提取方法的名称可能更具表现力。可以以更具凝聚力/表现力的方式以不同方式提取这些方法。
      • 我只是举个例子。您应该更清楚什么最适合您的情况。只是想帮忙。
      猜你喜欢
      • 2020-10-03
      • 1970-01-01
      • 2020-02-13
      • 1970-01-01
      • 1970-01-01
      • 2020-06-13
      相关资源
      最近更新 更多