分離和組合關注點。
引子
如下代碼所示:
@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: 只要一行,就可以對任意對象類型的任意字段的任意方向進行排序,剎那間有一種觸碰到“真理之光”的感覺 😃