数据转换就是指将某各类型数据,按照某种格式转成另一种类型数据。例如字符串转为数字,日期转为字符串。我们在 JSON, XML,数据注入等方面(如做框架),都经常会遇到类型转换。特别是作为框架,内部提供灵活的数据转换,可以帮助或有效改效框架的可用性和易用性。
要实现数据类型转换,也不是件难事。只要稍微一想,就可以写出一个通用接口,作为转换类的统一访问点。
public interface SimpleConverter{
/**
* 将源数据 source 转换为 targetType 类型
*
* @param source 数据源
* @param targetType 目标类型
* @return 返回转换后的数据
*/
public Object convert(Object source, Class targetType);
}
然后针对最基本的类型转换,实现这个接口,类似下面的做法:
public class StringToDateConverter implements SimpleConverter{
@Override
public Object convert(Object source, Class targetType){
//..实现转换代码
}
}
在JDK1.4之前,很多框架都是采取这样的做法。也就是说,这样的做法比较普遍使用。但是,这样做有一个缺点,就是 Object 过于泛化,无法对输入类型进行限制。例如 StringToDateConverter 按名称的含义来说,应该是将字符串转换为日期。但是,在使用中源数据 source 可能会误传入如日期,数值等,抛异常是其次,最主要的是,你不知道是否已经转换成功,用户体验不是很好。
不过在 JDK1.5 引入泛型之后,就有了比较好的解决方法,也就是使用泛型来约束源数据和目标数据的类型。于是就有了下面新一代的接口形式:
public interface Converter<S, T>{
/**
* 将源数据 S 转换为 目标数据 T
*
* @param source 数据源
* @return 返回转换后的数据
*/
public T convert(S source);
}
由于有了类型约束,我们的接口方法也就不用指定类型,简化了接口方法。相应例子:
public class StringToDateConverter implements Converter<String, Date>{
@Override
public Date convert(String source){
//..实现转换代码
}
}
方法不但要求输入的一定是字符串类型,而且返回类型也一定是 Date。使用者不怕传错参,也不用对结果进行强制类型转换,使用体验明显好过上面的。
感觉不错吧。。。啦啦啦。。啦啦啦。。我是快乐的小农码。。
接着: DateToStringConverter, IntegerToStringConverter, LongToStringConverter... Ctrl + c -> Ctrl + v
啦啦啦。。啦啦啦。。我是勤劳的小农码。。天天拷贝又粘贴。。。
当然,作为一位以懒惰为终极目标的,有品位小码农。拷贝粘贴也是懒得干滴。。。显然,如果对每种转换都写个类,这样的类也太多了吧?。。。这也是个问题。
回头审视一下 Converter<S, T> 接口,约束力非常强,比较适用于 1:1 这样的转换。也就是非常单纯的一种类型转成另一种类型。但是对于 DateToString, IntegerToString ... 之类的,却是 n:1 的转换。如果要减少转换类,很明显 Converter<S, T> 并不适合。
既然做不了小资,再回头看看 SimpleConverter 这个接口,有些东西虽然老土,但优点就是实在:SimpleConverter 能够解决 n:1 的问题。
那么,优点是有,缺点也明显。我们是否可以对这个接口进行改造,突显优点的同时,尽量减少或消除其缺点?
我们再回看 SimpleConverter 的缺点:SimpleConverter 象个“黑洞”,你啥都可以往里面放;而对于开发者来说,除了看类名或许可以“猜”出你能干些啥,但实际使用时心里可是没底。对于类型转换来说,事实上我们希望这个类能够“告诉”我,“我”能干些什么。
如果类能够“告诉”调用者,“我”能做些什么,这是一种比较友好的设计
于是,我们可以将 SimpleConverter 改造一下:(名字也换成 GenericConverter,更能表达接口的含意)
public interface GenericConverter{
/**
* 返回本类能够进行转换的类型转换集
*/
public Set<ConvertiblePair> getConvertiblePair();
/**
* 将源数据 source 转换为 targetType 类型
*
* @param source 数据源
* @param targetType 目标类型
* @return 返回转换后的数据
*/
public Object convert(Object source, Class<?> targetType);
}
其中,ConvertiblePair 类简单定义如下:
/**
* 源--转--目标匹配类
*/
public class ConvertiblePair{
/**
* 数据源类型
*/
private Class<?> sourceType;
/**
* 能够转成的数据的类型
*/
private Class<?> targetType;
public ConvertiblePair(Class<?> sourceType, Class<?> targetType) {
this.sourceType = sourceType;
this.targetType = targetType;
}
/**
* @return the sourceType
*/
public Class<?> getSourceType() {
return sourceType;
}
/**
* @return the targetType
*/
public Class<?> getTargetType() {
return targetType;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
ConvertiblePair other = (ConvertiblePair) obj;
return sourceType.equals(other.sourceType) && targetType.equals(other.targetType);
}
}
这样,我们就可以通过 getConvertiblePair() 方法来让使用者知道,我的能力可以去到那儿。
这种表述能力的方法:你能干些啥呢?。。“诺,你自己看”,扔给一张表让使用者自己查。事实上,我们还可以使用另外一种表述方式:我将要转换的类型扔给你,你负责帮我查。下面我们引入一个这样的接口:
public interface ConditionalConverter{
/**
* 回答是否能够将 sourceType 类型数据转成 targetType 类型的数据
*/
public boolean matches(Class<?> sourceType, Class<?> targetType);
}
这种服务类型从使用的角度来看,好感明显上升。我们可以将这个接口与上面的GenericConverter 合并,也可以独立使用,提供足够的灵活度。
注:我们看到,代码的复杂度在增加。事实如此,当我们想将粗的东西化成细的,又或者追求更灵活更广泛的适应性时,代码的增加是不可避免的,这需要我们来权衡。如果只是想做简单的转换,其实上 SimpleConverter 已经够用。但是,如果想做一个底层的,基本的框架,估计得要一起上。
至此,我们解决了 SimpleConverter 的问题了吗?只解决了一半,可以知道类能干些什么事。但是,结果我们还得要进行强制转换,如下面那样:
Date date = (Date)stringToDate.convert("2015-01-01", Date.class);
还有一件事就是,我们将面对一群的 Converter:StringToDateConverter, StringToEnumConverter, ArrayToStringConverter, CollectionToStringConverter等等。固然,我们可以根据需要使用所需的类,但另一方面,这些类都是转换类,是非常集中于某一方面的类(也就是这些类的共性)。
事实上,并不一定要使用者去掌握这些类,我们可以将细节隐藏起来。将背后一堆类隐藏起来,只提供使用者所需要服务的设计模式是什么?
我们可以使用门面模式(Facade 模式)来将这群小黄人隐藏起来,Facade 模式就是使用一个统一接口去访问背后的众多接口、类或模块。
既然从细节上,熊掌与鱼不可兼得。那么我们为什么不能在更高的层面上协调呢?于是我们考虑引入一个 Facade 类:
public class ConverterService{
/**
* 将源数据 source 转换为 targetType 类型
*
* @param source 数据源
* @param targetType 目标类型
* @return 返回转换后的数据
*/
public <T> T convert(Object source, Class<T> targetType){
//实现代码
}
}
很眼熟是吧?。。不就是 SimpleConverter 吗?。。确实如此,因为 SimpleConverter 就是最普适的转换方法。这是所有转换的基础共性,作为 Facade 类,当然以此为“模板”。
我们实现一个最简单的的 Facade 类:
public interface ConditionalGenericConverter{
/**
* 回答是否能够将 sourceType 类型数据转成 targetType 类型的数据
*/
public boolean matches(Class<?> sourceType, Class<?> targetType);
/**
* 将源数据 source 转换为 targetType 类型
*
* @param source 数据源
* @param targetType 目标类型
* @return 返回转换后的数据
*/
public Object convert(Object source, Class<?> targetType);
}
public class ConversionService{
private final set<ConditionalGenericConverter> converters = new HashSet<>();
/**
* 将源数据 source 转换为 targetType 类型
*
* @param source 数据源
* @param targetType 目标类型
* @return 返回转换后的数据
*/
public <T> T convert(Object source, Class<T> targetType){
ConditionalGenericConverter converter = null;
for(ConditionalGenericConverter c: converters){
if(c.matches(source.getClass(), targetType)){
converter = c;
break;
}
}
return (T)c.convert(source, targetType);
}
/**
* 添加转换器
*/
public ConversionService addConverter(ConditionalGenericConverter c){
converters.add(c);
}
}
更进一步的话,可以多加上一个工厂类,方便大家使用:
public class ConverterServiceFactory{
/**
* 创建一个省缺转换服务类
**/
public static ConversionService createDefaultConversionService(){
ConversionService service = new ConversionService();
addDefaultConverter(service);
return service;
}
private static void addDefaultConverter(ConversionService service){
service.addGenericConverter(new DateGenericConverter());
service.addGenericConverter(new StringGenericConverter());
....
}
}
使用时,可以这样:
ConversionService conversionService = ConverterServiceFactory.createDefaultConversionService();
Date value = conversionService.convert(rquestValue, Date.class);
小结:
事实上,如果仅做简单的转换,无需费这种周章。但是对于通用性框架来说,做这样的构思设计还是有必要的。
上面所说的内容,只是一个概要说明。事实上,在一些通用情景中,还有很多细节内容需要考虑:
-
继承方面的判断与转换。例如,你做了一个类 A 的转换,但是如果 A 的子类呢?是再写一个转换器,还是让他作为 A 类由 A 类的转换器来转换?
-
效率需要考虑吗? 我在 ConversionService 中使用了 Set 来存放转换器,如果使用Map来存放转换器,无疑在效率上会大大提高。在这咱情况下,public Set<ConvertiblePair> getConvertiblePair(); 这个方法就有很大的好处。例如:
public class ConversionService{
private final Map<ConvertiblePair, GenericConverter> converters = new HashMap<>(); public GenericConverter findConverter(Class<?> sourceType, Class<?> targetType){ return converters.get(new ConvertiblePair(sourceType, targetType)); }
}
-
转换细节。例如,日期类型转成字符串类型,存在一个转换格式的问题。有些应用是“2015-01-01”,有些应用是"2015/01/01"等等。那么,我们又如何为使用者提供定制这些格式的机会呢?解决方法有很多,例如 定义接口 BeforeConvert,在转换之前将匹配的转换器,通过这个接口来“暴露”给使用者,让使用者可以在转换之前设置转换格式等等。
附:
-
如果想了解复杂的转换,看看 Spring 的源码就明白。而 Apache BeanUtils 中的转换,就是采用最简单的那种。
-
不要为了设计模式而设计,而是为了需要而设计。白鹤亮翅看起来很美,不过反过来,就是屁股向后平沙落雁式。