作一個不復制粘貼的程序員[1]: 使用模板方法模式(1)- 分頁查詢實例

對於重複的代碼,若是是重複的字符串,咱們會想到提出一個變量。若是是重複的代碼塊,咱們會想到提取出一個方法。html

但若是這重複的代碼塊中有一處或幾處是會變化的,那麼就沒那麼容易提取出一個方法。提及來有點抽象,下面看一個例子。前端

1、分頁查詢

寫過CRUD的同窗確定寫過不少分頁查詢,分頁查詢的主要步驟是先校驗前端傳過來的查詢條件(包括哪一頁以及每頁顯示記錄數等),若是不合法則設置默認值。而後根據條件查詢符合條件的總記錄數,以及查詢符合條件的一頁記錄,最後通過處理返回給前端。java

這一段話中會變的部分,也就是每一個分頁查詢不一樣的部分,就是兩個查詢。這不就是一個模板,對於不一樣的分頁查詢,只要往其中填兩塊代碼。算法

不過有的同窗可能會說平時用分頁插件,不須要寫查詢總數的方法,不過有時仍是須要本身寫查詢總數的方法來優化SQL滴。sql

2、Java8以前的方式

下面是一些核心類app

/**
 * 分頁查詢結果對象
 *
 * @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);
    }
}

下面是demoide

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表達式,咱們就能夠把方法做爲方法的參數。

3、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之後咱們就能夠把方法做爲方法的參數傳入方法中,從而省去了寫不少類去繼承抽象的模板類的麻煩。

4、模板方法模式

模板方法模式是一個在咱們平時寫代碼中常常用到的模式,能夠幫咱們少寫不少重複的代碼,從而提升開發效率。在一些框架中也會常常看到,好比Spring的JdbcTemplate。GOF給模板方法模式下過如下定義:

定義一個操做中的算法骨架,而將一些步驟遲到子類中。模板方法使得子類能夠不改變一個算法的結構便可重定義該算法的某些特定步驟。

若是你不明白這個定義也無所謂,只要你看懂了上面那個Java8的例子就能夠了。在我看來,模板方法模式就是把方法傳入方法中,有了lambda,就是把把方法做爲方法參數傳入方法中


回到本系列的目錄

相關文章
相關標籤/搜索