Статьи

Общий API ввода / вывода в Java

На прошлой неделе мне пришлось столкнуться с большим количеством перестановок данных, как в необработанном виде, как байты и строки, так и в виде объектов SPI и уровня домена. Что меня поразило, так это то, что общеизвестно, что трудно перемещать вещи из одного места в другое таким образом, чтобы его можно было масштабировать, выполнять и правильно обрабатывать ошибки. И мне приходилось делать что-то снова и снова, например, читать строки из файлов.

Таким образом, возникла мысль: должен быть общий образец того, как эта вещь работает, который можно извлечь и поместить в библиотеку. «Чтение строк из текстового файла» необходимо выполнить только один раз, а затем использовать в любом сценарии, который этого требует. Давайте рассмотрим типичный пример чтения из одного файла и записи в другой, чтобы увидеть, сможем ли мы выяснить, какими могут быть возможные фрагменты:

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)

Как показывают цифры слева, в этом типе кода я выделил четыре части, которые можно отделить друг от друга. 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;
}

Входы, такие как Iterables, могут использоваться снова и снова, чтобы инициировать передачу данных из одного места в другое, в данном случае это Выход. Поскольку я хочу, чтобы это было универсальным, тип отправляемых сообщений — T, поэтому может быть что угодно (byte [], String, EntityState, MyDomainObject). Я также хочу, чтобы отправитель и получатель данных могли генерировать свои собственные исключения, и это отмечается объявлением их как общих типов исключений. Например, вход может захотеть выбросить SQLException и выходной IOException, если что-то пойдет не так. Это должно быть строго напечатано, и отправитель и получатель должны знать, когда какая-либо из сторон испортит, чтобы они могли правильно восстановиться и закрыть все ресурсы, которые они открыли.

На приемной стороне мы имеем:

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

Когда receiveFrom вызывается входом, в результате вызова TransferTo на входе, выход должен открывать все ресурсы, в которые ему нужно записывать, а затем ожидать, что данные поступят от отправителя. И вход, и выход должны иметь одинаковый тип T, чтобы они согласовывали то, что отправляется. Позже мы увидим, как это можно сделать, если это не так.

Далее у нас есть Отправитель:

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

Выход вызывает sendTo и передает получателю, который отправитель будет использовать для отправки отдельных элементов. В этот момент отправитель может начать передавать данные типа T получателю по одному. Приемник выглядит так:

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

Когда получатель получает отдельные элементы от отправителя, он может либо немедленно записать их в свой базовый ресурс, либо пакетировать их. Поскольку получатель будет знать, когда передача будет выполнена (sendTo вернется), он может правильно записать оставшиеся пакеты и закрыть любой ресурс, который он содержит.

Этот простой шаблон с двумя интерфейсами на отправляющей стороне и двумя на принимающей стороне дает нам возможность осуществлять масштабируемую, эффективную и отказоустойчивую передачу данных.

Стандартные входы и выходы

Итак, теперь, когда вышеуказанный API-интерфейс определяет контракт отправки и получения данных, я могу создать пару стандартных входов и выходов. Допустим, чтение строк текста из файла и запись строк текста в файл. Эти реализации я могу затем вставить в статические методы, чтобы их было легко использовать. В конце концов, сделать копию текстового файла выглядит так:

File source = ...
File destination = ...
Inputs.text( source ).transferTo( Outputs.text(destination) );

Одна строка кода, которая обрабатывает чтение, запись, очистку, буферизацию и еще много чего. Довольно изящно! Метод TransferTo генерирует IOException, который я могу поймать, если я хочу представить какие-либо ошибки пользователю. Но на самом деле обработка этих ошибок, то есть закрытие файлов и, возможно, удаление места назначения в случае неудачной передачи, уже обрабатывается вводом и выводом. Мне больше никогда не придется иметь дело с подробностями чтения текста из файла!

Перехват передачи

В то время как вышеописанное обрабатывает основной ввод / вывод передачи, обычно есть и другие вещи, которые я хочу сделать также. Я могу посчитать, сколько элементов было передано, выполнить фильтрацию или зарегистрировать каждые 1000 элементов или около того, чтобы увидеть, что происходит. Поскольку вход и выход теперь разделены, это становится просто вопросом вставки чего-то посередине, которое опосредует ввод и вывод. Поскольку многие из этих посредников имеют схожий характер, я могу использовать их в стандартных служебных методах, чтобы их было проще использовать.

Первый стандартный декоратор — это фильтр. Я буду реализовывать это путем предоставления спецификации:

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> ...
}

Где спецификация является:

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) );

Вторая распространенная операция — отображение одного типа на другой. Это касается случая, когда один ваш входной сигнал может не совпадать с выходным, на который вы хотите отправить, но есть способ сопоставить тип входа с типом выхода. Например, можно сопоставить String с JSONObject. Сама операция выглядит так:

public static <From,To,ReceiverThrowableType extends Throwable> Output<From, ReceiverThrowableType> map( final Function<From,To> function, final Output<To, ReceiverThrowableType> output)

Где функция определяется как:

interface Function<From, To>
{
To map(From from);
}

Теперь я могу подключить вход строк к выходу JSONObject следующим образом:

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 SPI

Теперь я наконец могу вернуться к своей первоначальной проблеме, которая заставила меня разобраться в этом: как реализовать хороший способ доступа к EntityStates в Qi4j EntityStore и выполнять восстановление резервных копий. Текущая версия доступа к EntityStates выглядит следующим образом:

<ThrowableType extends Throwable> void visitEntityStates( EntityStateVisitor<ThrowableType> visitor, ModuleSPI module )
throws ThrowableType;

interface EntityStateVisitor<ThrowableType extends Throwable>
{
void visitEntityState( EntityState entityState )
throws ThrowableType;
}

Теперь это можно заменить на:

Input<EntityState, EntityStoreException> entityStates(ModuleSPI module);

Из-за паттерна, описанного выше, пользователи этого будут получать больше информации о том, что происходит в обходе, например, если EntityStore вызвал исключение EntityStoreException во время обхода, который они могут затем обработать изящно. Также пользователям становится легко добавлять декораторы, такие как карты и фильтры. Допустим, вас интересуют только EntityState данного типа. Затем добавьте фильтр для этого, не меняя потребителя.

Для импорта резервных данных в EntityStore используется интерфейс, который выглядит примерно так:

interface ImportSupport
{
ImportResult importFrom( Reader in )
throws IOException;
}

Это связывает EntityStore с возможностью только чтения строк JSON из Reader, клиент не будет знать, вызвано ли возникшее IOException из-за ошибок в Reader или записи в хранилище, и ImportResult, который содержит список исключений и количество материал, довольно уродливый, чтобы создавать и использовать. Имея API ввода / вывода, теперь его можно заменить на:

interface ImportSupport
{
Output<String,EntityStoreException> importJSON();
}

Чтобы использовать это, учитывая описанные выше помощники, так же просто, как:

File backup = ...
ImportSupport entityStore = ...
Inputs.text(backup).transferTo(entityStore.importJSON());

Если клиенту нужны какие-либо «дополнительные возможности», такие как подсчет количества импортированных объектов, это можно сделать, добавив фильтры, как показано ранее. Если вы хотите, например, импортировать объекты, измененные до определенной даты (допустим, вы знаете, что какой-то мусор был введен после определенного времени), то добавьте фильтр спецификации, который выполняет эту проверку. И так далее.

Вывод

При разработке программного обеспечения довольно часто приходится перетасовывать данные или объекты с одного входа на другой, возможно, с некоторыми преобразованиями посередине. Обычно эти вещи нужно делать с нуля, что открывает ошибки и плохо примененные шаблоны. Благодаря введению универсального API ввода / вывода, который инкапсулирует и разделяет эти вещи, становится легче выполнять эти задачи масштабируемым, производительным и безошибочным способом, и в то же время позволяя этим задачам при необходимости дополнять их дополнительными функциями.

В этой статье описан один из способов сделать это, и API и помощники, которые я описал, доступны в текущей версии Qi4j Core 1.3-SNAPSHOT в Git (подробности доступа см. На домашней странице Qi4j ). Идея состоит в том, чтобы начать использовать его по всему Qi4j везде, где нам нужно сделать ввод / вывод описанного здесь типа.

Спасибо за чтение, и я надеюсь, что вы чему-то научились ?

 

От http://www.jroller.com/rickard/entry/a_generic_input_output_api