一次重复代码重构的思考及探索

分离和组合关注点。

引子

如下代码所示:


@Setter
@Getter
public class ConnBriefInfo {
    private Long visitCount;
    private String firstTime;
    private String lastTime;

    public static Comparator<? super ConnBriefInfo> getComparator(Sort sort) {
        if (sort == null) {
            return (o1, o2) -> (StringUtils.compare(o2.getFirstTime(), o1.getFirstTime()));
        }

        if (sort.iterator().hasNext()) {
            Sort.Order order = sort.iterator().next();
            if ("firstTime".equals(order.getProperty())) {
                if (order.getDirection().isAscending()) {
                    return (o1, o2) -> (StringUtils.compare(o1.getFirstTime(), o2.getFirstTime()));
                } else {
                    return (o1, o2) -> (StringUtils.compare(o2.getFirstTime(), o1.getFirstTime()));
                }
            }

            if ("lastTime".equals(order.getProperty())) {
                if (order.getDirection().isAscending()) {
                    return (o1, o2) -> (StringUtils.compare(o1.getLastTime(), o2.getLastTime()));
                } else {
                    return (o1, o2) -> (StringUtils.compare(o2.getLastTime(), o1.getLastTime()));
                }
            }

            if ("visitCount".equals(order.getProperty())) {
                if (order.getDirection().isAscending()) {
                    return Comparator.comparingLong(ConnBriefInfo::getVisitCount);
                } else {
                    return (o1, o2) -> (Long.compare(o2.getVisitCount(), o1.getVisitCount()));
                }
            }
        }

        return (o1, o2) -> (StringUtils.compare(o2.getFirstTime(), o1.getFirstTime()));
    }
}

displayMd5s = connBriefInfos.stream()
                    .sorted(ConnBriefInfo.getComparator(param.toPageable().getSort()))
                    .skip((param.getPage() - 1) * param.getSize())
                    .limit(param.getSize())
                    .collect(Collectors.toList());
@Getter
@Setter
public class ConnectionList {

    private long visitCount;
    private String firstTime;
    private String lastTime;

    public static Comparator<? super ConnectionList> getComparator(Sort sort) {
        if (sort == null) {
            return (o1, o2) -> (StringUtils.compare(o2.getFirstTime(), o1.getFirstTime()));
        }

        if (sort.iterator().hasNext()) {
            Sort.Order order = sort.iterator().next();
            if ("firstTime".equals(order.getProperty())) {
                if (order.getDirection().isAscending()) {
                    return (o1, o2) -> (StringUtils.compare(o1.getFirstTime(), o2.getFirstTime()));
                } else {
                    return (o1, o2) -> (StringUtils.compare(o2.getFirstTime(), o1.getFirstTime()));
                }
            }

            if ("lastTime".equals(order.getProperty())) {
                if (order.getDirection().isAscending()) {
                    return (o1, o2) -> (StringUtils.compare(o1.getLastTime(), o2.getLastTime()));
                } else {
                    return (o1, o2) -> (StringUtils.compare(o2.getLastTime(), o1.getLastTime()));
                }
            }

            if ("visitCount".equals(order.getProperty())) {
                if (order.getDirection().isAscending()) {
                    return Comparator.comparingLong(ConnectionList::getVisitCount);
                } else {
                    return (o1, o2) -> (Long.compare(o2.getVisitCount(), o1.getVisitCount()));
                }
            }
        }

        return (o1, o2) -> (StringUtils.compare(o2.getFirstTime(), o1.getFirstTime()));
    }

}

connectionList.stream()
        .sorted(ConnectionList.getComparator(param.toPageable().getSort()))
        .collect(Collectors.toList());

两段 getComparator 有一些明显重复的代码。 看上去应该可以消减这种重复,不过仔细一看,似乎还不那么容易。

这里有三点差异:

  • 根据指定字段比较;
  • 根据指定方向排序;
  • 返回指定对象类型的比较器。

看上去是三个不同维度的用来排序的组合。怎么才能把这三个维度分离开呢?


使用反射进行优化

第一个自然的想法,是使用反射的方式,将字段的获取通用化。

如下所示: 可以说比之前整洁多了,这样的代码对于生产环境,已经足够好了。只是,返回的对象类型仍然有点受限,且处理的类型也有点受限。有没有办法做得更好呢?


使用函数式编程

要按照指定字段排序,除了使用反射,还可以使用函数式编程。 函数式编程的前身就是函数指针。

  • 用字段获取函数来表达获取对象指定字段的语义,用枚举来获取字段对应的字段获取函数;
  • 比较对象实现比较字段的接口;

如下所示:


/**
 * @Description 获取对象比较器
 *
 * 差异点:
 * 1. 比较使用的字段不同,依据指定字段
 * 2. 比较的顺序不同;
 * 3. 返回的比较器对象不同;
 */

public class ComparatorGenerator {

    public static Comparator<? extends ComparatorObject> getComparator(Function<ComparatorObject,Object> compFunc, boolean isAscending) {
        return isAscending ? (o1, o2) -> (StringUtils.compare(String.valueOf(compFunc.apply(o1)), String.valueOf(compFunc.apply(o2)))) :
                                     (o1, o2) -> (StringUtils.compare(String.valueOf(compFunc.apply(o2)), String.valueOf(compFunc.apply(o1))));
    }

    public static Comparator<? extends ComparatorObject> getComparator(Sort sort) {

        if (sort == null) {
            return getComparator(ComparatorFuncEnum.getCompFunc("firstTime"), true);
        }

        if (sort.iterator().hasNext()) {
            Sort.Order order = sort.iterator().next();
            Function<ComparatorObject, Object> compFunc = ComparatorFuncEnum.getCompFunc(order.getProperty());
            return getComparator(compFunc, order.getDirection().isAscending());
        }

        return getComparator(ComparatorFuncEnum.getCompFunc("firstTime"), true);

    }


    @Getter

    enum ComparatorFuncEnum {

        firstTime("firstTime", ComparatorObject::getFirstTime),
        lastTime("lastTime", ComparatorObject::getLastTime),
        visitCount("visitCount", ComparatorObject::getVisitCount),

        ;

        private String field;
        Function<ComparatorObject, Object> compFunc;

        ComparatorFuncEnum(String field, Function<ComparatorObject, Object> compFunc) {
            this.field = field;
            this.compFunc = compFunc;
        }

        public static Function<ComparatorObject, Object> getCompFunc(String field) {
            for (ComparatorFuncEnum cfe: ComparatorFuncEnum.values()) {
                if (cfe.getField().equals(field)) {
                    return cfe.compFunc;
                }
            }
            return null;
        }
    }

}

public interface ComparatorObject {
    Long getVisitCount();
    String getFirstTime();
    String getLastTime();
}
 
public class ConnBriefInfo implements ComparatorObject {
    // ...
}

测试用例:




public class ComparatorGeneratorTest {

    @Test

    public void testGetComparator() {

        List<ConnBriefInfo> connBriefInfoList = Arrays.asList(
              new ConnBriefInfo(3L, "2021-06-11", "2021-05-12"),
                new ConnBriefInfo(5L, "2021-06-19", "2021-05-19"),
                new ConnBriefInfo(2L, "2021-06-15", "2021-05-15")
        );

        List<ConnBriefInfo> sortedByFirstTime = connBriefInfoList.stream()
                .sorted((Comparator<? super ConnBriefInfo>) ComparatorGenerator.getComparator(null, "firstTime"))
                .collect(Collectors.toList());

        System.out.println(sortedByFirstTime);

        List<ConnBriefInfo> sortedByVisitorCount = connBriefInfoList.stream()
                .sorted((Comparator<? super ConnBriefInfo>) ComparatorGenerator.getComparator(new Sort("visitCount")))
                .collect(Collectors.toList());

        System.out.println(sortedByVisitorCount);

        List<ConnBriefInfo> sortedByVisitorCountDesc = connBriefInfoList.stream()
                .sorted((Comparator<? super ConnBriefInfo>) ComparatorGenerator.getComparator(new Sort(Sort.Direction.DESC,"visitCount")))
                .collect(Collectors.toList());

        System.out.println(sortedByVisitorCountDesc);


        List<ConnBriefInfo> sortedByLastTime = connBriefInfoList.stream()
                .sorted((Comparator<? super ConnBriefInfo>) ComparatorGenerator.getComparator(new Sort(Sort.Direction.DESC,"lastTime")))
                .collect(Collectors.toList());

        System.out.println(sortedByLastTime);

    }

}



不过,这种方式仍然有限制:受限于指定的字段枚举。如果我需要使用其它对象的其它字段比较,就要定义新的接口及新的字段枚举;而且需要不安全的强制类型转换。

显然,这样并不够好。如何摆脱这种限制呢?


使用泛型来解除类型限制

前面的 getComparator 使用了 ComparatorObject 对象来表达要返回的可比较的对象类型。实际上,可以使用泛型来表达,解除类型限制。

稍作改动,定义字段获取函数 getComparator(Function<T, String> compFunc, boolean isAscending) ,这样就可以应对各种排序要求了。

不过,我们必须能够支持 Sort 传参,这样就需要把指定字段名转换为 Function<T, String> compFunc, 就有 convert 函数; 最后,我们还需要支持默认字段排序。但我不想在方法上增加一个参数。怎么办?可以在类里定义一个静态变量,反射获取该变量。

重构的代码如下。现在,我们不需要定义额外的东西了,而且根据任意对象类型的任意字段的任意方向排序 😃




/**
 * 差异点:
 * 1. 根据指定字段比较;
 * 2. 根据指定排序;
 * 3. 返回一个指定对象的比较器
 */

public class ComparatorGenerator2 {

    public static <T> Comparator<T> getComparator(Function<T, String> compFunc, boolean isAscending) {
        return isAscending ? (o1, o2) -> (StringUtils.compare(String.valueOf(compFunc.apply(o1)), String.valueOf(compFunc.apply(o2)))) :
                (o1, o2) -> (StringUtils.compare(String.valueOf(compFunc.apply(o2)), String.valueOf(compFunc.apply(o1))));
    }

    public static <T> Comparator<T> getComparator(Sort sort, Class<T> cls) {
        if (sort == null) {
            return getComparator(convert(cls, getDefaultField(cls)), true);
        }

        if (sort.iterator().hasNext()) {
            Sort.Order order = sort.iterator().next();
            boolean isAscending = order.getDirection().isAscending();
            String field = order.getProperty();
            return getComparator(convert(cls, field), isAscending);
        }
        return getComparator(convert(cls, getDefaultField(cls)), true);
    }

    public static <T> Function<T, String> convert(Class<T> cls, String field) {
        return o -> {
            try {
                return String.valueOf(FieldUtils.getDeclaredField(cls, field, true).get(o));
            } catch (IllegalAccessException e) {
                return "";
            }
        };
    }

    private static String getDefaultField(Class cls) {
        return Arrays.stream(cls.getFields())
                .filter(f -> "DEFAULT_SORT_FIELD".equals(f.getName()))
                .findFirst().toString();
    }
}

public class ConnBriefInfo implements ComparatorObject {
    public static final String DEFAULT_SORT_FIELD = "firstTime";
}

测试用例:


public class ComparatorGeneratorTest2 {

    @Test
    public void testGetComparator() {

        List<ConnBriefInfo> connBriefInfoList = Arrays.asList(
              new ConnBriefInfo(3L, "2021-06-11", "2021-05-12"),
                new ConnBriefInfo(5L, "2021-06-19", "2021-05-19"),
                new ConnBriefInfo(2L, "2021-06-15", "2021-05-15")
        );

        List<ConnBriefInfo> sortedByFirstTime = connBriefInfoList.stream()
                .sorted(ComparatorGenerator2.getComparator(ConnBriefInfo::getFirstTime, true))
                .collect(Collectors.toList());

        System.out.println(sortedByFirstTime);


        List<ConnBriefInfo> sortedByVisitorCount = connBriefInfoList.stream()
                .sorted( ComparatorGenerator2.getComparator(cb -> String.valueOf(cb.getVisitCount()), false))
                .collect(Collectors.toList());

        System.out.println(sortedByVisitorCount);

        List<ConnBriefInfo> sortedByVisitorCountDesc = connBriefInfoList.stream()
                .sorted(ComparatorGenerator2.getComparator(new Sort(Sort.Direction.DESC,"visitCount"), ConnBriefInfo.class))
                .collect(Collectors.toList());

        System.out.println(sortedByVisitorCountDesc);

        List<ConnBriefInfo> sortedByLastTime = connBriefInfoList.stream()
                .sorted(ComparatorGenerator2.getComparator(new Sort(Sort.Direction.DESC,"lastTime"), ConnBriefInfo.class))
                .collect(Collectors.toList());

        System.out.println(sortedByLastTime);
    }

}


修复引入的BUG

使用泛型之后,看上去代码已经很灵活了。不过,这里引入了一个 BUG。如果是整型比较的话,会先转换为字符串,再比较,就会有一个 BUG。 比如 “3” 比 “15” 更大。显然这是不符合预期的。这是因为 Function<T, String> compFunc 中的 String 不够灵活。 实际上,它应该是一个可比较的对象,比如 String 或 Long 或其它。 把它换成 Comparable 之后,就变成如下所示。这样,就更灵活了,也更能直接表达比较的语义。

客户端代码不需要动。因为我们只是做了一个里氏替换(所有引用基类的地方必须能透明地使用其子类的对象,见 SOLID 原则)。


    public static <T> Comparator<T> getComparator(Function<T, ? extends Comparable> compFunc, boolean isAscending) {
        return isAscending ? (o1, o2) -> compFunc.apply(o1).compareTo(compFunc.apply(o2)) :
                             (o1, o2) -> compFunc.apply(o2).compareTo(compFunc.apply(o1)) ;
    }

    public static <T> Function<T, ? extends Comparable> convert(Class<T> cls, String field) {
        return o -> {
            try {
                return (Comparable) FieldUtils.getDeclaredField(cls, field, true).get(o);
            } catch (IllegalAccessException e) {
                return null;
            }
        };
    }

经过 IDE 的提示, getComparator 可以写得更加简洁(只要一行,就可以对任意对象类型的任意字段的任意方向进行排序,你还怀疑函数式编程的强大能力吗?):


    public static <T> Comparator<T> getComparator(Function<T, ? extends Comparable> compFunc, boolean isAscending) {
        return isAscending ? Comparator.comparing(compFunc::apply) :  Comparator.comparing(compFunc::apply).reversed() ;
    }

    stream().sorted(ComparatorGenerator2.getComparator(param.toPageable().getSort(), ConnectionList.class))...;  // 对 ConnectionList 排序
    stream().sorted(ComparatorGenerator2.getComparator(param.toPageable().getSort(), ConnBriefInfo.class))...;   // 对 ConnBriefInfo 排序

至此,这一次重构就达到了目标。


小结

要编写可复用性更好的代码,基本指导思想就是分离和组合关注点。首先,善于从语义上分析和识别出各个维度的关注点(概念或事实);然后,建立关注点之间的交互;最后,将这些关注点及交互良好地组织起来。注意:我们始终首先从语义上去思考,然后才去找技术手段来支持这种语义的表达。

从代码技巧上来看:

  • 当需要针对不同字段做不同操作时,可以考虑函数来表达;

  • 当需要突破类型的束缚时,可以考虑泛型。

函数式 + 泛型,是一对强大的编程技巧组合,能够让代码表达能力异常灵活。


PS: 只要一行,就可以对任意对象类型的任意字段的任意方向进行排序,刹那间有一种触碰到“真理之光”的感觉 😃

posted @ 2021-06-11 22:51  琴水玉  阅读(440)  评论(2编辑  收藏  举报