java 通用IO API 设计-- 分析

2021年05月27日 928次浏览

无争

这里先贴一下原文内容:

从 https://github.com/oldratlee/translations/blob/master/generic-io-api-in-java-and-api-design/README.md 上摘入。

原文链接:A generic input/output API in Java - Rickard Öberg (PS:文章原始链路已失效)
译文发在:【译】Java的通用I/O API,2012-05-11

本文给出了一个通用Java IO API设计,并且有API的Demo代码。

更重要的是给出了这个API设计本身的步骤和过程,这让API设计有些条理。 文中示范了从 普通简单实现 整理成 正交分解、可复用、可扩展、高性能、无错误的API设计 的过程,这个过程是很值得理解和学习!

设计偏向是艺术,一个赏心悦目的设计,尤其是API设计,旁人看来多是妙手偶得的感觉,如果能有些章可循真是一件美事。

给出 减少艺术的艺术工作量 的方法的人是 大师。

上周处理了很多数据搬移,有原始byte形式的,也有String形式的,还有SPI和领域级对象形式。这些活让我觉得,以可伸缩、高性能、正确处理错误的方式把数据从一处搬到另一处,是非常有难度。我要一遍又一遍做一些事,比如从文件中读出String。

这让我有了个想法:一定有个通用模式来处理这些事,可以抽取出来放到库中。“从文本文件中读出文本行”这样的事应该只做一遍,然后用在各个需要的场景中。让我们看一个读文件然后写入另一个文件的典型场景,看看能不能从中发现包含了哪几个部分。

1: File source = new File( getClass().getResource( "/iotest.txt" ).getFile() );
1: File destination = File.createTempFile( "test", ".txt" );
1: destination.deleteOnExit();
2: BufferedReader reader = new BufferedReader(new FileReader(source));
3: long count = 0;
2: try
2: {
4:    BufferedWriter writer = new BufferedWriter(new FileWriter(destination));
4:    try
4:    {
2:        String line = null;
2:        while ((line = reader.readLine()) != null)
2:        {
3:            count++;
4:            writer.append( line ).append( '\n' );
2:        }
4:        writer.close();
4:    } catch (IOException e)
4:    {
4:        writer.close();
4:        destination.delete();
4:    }
2: } finally
2: {
2:     reader.close();
2: }
1: System.out.println(count)

行左边的数字是我标识的4个部分。

  1. 客户代码,初始化了传输,要知道输入和输出的源。
  2. 从输入中读的代码。
  3. 辅助代码,用于跟踪整个过程。这些代码我希望能够重用,而不管是何种传输的类型。
  4. 最后这个部分是接收数据,写数据。这个代码,我要批量读写,可以在第2第4部分修改,改成一次处理多行。

API

public interface Input<T, SenderThrowableType extends Throwable> {
    <ReceiverThrowableType extends Throwable> void transferTo( Output<T,ReceiverThrowableType> output ) throws SenderThrowableType, ReceiverThrowableType;
}

Input,如Iterables,可以被多次使用,用于初始化一处到另一处的传输。因为我泛化传输的数据类型为T,所以可以是任何类型(byte[]、String、EntityState、MyDomainObject)。为了让发送者和接收者可以抛出各自的异常,接口上把各自己的异常声明成了类型参数。比如:在出错的时,Input抛的可以是SQLException,Output抛的是IOException。异常是强类型的,并且在出错时发送和接收双方都必须知道的,这使的双方做合适的恢复操作,关闭他们打开了的资源。

在接收端的是Output接口:

public interface Output<T, ReceiverThrowableType extends Throwable> {
    <SenderThrowableType extends Throwable> void receiveFrom(Sender<T, SenderThrowableType> sender) throws ReceiverThrowableType, SenderThrowableType;
}

当receiveFrom方法被Input调用时(通过调用Input的transferTo方法触发),Output应该打开好了它所需要的资源,然后期望数据从Sender发送过来。Input和Output必须要有类型T,两者对要发送的内容达到一致。后面我们可以看到如何处理不一致的情况。

接下来是Sender接口:

public interface Sender<T, SenderThrowableType extends Throwable> {
    <ReceiverThrowableType extends Throwable> void sendTo(Receiver<T, ReceiverThrowableType> receiver) throws ReceiverThrowableType, SenderThrowableType;
}


Output调用sendTo方法,传入一个Receiver,Sender使用这个Receiver来发送一个一个的数据。Sender在这个时候发起传输,把类型数据T传输到Receiver,一次一个。Receiver接口如下:

public interface Receiver<T, ReceiverThrowableType extends Throwable> {
    void receive(T item) throws ReceiverThrowableType;
}

当Receiver从Sender收到数据时,即可以马上写到底层资源中,也可以分批写入。Receiver知道传输什么时候结束(sendTo方法返回了),所以正确写入剩下的分批数据、关闭持有的资源。

这个简单的模式在发送方和接收方各有2个接口,并保持了以可伸缩、高性能和容错的方式传输数据的潜能。

设计分析

上面是部分原文内容,结合之前分享的逻辑和控制的正交,我们来看下这块的设计。

根据上面的设计画了下面的图
image.png

整体层次还是很清晰的,跟网络的七层协议有点类似,越上层越靠近业务面向功能,越底层越靠近细节。第1层直接面向用户,所以直接面向逻辑,只需要感知输入输出就可以了,这里可以进一步封装一下用户只要提供输入源和输出源就可以了,不需要感知到transferTO 方法的调用,同时这层需要负责对资源生命周期的管理,在Input和Output 中形成闭环。第2层需要构造发送器Sender,就是解析输入源为可处理的信息。第3层构造接收器Receiver,定义如何接收信息(写入输出源)。每一层做的事情都比较聚焦,整体结构是高内聚和低耦合的。但是带来结构上的复杂性,Input 的方法入参依赖了Output,同时需要为Output的方法准备好入参Sender,同样Output 的方法receiverFrom 需要为Sender 的方法准备好Receiver。这样确实没有强耦合,但是又让人有一种隐隐相关的联系。