Совершенство достигается не тогда, когда уже нечего прибавить,
а когда уже ничего нельзя отнять.
Антуан де Сент-Экзюпери, Ветер, песок и звезды, 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.
