在JavaWeb的后台开发中,习惯上会使用三层架构,即视图层、业务逻辑层和持久层作为默认的架构进行开发,实际上关于三层架构一直有一个让我困惑的地方,那就是在三层之间传输的数据。
三层之间实际上只要关注业务逻辑层和持久层的返回值就可以了,因为视图层的返回值是框架决定的,而业务逻辑层和持久层的返回值则需要完全自己设计。
我一开始接触JavaWeb是看当时工作室的师兄们写的代码,那个时候师兄们的业务逻辑层和持久层的返回值都是比较直接的思路,例如,一个返回多条记录的dao方法的返回值就是List,返回一条记录的dao方法的返回值就是那个实体类,返回类似select count(*)这种方法的返回值就是int,而增删改可能就是void或者boolean了,如下图:
public interface ITestDao {
List<?> getAll();
Entity get();
int getCount();
boolean save();
boolean update();
boolean delete();
}
后来我自己觉得,这个返回值怎么可以只想着成功时的状态呢,应该考虑到失败时的情况,如果失败的时候就应该把失败的这个状态和失败的原因返回给上层,但是上述的接口根本不足以用来存储这么多样的信息。
当时师兄的dao层感觉也不算写的很好,他们喜欢在getAll里面直接做分页的一些计算,所以返回的不仅仅是一个List,而应该是一个Map,里面包含了结果的List还有分页的一些信息(例如总页数啊、当前页数等)。
那时候的我灵光一闪,诶这个Map不就是一个超棒的东西吗,无论是什么都能往里面塞,我只要成功的时候把success这个状态和获取到的数据put进去,失败的时候把fail这个状态和失败原因put进去,然后把整个Map返回不就可以了?于是当时我的dao层就改成了类似下面这个样子:
public class TestDao {
public Map<String,Object> getAll(){
Map<String,Object> theResult=new HashMap<>();
try {
List<?> list = null;//假设这里从数据库查到数据
theResult.put("status", "success");
theResult.put("result", list);
}catch (Exception e) {
theResult.put("status", "fail");
theResult.put("errInfo", e.getMessage());
}
return theResult;
}
}
Map这种能伸能缩的数据结构深深地吸引了当时的我,这真是太无敌了,然后我的接口(无论是service层还是dao层)就都变成了这样:
public interface ITestDao {
Map<String,Object> getAll();
Map<String,Object> get();
Map<String,Object> getCount();
Map<String,Object> save();
Map<String,Object> update();
Map<String,Object> delete();
}
这key和value的选择上我喜欢用String当key,用Object当value。因为String的key的可读性高,如果用int的话上一层就不知道这个值是什么意思了;Object的value是考虑到value有可能是多种东西,在getAll()中,它就是List,在get()中,它就是Entity,依此类推。
使用这样的容器作为层与层之间的数据传输载体貌似已经可以满足任何情况下的需求了,实际上在很长的一段时间内,我的接口都是这样定义的,也并没有出现什么异常情况,就是从Map中取值出来时需要强制转换这一点上有点小小的不舒服。
后来,我读了一本关于struts2的代码研读类书籍,这本书给我的启发很大,而且其中的内容个人感觉也十分地有趣,其中这本书还谈到了关于“数据载体”在MVC框架中的发展的话题,这里简单地提一下书上关于这个话题的内容。
这本书认为从最原始的jsp/servlet,到struts1,再到struts2里,controller层用来接收数据的数据载体在不停地进化,总共分为三个阶段:
1、Map载体。最经典的例子就是jsp/servlet中的session、request和response对象,这些对象可以自由地put任意的key和value。它们实现简单、学习成本低,而且很实用。但是问题是在类型检查、代码可读性上存在较严重的问题。
2、FormBean载体。FormBean是struts1提出来的一个概念,实际上就是一个类就对应了html中的一个form,类里面的一个属性对应form里面的一个input,FormBean需要提供setter给struts1去注入input的值。FormBean实际上就是为了解决Map载体的类型检查和代码可读性的问题,假设FormBean的一个属性是int,而前端却传来了一个String,那么此时struts1的注入就会报错,从而实现类型检查,但这在Map载体中是不会报错的。但是FormBean也有自己的问题:它必须继承struts1的一个叫ActionForm的类,这就说明它对代码的污染过大,耦合度高,这也是那个时候的开发者对struts1诟病的原因。
3、POJO载体。struts2学聪明了,这次不用FormBean来接收数据了,只要是一个POJO就可以接收数据了,使用POJO就既能解决Map的问题,又能解决FormBean的问题。
当然,现在struts2感觉又要被spring mvc干掉了,不过这也是后话,我们这里只需要关注这些数据载体的变化历史即可。
看完这一节的我实际上在“service层和dao层的数据载体”这一点上还没有得到什么启发,我是过了一段时间才突然意识到,这段知识点是对我重构代码有用的。
我当时极力鼓吹的Map实际上不就是书上所说的Map载体吗?过于自由的key/value使得Map载体丧失了类型安全的特性,那么我也可以按照这个历史进程来重构我的代码。那时候的我才知道,原来所谓的以史为鉴是这个意思。
按照书上的思路,Map载体应该是不行了,POJO才是最后进化的完全体,那么我就把返回值改成一个POJO好了,于是我写了这么一个类:
public class Result {
private boolean successed;
private Object value;
private String errInfo;
//忽略getter和setter
}
然后接口的返回值全部改成Result:
public interface ITestDao {
Result getAll();
Result get();
Result getCount();
Result save();
Result update();
Result delete();
}
看上去感觉还行,现在我的dao层代码就不是单纯的put了,而是会调用具体属性的setter,而这就意味着我得到了语言层面的类型检查(例如我不能把一个字符串传进isSuccessed()里面了,而使用Map的时候是可以的):
public class TestDao {
public Result getAll(){
Result result=new Result();
try {
List<?> list = null;//假设这里从数据库查到数据
result.isSuccessed(true);
result.setResult(list);
}catch (Exception e) {
result.isSuccessed(false);
result.setErrInfo(e.getMessage());
}
return result;
}
}
但我还是没有解决那个Object的问题,在Result类中,依然有一个碍眼的Object value,把它get出来依然还需要强制转换。
这个POJO又陪伴了我开发的一段时间,后来我把这个POJO按照service层和dao层分开了,分别是ServiceResult和DaoResult,因为我发现service层返回的数据和dao层返回的数据要求不同,service层的比较复杂一些。
再后来,我接触到了一本非常著名的书,它叫Effective Java,这本书和以前那本struts2一样给了我很大的启发,在这本书的第29条中,提到了一种叫做【类型安全的异构容器】,实例的代码如下:
public class Context {
private Map<Class<?>, Object> context = new HashMap<>();
public <T> void put(Class<T> type, T instance) {
if (type == null)
throw new NullPointerException();
context.put(type, type.cast(instance));
}
public <T> T get(Class<T> type) {
return type.cast(context.get(type));
}
}
Context的内部依然是一个Map,Map的value依然是Object类型,但是这个容器巧妙的地方就在于它在put和get之前都会使用Class.type方法(class就从Key那里来)来转换value的类型,如果转换失败就会报错,最重要的是,它本身和使用它的代码都没有warn!
当时看到这个容器的我就傻眼了,居然还有这种操作,太骚了。当时我在做一个框架的开发工作,马上决定就用这种容器来充当框架底层的容器(后来运行得还不错),但是当时的我还没意识到,在ServiceResult和DaoResult身上,这个容器也是有用武之地的。
意识到ServiceResult和DaoResult的蹩脚又是一段时间后的事情了,那时候我用这个【类型安全的异构容器】也挺多次了,慢慢地熟练了起来,然后才想起,最重要的ServiceResult和DaoResult不是还有那个Object value没解决的问题吗?巧了,赶紧用这个容器去重构它们,于是它们就变成了类似下面这个样子(这里用DaoResult举例):
public class DaoResult {
private boolean successed;
private Map<Class<?>, Object> theResult;
private String errInfo;
public DaoResult(boolean successed, Object value) {
theResult.put(value.getClass(), value.getClass().cast(value));
this.successed = successed;
}
public <T> T getResult(Class<T> type) {
return type.cast(theResult.get(type));
}
}
使用了【类型安全的异构容器】之后,我发现theResult不仅仅可以放一个值了,而是可以放多个,这个也算作是意外的收获吧。自此,我的ServiceResult和DaoResult最后那个顽强的Object value终于解决了,这个困惑了我三年的问题最终也迎来HAPPY END。
但是过了不久,我逐渐开始发觉,【类型安全的异构容器】并不完美,它自身仍然存在着一个致命的缺点,那就是同一类型的数据,只能放一个value,举个例子:
DaoResult daoResult=new DaoResult();
List list1=new ArrayList();
List list2=new ArrayList();
daoResult.setResult(List.class,list1);
//当放入list2的时候,因为key冲突的原因list1会被替换掉
daoResult.setResult(List.class,list2);
在Effective Java中,也没有就这个问题提出具体的解决方案。不过当时意识到这个局限性的我还没有遇到需要一个DaoResult需要有两个以上的同样类型value的情况,所以就暂时没有管它了。
实际上,DaoResult是不需要考虑这个问题的,因为dao层返回的结果都是比较单纯的,如果是列表就是List,单体就是实体类,计数就是一个整型数,一般value只有一个,但是service层就不一样了,很快我的ServiceResult就要面对这样一个问题。
例如现在我有一个类似于淘宝上的一个商品详细信息的页面,这个页面有这个商品的详细信息,这个就是单体,ServiceResult没有冲突,但是,它还有一个展示图片路径列表,这就是一个List,另外还有一个作品类型的列表,这就是第二个List了,现在两个List都要塞进ServiceResult中,那么第一个塞进去的List一定会被第二个List替换掉,这样页面就出错了。实际上再往深考虑,不仅仅是这两个List,万一未来需要加入商品的评论或者其他需要List型数据的功能时,这个冲突就更严重了。当然,可以通过拼凑这些List进一个Map里,再把一整个Map放进ServiceResult中,但是这样就又回到一开始的Map载体的问题了。
那么,把这些List放进一个POJO里面如何呢?看起来是一个不错的想法,但是这样子的话我就要为各种类型的value创建不同的POJO,而且我也不知道一个POJO里面还存多少个value(像上面的两个List冲突,那我就要创建一个有两个List属性的POJO,但是如果变成三个List冲突,那这个POJO就没用了,如此下去,类数量肯定会爆炸增长)。也不能在POJO里创建一个Map,这样又回到一开始的Map载体的问题了。貌似这个问题陷入了死循环:因为想要类型安全而放弃了Map,但是又因为放弃了Map而降低了数据的伸缩性。
实际上当时的我最开始的想法是,既然class作为key会造成冲突,那么就为class在外面包一层POJO,然后把这个POJO当作key传进ServiceResult就可以了,这个POJO很简单,只有一个class的属性:
public class ResultKey {
private Class<?> clazz;
public ResultKey(Class<?> clazz) {
this.clazz = clazz;
}
public Class<?> getClazz() {
return clazz;
}
}
这样子做会出现两个问题,第一,类数量肯定会爆炸;第二,Map的key机制是需要同一个key对象才能保证取出来的value是相同的,看下面的代码:
Map<ResultKey,Object> map=new HashMap<>();
map.put(new ResultKey(List.class),"something");
Object value=map.get(new ResultKey(List.class));
最后获取出来的value其实是null的,因为key的内部值虽然一样,但是并不是同一个对象,如果改成这样,value就可以获取到了:
Map<ResultKey,Object> map=new HashMap<>();
ResultKey resultKey=new ResultKey(List.class);
map.put(resultKey,"something");
Object value=map.get(resultKey);
这就意味着,我要为每个新加的POJO提供单例实现,当然这也是一种解决方案,但是并不算很优雅。
在这个时候,Effective Java再次给了我启发。Effective Java的内容非常丰富,除了上面提到的【类型安全的异构容器】外,还有许多有趣的内容,例如,枚举。
枚举从jdk1.5就已经提供了支持,只不过我一直都没怎么使用过它,倒不如说,我不懂怎么用,也不知道用它到底有什么好处,而Effective Java专门用了一章来讲述枚举的用法以及好处,读完之后我对枚举的好感度直线上升,同时想着有什么机会能够用上它。
实际上枚举已经可以很好地解决上面的问题了,首先,每个枚举值都是单例的,这由JVM提供实现的,不需要自己写,这样我就可以很方便地从Map中根据枚举值来取出对应的value;第二,它能解决类数量爆炸的问题,在前面的“用类去包裹class”的想法中,一个key就需要一个新的类,这也导致类的数量会越来越多,但是如果用枚举的话,增加的只是枚举类里面的一个属性值而已。决定好使用枚举这个方案后,我马上把枚举类写了出来:
public enum ResultKey {
private Class<?> clazz;
ResultKey(Class<?> clazz) {
this.clazz = clazz;
}
public Class<?> getClazz() {
return clazz;
}
}
后续如果要添加key的种类的话,就只要在ResultKey中增加枚举值就行了,例如如果有两个List冲突的话,那就添加两个枚举值:
LIST1(List.class),LIST2(List.class);
然后在service层分别使用它们。
ServiceResult sr=new ServiceResult();
List list1;
List list2;
sr.setResult(ResultKey.LIST1,list1);
sr.setResult(ResultKey.LIST2,list2);
正当我兴高采烈地以为问题已经全部解决时,我突然发现我的ServiceResult类有一个警告,这个警告来自于getResult方法。
public <T> T getResult(ResultKey resultKey) {
return (T) resultKey.getClazz().cast(theResult.get(resultKey));
}
这里即使使用cast()去动态转换类型也会报错,因为cast()返回的是一个Object,而这个泛型方法要求返回一个T,所以还需要一次强制类型转换。如果是一般的类的话,那么只要把ResultKey也改为泛型就不需要强转了,但可惜的是,枚举目前是不支持泛型的(枚举的思想本身就是一种单例的泛型),看来这个问题还是没有完美地得到解决,只能期待后续的我在哪一天又突然灵光一现,想到了下一步的解决方案了,重构的道路还很漫长。
==================================================================
在写下这篇文章的同时,我还同时继续思考了一下目前我的ResultKey的一些问题,一个重要的问题是ResultKey的枚举值还是太多了,我现在在为一个志愿者组织写官网,我对ResultKey的原则就是一个业务就用一个枚举值,这导致了枚举值的数量变得很多(如果不用枚举,那这个数量就是新增的类的数量了,那就更恐怖),到目前为止,枚举值已经有16个了,而且大多数都是List.class。
实际上完全不需要这么多重复的枚举值,只要为每个类型准备三个左右的枚举值目前就够用了,但是这样枚举值的名字看起来就过于通用导致可读性不高,不过感觉问题不大。不过这些枚举值都是静态写死了,感觉不是很好,如果枚举类可以运行时动态地添加枚举值就更好了,不过java目前貌似还做不到这一点,那这篇关于层与层之间的数据载体的研究就先到此为止吧。