設計模式是在特定場景下對特定問題的解決方案,這些解決方案是經過反覆論證和測試總結出來的。實際上,除了軟件設計,設計模式也被廣泛應用於其他領域,比如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所示。
這就是模板方法設計模式了,那麼如何使用呢?示例代碼如下:
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所示。
從圖3中可見查詢(Read)過程最多需要7個步驟。修改(C插入、U更新、D刪除)過程最多需要6個步驟。其中有些步驟是不變的,而有些步驟是可變的。如圖3所示,查詢過程中1、2、5和7步是不可變的所有查詢都是一樣的,而3、4和6步不同,第3步在“創建語句對象”時需要指定SQL語句,這是“此查詢”與“彼查詢”的不同之處;由於SQL語句的不同綁定參數也可能不同,所以第4步也是不同的;另外,第6步是“遍歷結果集”也會根據查詢的不同字段,以及字段提取後處理的方式不同而有所不同。
使用代碼模板方法模式,可以將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步是不同的。
使用代碼模板方法模式,可以將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學院該課程