模板方法設計模式在JDBC中的應用

設計模式是在特定場景下對特定問題的解決方案,這些解決方案是經過反覆論證和測試總結出來的。實際上,除了軟件設計,設計模式也被廣泛應用於其他領域,比如UI設計和建築設計等。Java軟件設計模式大都來源於GoF的23種設計模式。

這段時間一直在錄製Java EE視頻課程,其中在JDBC(Java數據庫連接)中使用了模板方法設計(Template Method),下面給大家分享一下。

1. 什麼是模板方法設計模式?

在生活中完成一些“任務”有着固定的步驟,例如,我要完成“喝茶”任務,需要的步驟如下:
①燒水→②沏茶→③喝茶
而很多任務也有類似的步驟,例如,我要完成“喝咖啡”任務,當然是速溶咖啡那種。需要的步驟如下:
①燒水→②衝咖啡→③喝咖啡
對應兩個任務他們有類似的3個步驟,步驟①和③是相同的,而步驟②是不同的。這樣可以設計一個父類TaskTemplate代碼如下:

public abstract class TaskTemplate {

    public final void 任務() {
        // 步驟①
        燒水();
        // 步驟②
        沖泡();
        // 步驟③
        喝();
    }

    private void 燒水() {
        System.out.println("燒水...");
    }

    protected abstract void 沖泡();

    private void 喝() {
        System.out.println("喝...");
    }

}

TaskTemplate是一個抽象類,其中“任務()”方法中定義了執行“任務”的流程,其中“燒水()”和“喝()”是兩個具體方法,由於父類中無法確定沖泡什麼,因此“沖泡()”方法是抽象方法,留給子類實現。“任務()”就是模板方法。
“喝茶”任務實現類TeaTask代碼如下:

public class TeaTask extends TaskTemplate {

    @Override
    protected void 沖泡() {
        System.out.println("來壺鐵觀音。");
    }
}

“喝咖啡”任務實現類CoffeeTask代碼如下:

public class CoffeeTask extends TaskTemplate {

    @Override
    protected void 沖泡() {
        System.out.println("衝卡布奇諾咖啡+糖+奶。");
    }
}

他們的類圖如圖1所示。
圖1 類圖
這就是模板方法設計模式了,那麼如何使用呢?示例代碼如下:

public class Main {

    public static void main(String[] args) {

        System.out.println("------喝茶任務------");
        TaskTemplate template = new TeaTask();
        template.任務();

        System.out.println("------喝咖啡任務------");
        template = new CoffeeTask();
        template.任務();
    }
}

輸出結果如下:

------喝茶任務------
燒水...
來壺鐵觀音。
喝...
------喝咖啡任務------
燒水...
衝卡布奇諾咖啡+糖+奶。
喝...

上述代碼模板子類是有名類,而有時候子類個數太多,也可以採用匿名內部類作爲模板子類。修改Main調用代碼如下:

public class Main {

    public static void main(String[] args) {

        System.out.println("------喝茶任務------");
        TaskTemplate template = new TaskTemplate() { ①
            @Override
            protected void 沖泡() {
                System.out.println("來壺鐵觀音。");
            }
        };
        template.任務();

        System.out.println("------喝咖啡任務------");
        template = new TaskTemplate() {  ②
            @Override
            protected void 沖泡() {
                System.out.println("衝卡布奇諾咖啡+糖+奶。");
            }
        };
        template.任務();

    }
}

上述代碼第①行是實現了喝茶任務子類功能,代碼第②行是實現了喝咖啡任務子類功能。

2. 糟糕的JDBC代碼

上面的介紹的設計模式或許很容易理解,但是又有什麼用途呢?使用設計模式是學習的難點。下面先來看看糟糕的JDBC代碼:

public class Main {

    public static void main(String[] args) {

        //查詢數據
        read();
        //數據插入
        create();
        //數據更新
        update();
        //刪除數據
        delete();

    }

    /**
     * 查數據
     */
    private static void read() {

        // 載數據庫驅動
        loadDBDriver();

        String sql = "select name, userid from user where userid > ? order by userid";

        Connection connection = null;
        PreparedStatement ps = null;
        try {
            // 創建數據庫連接
            connection = getConnection();

            // 創建語句對象
            ps = connection.prepareStatement(sql);

            // 綁定參數
            ps.setInt(1, 0);
            ResultSet rs = ps.executeQuery();

            //遍歷結果集
            while (rs.next()) {
                System.out.printf("name: %s     id: %d \n",
                        rs.getString("name"),
                        rs.getInt("userid"));
            }

        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (ps != null) {
                try {
                    ps.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 插入數據
     */
    private static void create() {

        // 載數據庫驅動
        loadDBDriver();

        String sql = "insert into user (userid, name) values (?, ?)";

        Connection connection = null;
        PreparedStatement ps = null;

        try {
            // 建數據庫連接
            connection = getConnection();

            // 創建語句對象
            ps = connection.prepareStatement(sql);

            // 綁定參數
            ps.setInt(1, 999);
            ps.setString(2, "Tony999");
            // 執行SQL語句
            int count = ps.executeUpdate();

            System.out.printf("成功插入%d條數據.\n", count);

        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (ps != null) {
                try {
                    ps.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 更新數據
     */
    private static void update() {

        // 載數據庫驅動
        loadDBDriver();

        String sql = "update user set name=? where userid =?";

        Connection connection = null;
        PreparedStatement ps = null;

        try {
            // 創建數據庫連接
            connection = getConnection();

            // 創建語句對象
            ps = connection.prepareStatement(sql);

            // 綁定參數
            ps.setString(1, "Tom999");
            ps.setInt(2, 999);
            // 執行SQL語句
            int count = ps.executeUpdate();

            System.out.printf("成功更新%d條數據.\n", count);

        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (ps != null) {
                try {
                    ps.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }

    }

    /**
     * 刪除數據
     */
    private static void delete() {

        // 載數據庫驅動
        loadDBDriver();

        String sql = "delete from user where userid = ?";

        Connection connection = null;
        PreparedStatement ps = null;

        try {
            // 創建數據庫連接
            connection = getConnection();

            // 創建語句對象
            ps = connection.prepareStatement(sql);

            // 綁定參數
            ps.setInt(1, 999);
            // 執行SQL語句
            int count = ps.executeUpdate();

            System.out.printf("成功刪除%d條數據.\n", count);

        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (ps != null) {
                try {
                    ps.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 建立數據庫連接
     *
     * @return 返回數據庫連接對象
     * @throws SQLException
     */
    private static Connection getConnection() throws SQLException {

        String url = "jdbc:mysql://localhost:3306/mydb?verifyServerCertificate=false&useSSL=false";
        String user = "root";
        String password = "12345";

        Connection connection = DriverManager.getConnection(url, user, password);
        return connection;
    }

    /**
     * 加載數據庫驅動
     */
    private static void loadDBDriver() {
        // 1.
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

}

上述代碼中訪問數據的方法有4個read()、create()、update()和delete()。其中create()、update()和delete()三個方法代碼非常相似,只是SQL語句和綁定參數不同而已。雖然read()方法與create()、update()和delete()方法不同,但是差別也不大。
JDBC代碼主要的問題是:大量的重複代碼!!!

3. 在JDBC中使用模板設計方法模式

從上一節代碼總結數據庫編程一般過程,如圖2所示。
圖2 數據庫編程一般過程
從圖3中可見查詢(Read)過程最多需要7個步驟。修改(C插入、U更新、D刪除)過程最多需要6個步驟。其中有些步驟是不變的,而有些步驟是可變的。如圖3所示,查詢過程中1、2、5和7步是不可變的所有查詢都是一樣的,而3、4和6步不同,第3步在“創建語句對象”時需要指定SQL語句,這是“此查詢”與“彼查詢”的不同之處;由於SQL語句的不同綁定參數也可能不同,所以第4步也是不同的;另外,第6步是“遍歷結果集”也會根據查詢的不同字段,以及字段提取後處理的方式不同而有所不同。
圖3  查詢過程

使用代碼模板方法模式,可以將1、2、5和7步定義在父類在,將3、4和6步定義在子類中。代碼如下:

public abstract class JdbcTemplate {

  public final void query() {

    // 1、載數據庫驅動
    loadDBDriver();

    Connection connection = null;
    PreparedStatement ps = null;
    try {
        // 2、創建數據庫連接
        connection = getConnection();

        // 3、創建語句對象 4、綁定參數
        ps = createPreparedStatement(connection);

        // 5、執行查詢
        ResultSet rs = ps.executeQuery();

        // 6、遍歷結果集
        while (rs.next()) {
            proce***ow(rs);
        }

    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        // 7、釋放資源
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
  }

    /**
     * 遍歷結果集時,處理結果集
     * @param rs 結果集
     * @throws SQLException
     */
    public abstract void proce***ow(ResultSet rs) throws SQLException; ③

    /**
     * 創建語句對象,其中包括指定SQL語句,綁定參數。
     * @param conn 連接對象
     * @return 語句對象
     * @throws SQLException
     */
    public abstract PreparedStatement 
            createPreparedStatement(Connection conn)  throws SQLException; ④

    /**
     * 建立數據庫連接
     *
     * @return 返回數據庫連接對象
     * @throws SQLException
     */
    private static Connection getConnection() throws SQLException {

        String url = "jdbc:mysql://localhost:3306/mydb?verifyServerCertificate=false&useSSL=false";
        String user = "root";
        String password = "12345";

        Connection connection = DriverManager.getConnection(url, user, password);
        return connection;
    }

    /**
     * 加載數據庫驅動
     */
    private static void loadDBDriver() {
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

在查詢方法中代碼第①行調用抽象方法createPreparedStatement(connection)創建預處理的語句對象,事實上在創建語句對象時,還可以爲其綁定參數,所以代碼第①行調用createPreparedStatement(connection)過程中實現“3、創建語句對象”和“4、綁定參數”。
代碼第②行是在遍歷結果集過程中調用抽象方法proce***ow(rs)處理結果集。一般而言所有遍歷結果集都是while (rs.next()) {…}循環語句實現的,只是提取的字段不同,提取之後的處理過程不同。
那麼調用read()方法代碼如下:

/**
 * 查數據
 */
private static void read() {

    String sql = "select name, userid from user where userid > ? order by userid";

    JdbcTemplate template = new JdbcTemplate() {  ①

        @Override
        public PreparedStatement createPreparedStatement(Connection conn) throws SQLException {
            // 綁定參數
            PreparedStatement ps = conn.prepareStatement(sql);
            // 綁定參數
            ps.setInt(1, 0);

            return ps;
        }

        @Override
        public void proce***ow(ResultSet rs) throws SQLException {
            System.out.printf("name: %s     id: %d \n",
                    rs.getString("name"),
                    rs.getInt("userid"));
        }

    }; ②

    template.query(); ③

}

上述代碼第①行~第②行採用匿名內部類子類化JdbcTemplate類,並且實例化它,而沒有采用有名類子類化JdbcTemplate類,這是因爲每一次查詢都需要一個JdbcTemplate子類,以及該子類的實例。這樣會需要創建很多個JdbcTemplate子類。代碼第③行調用模板方法query()執行查詢。

圖4所示是修改過程,其中1、2、5和6步是不可變的所有修改(插入、刪除和更新)都是一樣的,而3和4步是不同的。
圖4  JDBC修改過程

使用代碼模板方法模式,可以將1、2、5和7步定義在父類在,將3、4和6步定義在子類中。代碼如下:

public abstract class JdbcTemplate {

    public final void update() {

        // 1、載數據庫驅動
        loadDBDriver();

        Connection connection = null;
        PreparedStatement ps = null;
        try {
            // 2、創建數據庫連接
            connection = getConnection();

            // 3、創建語句對象 4、綁定參數
            ps = createPreparedStatement(connection); ①

            // 5、執行SQL語句
            int count = ps.executeUpdate();

            System.out.printf("成功修改%d條數據.\n", count);

        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 6、釋放資源
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (ps != null) {
                try {
                    ps.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

代碼第①行的createPreparedStatement(connection)方法與查詢時共用該方法,當子類實現該方法時創建預編譯語句對象和綁定參數。

那麼調用create()方法的代碼如下:

/**
 * 插入數據
 */
private static void create() {

    String sql = "insert into user (userid, name) values (?, ?)";
    JdbcTemplate template = new JdbcTemplate() {

        @Override
        public PreparedStatement createPreparedStatement(Connection conn) throws SQLException {
            // 綁定參數
            PreparedStatement ps = conn.prepareStatement(sql);
            // 綁定參數
            ps.setInt(1, 999);
            ps.setString(2, "Tony999");

            return ps;
        }

        @Override
        public void proce***ow(ResultSet rs) throws SQLException {}  ①

    };

    template.update();
}

插入數據的模板也是採用匿名內部類子類化JdbcTemplate,由於插入過程不需要遍歷結果集,所以抽象方法proce***ow()採用空實現,見代碼第①行。另外update()也是模板方法。
更新數據和刪除數據方法與插入數據方法是類似的,代碼如下:

/**
 * 更新數據
 */
private static void update() {

    String sql = "update user set name=? where userid =?";
    JdbcTemplate template = new JdbcTemplate() {

        @Override
        public PreparedStatement createPreparedStatement(Connection conn) throws SQLException {
            // 綁定參數
            PreparedStatement ps = conn.prepareStatement(sql);
            // 綁定參數
            ps.setString(1, "Tom999");
            ps.setInt(2, 999);

            return ps;
        }

        @Override
        public void proce***ow(ResultSet rs) throws SQLException {
        }

    };

    template.update();

}

/**
 * 刪除數據
 */
private static void delete() {

    String sql = "delete from user where userid = ?";
    JdbcTemplate template = new JdbcTemplate() {

        @Override
        public PreparedStatement createPreparedStatement(Connection conn) throws SQLException {
            // 綁定參數
            PreparedStatement ps = conn.prepareStatement(sql);
            // 綁定參數
            ps.setInt(1, 999);

            return ps;
        }

        @Override
        public void proce***ow(ResultSet rs) throws SQLException {
        }

    };

    template.update();
}

讀者可以比較一下,採用了模板設計方法後是不是代碼變得很簡單了呢!

4. 後記

JDBC模板子類不要採用有名子類化JDBC模板父類,這會使我們爲每一個查詢和修改操作而編寫一個子類,這個數量會很多。
再有,從上面的代碼可見,模板設計方法還是可以進行優化的。事實上還可以更加抽象一下,即採用接口替代兩個抽象方法,這樣會更加靈活,而且可以使用Lambda表達式替代內部類。這種方式就Spring框架的實現Jdbc模板的實現方法,感興趣的同學可以看看Spring的源代碼。另外,可以通過關東昇老師《Java Web從入門到實戰》視頻課程第5章JDBC技術瞭解具體細節。

代碼下載地址:https://github.com/tonyguan/JdbcTemplate

《Java Web從入門到實戰》視頻課程:
1、進入51CTO學院該課程

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章