
Совершенство достигается не тогда, когда уже нечего прибавить,
а когда уже ничего нельзя отнять.
Антуан де Сент-Экзюпери, Ветер, песок и звезды, 1939
Часто приходится проектировать и разрабатывать пакеты ввода/вывода для приложений на Java. С одной стороны есть java.io, которого бывает более чем достаточно. Однако, на практике редко удается обойтись набором стандартных классов и интерфейсов.
В статье, приводится практический пример идеи для реализации пакетов ввода/вывода на платформе Java.
Постановка задачи
Для наглядности рассмотрим пример. Пусть требуется разработать пакет ввода/вывода для матричной библиотеки. При этом, необходимо учитывать, что:
- Матриц (типов/классов) может быть бесчисленное множество, например — плотные, разреженные;
- Входных и выходных форматов может также быть много, например — MatrixMarket, XML;
Формат MatrixMarket
Для формата MatrixMarket будут следующие представления матрицы:
0 2
3 0
Для плотной матрицы:
%%MatrixMarket matrix array real general
2 2
0
2
3
0
Для разреженной матрицы:
%%MatrixMarket matrix coordinate real general
2 2 2
0 1 2
1 0 3
Реализация
Таким образом, реализация пакета I/O должна быть настолько гибкой, чтобы при расширении системы (добавления нового типа матрицы, например блочной или добавления нового формата, например CSV), не требовалось полностью переписывать пакет — а было достаточным лишь реализовать дополнительный класс — класс новой матрицы или нового формата.
Внимательный читатель заметит явное сходство описанной проблемы, с проблемой решаемой шаблоном мост. Это действительно так и статью можно воспринимать как пример реализации шаблона Мост для пакета ввода/вывода.
Возвращаясь к основам паттернов проектирования, можно кратко описать шаблон мост, как разделение абстракции и реализации. В нашем случае — отделение типа матрицы (плотная, блочная) от формата (XML, MatrixMarket). Достигается это за счет введения двух интерфейсов — интерфейса абстракции и интерфейса реализации. Интерфейс абстракции должен высокоуровнево описывать поведение пакета, например методы — readMatrix(), writeMatrix(). В то время, как интерфейс реализации должен описывать низкоуровневые моменты, такие как — readMatrixElement(), writeMatrixElement() и т.д. Тогда в самом простом случае, диаграмма классов для пакета ввода/вывода выглядит следующим образом.

Высоуровневый метод writeMatrix() представляет собой последовательность низкоуровневых вызовов:
- writeMatrixHeader() — запись информации о типе матрицы;
- writeMatrixMeta() — запись информации о размерности матрицы;
- writeMatrixElement() — запись информации об элементе матрицы.
Получается что, шаблон мост решает описанную ранее проблему, благодаря разделению реализации и абстракции. Но в большинстве случаев, объекты, с которыми работают пакеты ввода/вывода уже реализуют механизмы сериализации (Srializable, Externalizable). В нашем случае, интерфейс Matriх уже расширяет интерфейс Externalizable. Почему именно Externalizable а не Serizliable можно прочитать в этом или этом (работа автора) исследованиях. В кратце — Externalizble работает в разы быстрее за счет сокращенного количества вызовов в JVM/Reflection.
И так методы readExternal/writeExternal для плотной матрицы выглядят так:
public void writeExternal(ObjectOutput out) throws IOException {
out.writeInt(rows);
out.writeInt(columns);
for (int i = 0; i < rows; i++) {
for (int j = 0; j < columns; j++) {
out.writeDouble(self[i][j]);
}
}
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
rows = in.readInt();
columns = in.readInt();
self = new double[rows][columns];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < columns; j++) {
self[i][j] = in.readDouble();
}
}
}
Внимательный читатель скажет: “Это же в точности напоминает шаблон Мост!” и будет совершенно прав. Интерфейсы ObjectOutput и ObjectInput реализуют идею шаблона в качестве интерфейсов реализации. Тогда возникает вопрос — “Зачем плодить еще классы вида MatrixReader/MarixWriter и писать в них дубликаты методов readExterna()l/writeExternal()?”. Правильно — незачем. Тем более методология DRY (Don’t Repeat Yourself — Не повторяй себя) нам об этом напоминает.
В таком случае, попытаемся пересмотреть предложенную реализацию пакета, с учетом того, что java.io уже содержит интерфейсы реализации — ObjectInput/ObjectOutput. Т.е. нам требуется лишь реализовать классы форматов — MMOutoutStream/MMInputStream (MM = MatrixMarket), чтобы использовать их вместо стандартных классов для сериализации — ObjectInputStream/ObjectOutputStream. Тогда использование будет очень прозрачным:
// запись
ObjectOutput mmos = new MMOutputStream(“file.mm”);
mmos.writeObject(a);
mmos.close();
// чтение
ObjectInput mmis = new MMInputStream(“file.mm”);
Matrix b = (Matrix) mmis.readObject();
Приведенный выше код, легко трансформируется в код сериализации. Для этого, достаточно лишь заменить классы MM* на Object*. (MMOutputStream -> ObjectOutputStream).
Осталась одна нерешенная проблема. Проблема разделения логических блоков файла. В нашем случае файл делится на:
- Header — заголовок, содержащий тип матрицы;
- Meta — мета информацию, содержащую размерность матрицы;
- Data — данные.
В предыдущей архитектуре пакета были представлены разделенные методы, позволяющие записывать данную информацию раздельно. Однако интерфейсы ОbjectOutput/ObjectInput таких методов очевидно не содержат. Т.е. методы в стандартных классах являются более низкоуровневыми.
Для решения этой проблемы автор предлагает использовать специальные маркеры (байты), обозначающие границы каждого из блоков — заголовка (HEADER_MARKER), Мета-информации (META_MARKER) и элемента (ELEMENT_MARKER).
Тогда методы writeExternal()/readExternal() будут выглядеть следующим образом:
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeInt(rows);
out.writeInt(columns);
out.writeByte(META_MARKER); // записываем маркер META
for (int i = 0; i < rows; i++) {
for (int j = 0; j < columns; j++) {
out.writeDouble(self[i][j]);
out.writeByte(ELEMENT_MARKER); // записываем маркер ELEMENT
}
}
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
rows = in.readInt();
columns = in.readInt();
in.readByte(); // пропускаем меркер META
self = new double[rows][columns];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < columns; j++) {
self[i][j] = in.readDouble();
in.readByte(); // пропускаем меркер ELEMENT
}
}
}
C одной стороны — запись дополнительных нескольких байт не скажется на производительности приложения или читаемости кода, с другой — дает дополнительную возможность для реализации внешних потоков с поддержкой различных форматов.
С точки зрения потока, в нашем случае MMInputStream/MMOutputStream, запись будет выглядеть следующим образом:
- Записывать в буфер, любые данные, полученные через методы writeDouble()/writeInt();
- При получении одного из маркеров — выполнить ассоциированную с ним операции на основании буфера значений.
Ниже приведена основная часть реализации класса MMOutputSteam:
public class MMOutputStream extends OutputStream implements ObjectOutput {
@Override
public void writeByte(int v) throws IOException {
switch (v) {
case HEADER_MARKER:
writeHeader();
break;
case META_MARKER:
writeMeta();
break;
case ELEMENT_MARKER:
writeElement();
break;
}
}
@Override
public void writeInt(int v) throws IOException {
put(String.valueOf(v));
}
@Override
public void writeDouble(double v) throws IOException {
put(String.format(Locale.US, "%.12f", v));
}
@Override
public void writeObject(Object obj) throws IOException {
if (matrix instanceof SparseMatrix) {
put(SPARSE_HEADER);
} else if (matrix instanceof DenseMatrix) {
put(DENSE_HEADER);
}
writeHeader();
matrix.writeExternal(this);
flush();
}
private void writeHeader() throws IOException {
out.write("%%MatrixMarket ");
out.write(buffer[0] + " ");
out.write(buffer[1] + " ");
out.write("real general");
out.newLine();
}
private void writeMeta() throws IOException {
dumpBuffer();
out.newLine();
}
private void writeElement() throws IOException {
dumpBuffer();
out.newLine();
}
private void put(String value) {
buffer[length++] = value;
}
private void dumpBuffer() throws IOException {
for (int i = 0; i < length; i++) {
out.write(buffer[i] + " ");
}
}
}
Резюме
Предложенные вариант реализации пакета ввода/вывода в Java является достаточно удачным применением шблона Мост к существущим иерархиям в Java API. Автор надеется, что описанная в статье идея станет еще одним удобным инструментом в распоряжении читателей и натолкнет их на дополнительные рассуждения по этому теме.
*
Описанный в статье пример, является частью открытой библиотеки для решения задач линейной алгебры — la4j. Рассмотренную реализацию идеи можно посмотреть в пакете la4j.io. В текущей версии поддерживается только формат MatrixMarket.
PS автор топика = автор la4j.