版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/tonny_guan/article/details/86474194
设计模式是在特定场景下对特定问题的解决方案,这些解决方案是经过反复论证和测试总结出来的。实际上,除了软件设计,设计模式也被广泛应用于其他领域,比如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()) { processRow(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 processRow(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、绑定参数”。
代码第②行是在遍历结果集过程中调用抽象方法processRow(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 processRow(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 processRow(ResultSet rs) throws SQLException {} ① }; template.update(); }
插入数据的模板也是采用匿名内部类子类化JdbcTemplate,由于插入过程不需要遍历结果集,所以抽象方法processRow()采用空实现,见代码第①行。另外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 processRow(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 processRow(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、进入CSDN学院该课程
Design Patterns: Elements of Reusable Object-Oriented
(中文版《设计模式》)一书由Erich Gamma、Richard Helm、Ralph Johnson 和John Vlissides 合著(Addison-Wesley,1995),这四位作者常被称为“四人组”(Gang of
Four,GoF)。