以muppet为例利用模板方法模式增强异常信息的反馈
籍弘伟
2023-12-01
当你在定义方法时,应该保证此方法的正确性,但是你不能保证你的方法的调用者,他们的输入数据,或者
使用环境的正确性,当客户端代码调用此方法时,我们应该向调用方声明可能出现的异常,这样当出现
此种异常时,通过方法的文档,他们就清楚到底发生了什么,自己为什么错,(而不是不分青红皂白的埋怨他们调用的
代码的作者 “你们写的是 什么狗屁代码,怎么总报错" ,或者”强哥你的代码又有错误了",)合理的异常反馈是 代码库作者
,代码库调用者 之间信息交流的通道,或者说是,代码库作者推卸责任证明自己没有错的挡箭牌(当然你的代码明明有问题,却
故意向调用方 抛出输入数据异常, 就是你的职业道德问题了)
当你在开发初期,设计代码时,可能考虑不清楚可能出现什么异常(然后你没有定义,或者仅仅定义一两个),于是洋洋洒洒写
了几周后,发现我需要给某某个方法声明抛出异常,因为用户的输入,太离奇古怪,或者这段代码依赖的环境太不稳定了,需要调
用方捕获异常,并做异常处理,这时你抛出了新的异常,改了代码,发现项目中多了很多的编译错误,因为调用方并没有对异常做处理,这时你要
求你的调用方作者改代码?当然答案在大部分情况下是否定的。
以上两种情况仅仅是开发过程中最常见的情景中的两个,还有很多,总之合理的异常声明是 逻辑分层,代码分工,沟通交流的
有效保证。
我需要另外说明的是,当你的代码总是变动,总需要声明很多异常的时候,(三个异常就很多),你首先应该考虑是不是应该对这个
方法进行重构了(提取方法,或者提取类),因为频繁的变动代码很大意义上说明,这个方法变动的元素太多,应该分离,或者
把变化部分提取出去。这才是正确合理的解决方案。而不是向用户抛出一堆异常。
那么我们如何向用户正确反馈异常呢?
首先我们们可以从JDK的设计学会一些,JDK中所有的异常都继承自Exception(除了Throwable),当我们向上层(调用者)提供服务时,
,一个包应该是高内聚的,一个包下的类合作 完成一项大的任务,然后我们在这一项任务中抽象出来一个异常基类,
例如muppet其中一个包是 cn.bronzeware.muppet.sqlgenerate 这个包专门负责生成sql语句的,那么你就可以定义一个
异常基类 SqlGenerateException,那么以后这个包下的类,给包外调用方提供的核心方法抛出的异常都应该SqlGenerateException类型,我们
可以把这个类声明为抽象异常基类
public class SqlGenerateException extends Exception{
@Override
public final String getMessage() {
return "{Sql语句生成出错->"+Message();
}
public abstract String Message();
}
我们将getMessage方法设为final不能重写,同时其子类必须重写Message()方法,我们在getMessage方法中有一个
默认字符串叫做 “sql语句生成出错”,这个字符串可以放关于异常基类的描述,然后这个字符串在加上 子类的Message方法
利用模板方法设计模式,实现基类错误信息和子类错误信息的整合。
然后子类应该怎么写呢,
public class SelectSqlGenerateException extends SqlGenerateException{
public SelectSqlGenerateException(String message){
this.message = message;
}
public SelectSqlGenerateException(){
}
private String message = "";
public String message(){
return "Select语句生成出错->"+message+"}";
}
}
当在SelectGenerate生成Select语句时,如果出现关于生成语句的异常可以抛出
new SelectGenerateException("这里在进一步指出Select语句具体出现什么异常")
这样当SelectGenerate类的调用者在获取到SelectGenerateGenerate异常时
通过getMessage()返回的字符串 是"{Sql语句出错-> Select语句出错> Select语句定义错误,缺少逗号}" (举个例子)
那么这时,SelectGenerate的调用者其实获取的异常类是 SelectSqlGenerateException 类型,它可以通过这种手段
获得具体的异常处理
} catch (SqlGenerateException e) {
if(e instanceof SelectSqlGenerateException){
//做具体的处理
}else if/其他处理
}
如果它需要针对具体的异常类型做处理,就可以用上述类型检查做特殊处理,否则如果不需要做特殊处理,再向上抛出
当有另外一个包下的类或者其他层次(Sql语句层之上)的类 调用 SqlGenerate层(sql语句生成层)的服务时,他们本身
也有一个主要的功能任务,这个功能任务,也需要与之对应的异常信息抽象基类,这个基类同样这样设计
muppet中调用Sql语句层的包是cn.bronzeware.muppet.context ,是Context层,这个层负责数据操作,获取实体类信息(注解处理层),生成Sql操作 描述,生成sql语句,调用JDBC进行数据操作,封装结果集,主要是这几个功能,他的异常基类是ContextException()
和SqlGenerate同样的设计,作为抽象基类 有一个模板方法 message(),
其子类 SelectContextException同样这样设计
public class ContextException extends Exception{
private static final long serialVersionUID = -2272596789720418995L;
@Override
public final String getMessage() {
return "{数据操作Context层出错->"+Message();
}
public abstract String Message();
}
public class SelectContextException extends ContextException{
public SelectContextException(String message){
this.message = message;
}
public SelectContextException(){
}
private String message = "";
public String message(){
return "Select操作出错->"+message+"}";
}
}
当SelectContext中出现异常时,你可以抛出SelectContext异常,
throw new SelectContextException("具体的Select操作可能出现的异常");
此时可能出现什么情况,即SelectContext中出现的异常是因为SqlGenerate层出现了异常
这时你应该捕获SqlGenerateException异常,捕获到这个异常后,可以这样操作
} catch (SqlGenerateException e) {
throw new SelectContextException(e.getMessage());
}
应该是 "{数据操作Context层出错->Select操作出错->{Sql语句出错-> Select语句出错> Select语句定义错误,缺少逗号 }}"
这样上一层知道了原来SelectContext层出错是因为Sql语句生成的错误,同时又是因为Select语句定义错误,缺少逗号
这样的错误信息应该是很详细的,更详细的信息,需要你在合适的正确的地方,提供更详细的异常描述,然后做异常抛出,捕获,再抛出,再捕获等等
这样,在长长的一套语句的最后(可以调整在最前,读者可以想一想),总会有错误信息的最准确描述
当然你依然可以声明第四级异常子类,(Exceptinon ,ContextException,SelectContextException)但是我认为三级足够了,
首先,ContextException中有最基本的描述,SelectContextException中还会有描述,你还可以通过SelectContextException的构造方法
在传入更具体的错误信息,我觉得足够了,当然你仍然有需求的话,考虑下是不是有必要在划分一个包,或者再声明一个与之前的
包内的基类独立的另外一个包内基类。然后再做决定不迟。
muppet是一个封装JDBC的类库,支持对象关系映射,通过对象插入删除更新数据表,可以将查询结果映射为实体对象,同样支持跨表查询 基于注解,配置简单,相比原生JDBC性能以及开发效率都快很多,相比hibernate,mybatis使用简单,学习成本低,(当然功能远没他们
强大稳定)代码公开,支持工具类的二次开发 。svn: https://123.56.225.214/svn/muppet 用户名密码 均为muppet
于海强 2016.07.02