1.繁琐的数据库操作
不知道大家发现没有,无论什么语言,数据库操作永远是最繁琐的部分
//首先要获取conn
Connection connection=null;
Statement statement=null;
try{
connection=pool.getConnection();
statement=connection.createStatement();
ResultSet rs=statement.executeQuery("select * from xxx");
while(rs.next()){
...;
}
}catch(SQLException e){
logger.exception(e);
}finally{
if(statement!=null){
try{
statement.close();
}catch(SQLException e){
}
}
if(connection!=null){
try{
connection.close();
}catch(SQLException e){
}
}
这段代码真正核心部分其实就是对查询出来的ResultSet
进行操作而已,但是为什么要有这么多跟业务无关但是又不得不写的代码!我们可以不可以把具体的业务抽象出来,每次只需要写业务代码,而其他声明与异常捕获都统一放到一个函数里面去。
2.Template模式
在实际的代码编写过程中,我们经常会遇到一下很多大部分重复但是少部分又有区别的代码
public Data functionA(){
Data x;
流程1 //对变量x进行了一些操作
中间流程A//再次对变量x进行了一些操作
流程2//最后对变量x进行了一些操作
return x;
}
public Data functionB(){
Data x;
流程1 //对变量x进行了一些操作
中间流程B//再次对变量x进行了一些操作
流程2//最后对变量x进行了一些操作
return x;
}
上面两个函数大部分流程都是相同的,只有中间流程不同,而且中间流程与前后流程的变量都进行了一些操作。所以我们可以把中间流程单独剥离出来作为一个参数传进去
public Data commonFunction(Consumer<Data> function){
Data x;
流程1 //对变量x进行了一些操作
function.accept(x)//将中间流程作为一个参数传了进来
流程2//最后对变量x进行了一些操作
return x;
}
public Data functionA(){
return commonFunction(x->{
中间流程A//对变量x进行了一些操作
});
}
public Data functionB(){
return commonFunction(x->{
中间流程B//对变量x进行了一些操作
});
}
这样我们在写具体的代码的时候只需要关注真的中间流程而无需关注其他的通用的流程。
3. 数据库操作的简化与封装
- 我们平常使用数据库都是增删改查四个操作,其中增删改三个操作只需要执行SQL语句,而查操作需要返回ResultSet,因此我们可以抽象出两个函数
query()
和execute()
。- 无论是
query()
还是execute()
,操作流程都是类似的,声明一个Statement,使用Statement执行SQL语句,然后进行一些操作,最后进行Statement资源释放,所以我们可以抽象一个useStatement()
方法来负责Statement的创建与释放- 我们大部分查询操作都是通过statement查询获取一个ResultSet,然后对这个ResultSet一行一行进行操作,这个流程即是对ResultSet进行遍历,所以我们可以抽象出一个
mapResultSet()
函数。
public class DbHelper{
private Connection connection;
public DbHelper(Connection connection){
this.connectio=connection;
}
//使用statement,把对statement的具体操作封装成function作为参数传进来
public synchronized <T> T usePreparedStatement(String sql,Object[] params,SQLFunction<PreparedStatement,T> function) throws SQLExcption{
PreparedStatement statement=null;
try{
statement=connection.createPreparedStatement(sql);
for (int i = 0; i < params.length; i++) {
statement.setObject(i + 1, params[i]);
}
return function.apply(statement);
}finally{
try{
statement.close();
}catch(SQLException e){}
}
}
//遍历ResultSet,把对每行ResultSet的操作封装成Consumuer作为参数传进来
public int mapResultSet(String sql,Object[] params,SQLConsumer<ResultSet> consumer)throws SQLException{
return usePreparedStatement(sql,params,statement->{
ResultSet rs=statement.executeQuery();
int count=0;
while(rs.next()){
count++;
consumer.accept(rs);
}
return count;
});
}
//执行某条SQL语句
public void execute(String sql,Object[]params){
usePreparedStatement(sql,parames,statement->{
statement.executeUpdate();
return null;
});
}
}
上面的SQLFunction
和SQLConsumer
是自己写的一个函数接口,与Java自带的函数接口Function
和Consumer
唯一的差别就是能抛出SQLException,这样能保证异常被抛出并且能在合适的地方进行处理
@FunctionalInterface
public interface SQLFunction<P, R> {
R apply(P p) throws SQLException;
}
@FunctionalInterface
public interface SQLConsumer<P> {
void accept(P p) throws SQLException;
}
假如我们需要查询USER表中id大于100的数据,并生成指定的User类集合可以进行如下操作
public List<User> queryUserList(DbHelper dbHelper)throws SQLException{
List<User> list=new LinkedList<>();
dbHelper.mapResultSet("select id,name,password from user where id > ?;",
new Object[]{100},
rs->{//每一行ResultSet都生成一个User添加到list中
User user=new User();
user.setId(rs.getInt("id"));
user.setName(rs.getString("name"));
user.setPassword(rs.getString("password"));
list.add(user);
});
return list;
}
在上述代码中,我们只需要把焦点放在创建User并添加到集合的流程中,完全无需关注数据库资源的创建、释放等额外繁琐操作。
我们再举一个简单的例子
//数据库连接池接口
public interface DatabasePool{
void getConnection() throws SQLExcption;
}
上面是一个简单的数据库连接池接口,我们在使用数据库连接池的要注意每次获取一个连接使用完一定要将其释放,否则会产生资源泄露的情况
Connection conn=null
try{
conn=pool.getConnection();
//接下来对conn进行一些操作
}catch(SQLException e){
logger.excption(e);
}fially{
if(conn!=null){
try{
conn.close();
}catch(SQLException e){}
}
}
在上述代码中,我们真正关注的在于注释部分对conn的操作。但是我们不得不写很多其他的冗余代码对conn的获取与释放进行管理。我们可以Template模式进行简化代码编写
public interface DatabasePool{
Connection getConnection() throws SQLException;
//Java8之后接口也可以有default函数了
//useConnection的内容跟getConnection的具体实现无关,所以可以直接放到接口里面来
default void useConnectin(SQLConsumer<Connection> consumer)throws SQLException{
Connection conn=null
try{
conn=pool.getConnection();
consumer.accept(conn)//把接下来对conn进行的操作封装成一个函数
}catch(SQLException e){
logger.excption(e);
}fially{
if(conn!=null){
try{conn.close();}catch(SQLException e){}
}
}
}
}
我们来看看新的接口如何进行使用
pool.useConnection(conn->{
//接下来对conn进行一些操作
});
我们使用新的接口已经完全可以直接使用conn而无需在意conn的获取与释放了,看上去是不是很简洁?
4. Template模式的缺陷
虽然Template模式用起来很简单也很方便,但是仍然有一些缺陷
像最后一个例子中的useConnection()
函数,虽然简化了Connection的创建与获取,由于函数抛出的异常已经在代码中写死了(throws SQLException
),所以在使用useConnection()
对conn进行操作的过程中也只能抛出SQLException。
//之前的函数调用,可以抛出OtherException
public void functionA() throws OtherException,SQLException{
Connection conn=null
try{
conn=pool.getConnection();
//接下来对conn进行一些操作
throw new OtherException();//抛出其他异常
}catch(SQLException e){
logger.excption(e);
}fially{
if(conn!=null){
try{conn.close();}catch(SQLException e){}
}
}
}
//Template函数调用,不可以抛出OtherException
public void functionB() throws SQLException{
pool.useConnection(conn->{
//接下来对conn进行一些操作
throw new OtherException();//会编译错误,因为SQLFunction里面只允许抛出SQLException
});
}
这意味这原本在conn操作流程中可以抛出的OtherException在Template模式中无法抛出了。
解决办法有两个
方法一: 流程封装函数Consumer中声明抛出所有异常,这样子能保证所有异常都可以抛出
//更改useConnection抛出的异常为Exception
public interface DatabasePool{
Connection getConnection() throws SQLException;
default void useConnection(MyConsumer<Connection> consumer)throw Exception{
//省略
}
}
//更改Consumer抛出的异常为Exception
public interface MyConsumer<T>{
void accept(T t)throws Exception;
}
public void function()throws Exception{
pool.useConnection(conn->{
//对conn进行一些操作
throw new OtherException();//这下子可以正常通过编译了。缺陷就是抛出的异常都变成基本异常Exception,对于上层代码不好区分和识别
});
}
这种方式就是把所有异常都转成异常的基本类型Exception,这样子异常可以正常抛出,但是其他方法调用此函数的,无法清晰识别到底抛出的是什么异常
方法二:把要抛出的OtherException转成SQLExption抛出
public void function()throws SQLException{
pool.useConnection(conn->{
//对conn进行一些操作
throw new SQLException(new OtherException())//将OtherExcpeiton用SQLException包裹起来
});
}
这个方法相比第一方法要好,既能正确抛出代码,又能让调用者知道异常是SQLException,但是OtherException在代码编写阶段对于外面的人同样是无法识别的。
5. Template模式与模板模式
其实这个模式具体跟设计模式中的模板模式类似,更确切的应该是模板模式的一种简化。
public abstract class Template{
public Data template(){//把主要流程写在抽象类的模板方法中,
Data x;
流程1 //对变量x进行了一些操作
function(x);//具体的中间流程通过继承此类实现function来实现,对x进行了一些操作
流程2 //对变量x进行了一些操作
return x;
}
protected abstract void function(Data x);
}
class A extends Template{
public void function(Data x){
中间流程A //对变量x进行了一些操作
}
}
class B extends Template{
public void function(Data x){
中间流程B //对变量x进行了一些操作
}
}
A.template();//调用的是A的全部流程
B.template();//调用的是B的全部流程
但是模板模式需要新建类并且继承模板类,这样增加了类的容量而且也没有那么整洁,更何况Java8已经拥有了函数式编程,使用Template模式更加方便而且简洁!