對於重覆的代碼,如果是重覆的字元串,我們會想到提出一個變數。如果是重覆的代碼塊,我們會想到提取出一個方法。 但如果這重覆的代碼塊中有一處或幾處是會變化的,那麼就沒那麼容易提取出一個方法。說起來有點抽象,下麵看一個例子。 一、分頁查詢 寫過CRUD的同學肯定寫過很多分頁查詢,分頁查詢的主要步驟是先校驗 ...
對於重覆的代碼,如果是重覆的字元串,我們會想到提出一個變數。如果是重覆的代碼塊,我們會想到提取出一個方法。
但如果這重覆的代碼塊中有一處或幾處是會變化的,那麼就沒那麼容易提取出一個方法。說起來有點抽象,下麵看一個例子。
一、分頁查詢
寫過CRUD的同學肯定寫過很多分頁查詢,分頁查詢的主要步驟是先校驗前端傳過來的查詢條件(包括哪一頁以及每頁顯示記錄數等),如果不合法則設置預設值。然後根據條件查詢符合條件的總記錄數,以及查詢符合條件的一頁記錄,最後經過處理返回給前端。
這一段話中會變的部分,也就是每個分頁查詢不同的部分,就是兩個查詢。這不就是一個模板,對於不同的分頁查詢,只要往其中填兩塊代碼。
不過有的同學可能會說平時用分頁插件,不需要寫查詢總數的方法,不過有時還是需要自己寫查詢總數的方法來優化SQL滴。
二、Java8之前的方式
下麵是一些核心類
/**
* 分頁查詢結果對象
*
* @param <T> 分頁查詢對象類型
*/
@AllArgsConstructor
@Getter
@ToString
public final class Page<T> implements Serializable {
/**
* 總記錄數
*/
@NonNull
private final Long total;
/**
* 當前記錄集合
*/
@NonNull
private final List<T> rows;
}
/**
* 分頁查詢模板(Java8之前的寫法)
*/
public abstract class AbstractPageTemplate<E> {
/**
* "pageNumber"
*/
public static final String PAGE_NUMBER = "pageNumber";
/**
* "pageSize"
*/
public static final String PAGE_SIZE = "pageBegin";
/**
* "pageBegin"
*/
private static final String PAGE_BEGIN = "pageBegin";
/**
* 獲取分頁結果對象
* <p>
* 示例:
*
* <pre>
* {@code
* Page<FooDTO> fooDTOPage = PageUtil.page(mapper::selectPageCount1, mapper::selectPageEntities1, paramMap)
* }
* </pre>
*
* @param paramMap 分頁查詢參數(key需要包含”pageNumber“和”pageSize“,否則預設查詢第一頁的20條記錄)
* @return 分頁結果對象集合
*/
public Page<E> page(Map<String, Object> paramMap) {
Objects.requireNonNull(paramMap);
// 獲取頁數
Integer pageNumber = (Integer) paramMap.get(PAGE_NUMBER);
// 校驗頁數,不合法則設置預設值
pageNumber = pageNumber == null || pageNumber <= 0 ? 1 : pageNumber;
// 獲取頁大小,不合法設置預設值
Integer pageSize = (Integer) paramMap.computeIfAbsent(PAGE_SIZE, k -> 20);
// 計算SQL中limit的offset(Mysql)
paramMap.put(PAGE_BEGIN, (pageNumber - 1) * pageSize);
// 查詢符合條件的總記錄數
long total = pageCount(paramMap);
if (total <= 0) {
return new Page<>(0L, new ArrayList<>());
} else {
// 查詢符合條件的一頁記錄
return new Page<>(total, pageList(paramMap));
}
}
/**
* 查詢符合條件的總記錄數
*
* @param paramMap 分頁查詢參數(key需要包含”pageNumber“和”pageSize“,否則預設查詢第一頁的20條記錄)
* @return 總記錄數
*/
abstract long pageCount(Map<String, Object> paramMap);
/**
* 查詢符合條件的所有記錄
*
* @param paramMap 分頁查詢參數(key需要包含”pageNumber“和”pageSize“,否則預設查詢第一頁的20條記錄)
* @return 分頁結果集合
*/
abstract List<E> pageList(Map<String, Object> paramMap);
}
下麵是一些為demo準備的類
@Data
public class User {
}
public class UserDAO {
public long pageCount(Map<String, Object> paramMap) {
// select count(*) from user where ...
return 0;
}
public List<User> pageList(Map<String, Object> paramMap) {
// select * from user where ... limit pageBegin, pageSize
return new ArrayList<>();
}
}
public class UserService extends AbstractPageTemplate<User> {
private UserDAO userDAO = new UserDAO();
@Override
public long pageCount(Map<String, Object> paramMap) {
return userDAO.pageCount(paramMap);
}
@Override
public List<User> pageList(Map<String, Object> paramMap) {
return userDAO.pageList(paramMap);
}
}
下麵是demo
UserService userService = new UserService();
Page<User> userPage = userService.page(ImmutableMap.of(
PageUtil.PAGE_NUMBER, 1,
PageUtil.PAGE_SIZE, 20
// 其他參數...
));
分析下上面這種傳統的模板方法模式,我們把樣板式的代碼寫到AbstractPageTemplate#page()
方法中,當我們要寫新的分頁查詢時,只要繼承AbstractPageTemplate
類,然後實現其中的兩個方法,就可以很方便的獲取到分頁結果。
如果沒有想到模板方法模式,項目中肯定會有大量的類似於AbstractPageTemplate#page()
方法中的代碼,而且每個人寫的可能會不一樣,如果後面要修改預設的每頁大小,要找到所有的這些分頁代碼不是很容易,難免會有遺漏。模板方法模式為後期的重構、擴展提供了便利。
這種傳統的方式寫起來有點麻煩,一個模塊有幾個分頁查詢就要寫幾個Service類,去繼承AbstractPageTemplate
類。一個模塊有多個service好像有點不合理,如果能把AbstractPageTemplate#pageCount
方法和AbstractPageTemplate#pageList
方法作為方法參數傳入AbstractPageTemplate#page()
方法中,那麼就方便多了,不用再去寫那麼多Service類了。還好Java8之後有了lambda表達式,我們就可以把方法作為方法的參數。
三、Java8的方式
下麵是核心類
/**
* 分頁查詢工具類
*/
public class PageUtil {
/**
* "pageNumber"
*/
public static final String PAGE_NUMBER = "pageNumber";
/**
* "pageSize"
*/
public static final String PAGE_SIZE = "pageSize";
/**
* "pageBegin"
*/
private static final String PAGE_BEGIN = "pageBegin";
private PageUtil() {
}
/**
* 獲取分頁結果對象
* <p>
* 示例:
*
* <pre>
* {@code
* Page<FooDTO> fooDTOPage = PageUtil.page(mapper::selectPageCount1, mapper::selectPageEntities1, paramMap)
* }
* </pre>
*
* @param pageCountFunction 查詢分頁總數的方法(參數類型:{@code Map<String, Object>};返回值類型:{@code int})
* @param pageQueryListFunction 查詢分頁記錄的方法(參數類型:{@code Map<String, Object>};;返回值類型:{@code List<E>})
* @param paramMap 分頁查詢參數(key需要包含”pageNumber“和”pageSize“,否則預設查詢第一頁的20條記錄)
* @return 分頁結果對象集合
*/
public static <E> Page<E> page(ToLongFunction<Map<String, Object>> pageCountFunction,
Function<Map<String, Object>, List<E>> pageQueryListFunction,
Map<String, Object> paramMap) {
Objects.requireNonNull(pageCountFunction);
Objects.requireNonNull(pageQueryListFunction);
Objects.requireNonNull(paramMap);
Integer pageNumber = (Integer) paramMap.get(PAGE_NUMBER);
pageNumber = pageNumber == null || pageNumber <= 0 ? 1 : pageNumber;
Integer pageSize = (Integer) paramMap.computeIfAbsent(PAGE_SIZE, k -> 20);
paramMap.put(PAGE_BEGIN, (pageNumber - 1) * pageSize);
long total = pageCountFunction.applyAsLong(paramMap);
if (total <= 0) {
return new Page<>(0L, new ArrayList<>());
} else {
return new Page<>(total, pageQueryListFunction.apply(paramMap));
}
}
}
下麵是demo
Page<User> userPage = PageUtil.page(userService::pageCount, userService::pageList, ImmutableMap.of(
PageUtil.PAGE_NUMBER, 1,
PageUtil.PAGE_SIZE, 20
// 其他參數...
));
分析下Java8以後的寫法,對於一個模塊有多個分頁查詢的情況,我們只要在Service中定義多個“查詢符合條件的總記錄數”和“查詢符合條件的所有記錄”的方法,然後在PageUtil#page
方法中傳入方法引用。
概括來說,Java8以後我們就可以把方法作為方法的參數傳入方法中,從而省去了寫很多類去繼承抽象的模板類的麻煩。
四、模板方法模式
模板方法模式是一個在我們平時寫代碼中經常用到的模式,可以幫我們少寫很多重覆的代碼,從而提高開發效率。在一些框架中也會經常看到,比如Spring的JdbcTemplate。GOF給模板方法模式下過以下定義:
定義一個操作中的演算法骨架,而將一些步驟遲到子類中。模板方法使得子類可以不改變一個演算法的結構即可重定義該演算法的某些特定步驟。
如果你不明白這個定義也無所謂,只要你看懂了上面那個Java8的例子就可以了。在我看來,模板方法模式就是把方法傳入方法中,有了lambda,就是把把方法作為方法參數傳入方法中。
回到本系列的目錄