[Java] 浅说数据类型转换与设计

  数据转换就是指将某各类型数据,按照某种格式转成另一种类型数据。例如字符串转为数字,日期转为字符串。我们在 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 中的转换,就是采用最简单的那种。

  • 不要为了设计模式而设计,而是为了需要而设计。白鹤亮翅看起来很美,不过反过来,就是屁股向后平沙落雁式。

转载于:https://my.oschina.net/delphixp/blog/366286

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值