原文链接: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个部分。
- 客户代码,初始化了传输,要知道输入和输出的源。
- 从输入中读的代码。
- 辅助代码,用于跟踪整个过程。这些代码我希望能够重用,而不管是何种传输的类型。
- 最后这个部分是接收数据,写数据。这个代码,我要批量读写,可以在第2第4部分修改,改成一次处理多行。
一旦明确上面划分的内容,剩下就只是为每个部分整理成一个接口,并保证在各种场景能方便使用。结果如下。 首先要有输入,即Input
接口:
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个接口,并保持了以可伸缩、高性能和容错的方式传输数据的潜能。
上文的API
定义了数据发送和接收的契约,然后可以制定几个输入输出的标准。比如:从文本文件中读取文本行后再写成文本文件。这个操作可以静态方法中,方便的重用。最后,拷贝文本文件可以写成:
File source = ...
File destination = ...
Inputs.text( source ).transferTo( Outputs.text(destination) );
一行代码处理了读文件、写文件、资源清理和其它零零碎碎的操作。真心的赞!transferTo
方法会抛出IOException
,要向用户显示Error
可以catch
这个异常。但实际处理这些Error
往往是,关闭文件,把没有写成功的文件删除,而这些Input
、Output
已经处理好了。我们再也不需要关心文件读写的细节!
上面处理了基本的I/O
传输,我们常常还要做些其它的事。可能要计数一下传输了多少个数据,过滤一下数据,或者是每1000条数据做一下日志,又或者要看一下正在进行什么操作。既然输入输出已经分离,这些事变成在输入输出的协调代码中简单地插入一些逻辑。大部分协调代码有类似的功能,可以放到标准的工具方法中,更方便使用。
第一个标准修饰器是一个过滤器。实现时我用到了Specification
。
public static <T,ReceiverThrowableType extends Throwable>
Output<T, ReceiverThrowableType> filter( final Specification<T> specification, final Output<T, ReceiverThrowableType> output)
{
... create an Output that filters items based on the Specification<T> ...
}
Specification
如下:
interface Specification<T>
{
boolean test(T item);
}
有了这个简单部件,我可以在传输时轻松地过滤掉那些不要出现在接收者端的数据。下面的例子删除文件中的空行:
File source = ...
File destination = ...
Inputs.text( source ).transferTo( Transforms.filter(new Specification<String>()
{
public boolean test(String string)
{
return string.length() != 0;
}
}, Outputs.text(destination) );
第二个常见的操作是把数据从一个类型映射到另一个类型。就是处理要Input
和Output
的数据类型不同,要有方法把输入数据类型映射成输出的数据类型。下面例子的把String
映射成JSONObject
,操作方法会是这个样子:
public static <From,To,ReceiverThrowableType extends Throwable>
Output<From, ReceiverThrowableType> map(final Function<From,To> function, final Output<To, ReceiverThrowableType> output)
Function
定义是:
interface Function<From, To>
{
To map(From from);
}
通过这些,可以把String
的Input
连接到JSONObject
的Output
:
Input<String,IOException> input = ...;
Output<JSONObject,RuntimeException> output = ...;
input.transferTo(Transforms.map(new String2JSON(), output);
String2JSON
类实现了Function
接口,它的map
方法把String
转换成JSONObject
。
到了现在,我们可以实现前面提到数据计数的例子,可以把计数实现成一个通用的映射,转换前后的类型不变,只是维护了一个计数,在每次调用map
方法时更新计数。例子代码如下:
File source = ...
File destination = ...
Counter<String> counter = new Counter<String>();
Inputs.text( source ).transferTo( Transforms.map(counter, Outputs.text(destination) ));
System.out.println("Nr of lines:"+counter.getCount())
【译者注,这一节说具体库Qi4j
,略过】
软件开发时,从一个输入到另一个输出的数据和对象的搬移很常见,可能在中间还要做些转换。通常都是用一些零散代码(scratch
)来完成这些事,结果是代码错误和使用不当的模式。通过引入通用I/O
API
,恰当封闭和隔离,这个任务可以可以更轻松地以伸缩、高性能、无错误的方式完成,并且还可以在在需要额外功能时修饰实现。
这遍文章仅仅勾勒了这种使用方式,API
和辅助类可以在Qi4j Core 1.3-SNAPSHOT
中有(详见Qi4j
的主页)。理想状态是,在整个Qi4j
使用中任何使用I/O
的地方一开始按这种方式来。
【译注】
Qi4j
已经更名为polygene
,在Apache
上
多谢你的阅读,希望你能有所收获 :-)
-EOF-
原文中只给出设计的
- 发展思路
- 关键接口
- 典型的使用方式。
没有给出实现细节,看起来可能比较费力。(细致的分解后的设计往往比较抽象,不容易快速理解), 我实现了完整工程的Demo代码,并写了一篇简单分析。