一次重复代码重构的思考及探索
分离和组合关注点。
引子
如下代码所示:
@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: 只要一行,就可以对任意对象类型的任意字段的任意方向进行排序,刹那间有一种触碰到“真理之光”的感觉 😃