Эта статья была впервые опубликована в моем блоге .
Цель программирования, ориентированного на данные (DOP), состоит в том, чтобы снизить сложность программных систем, способствуя обращению с данными как с первоклассным гражданином.
Конкретно, это сводится к применению 3 принципов:
- Код отделен кодом от данных
- Данные являются неизменяемыми
- Доступ к данным является гибким
Эти принципы не новы: они были так или иначе приняты сообществом Java на протяжении многих лет с помощью различных шаблонов проектирования (например, Entity component system ) и интеллектуальных библиотек, которые используют аннотации Java (например, Project Lombok).
Однако я считаю, что комбинация этих 3 принципов создает целое, которое больше, чем сумма его частей , в том смысле, что программные системы, построенные поверх принципов DOP, как правило, менее сложны. В моей книге Программирование, ориентированное на данные , я более подробно исследую, как применять принципы DOPE в контексте производственной программной системы .
В настоящей статье я собираюсь проиллюстрировать, как применять принципы DOP в Java.
Предположим, мы хотим создать систему управления библиотекой со следующими требованиями:
- Два вида пользователей : члены библиотеки и библиотекари
- Пользователи входят в систему по электронной почте и паролю.
- Участники могут брать книги
- Участники и библиотекари могут искать книги по названию или по автору
- Библиотекари могут блокировать и разблокировать участников (например, когда они опаздывают с возвратом книги)
- Библиотекари могут перечислить книги , предоставленные в настоящее время участником
- Там может быть несколько экземпляров книги
Возможный (наивный) классический дизайн Java для такой системы был бы составлен из следующих классов:
Библиотека: Центральная часть, для которой предназначена системаКнига: КнигаЭлемент книги: Книга может иметь несколько копий, каждая копия рассматривается как элемент книгиBookLending: Когда книга предоставляется взаймы, создается объект книжного кредитованияУчастник: Сотрудник библиотекиБиблиотекарь: БиблиотекарьПользователь: Базовый класс дляБиблиотекарьиЧленКаталог: Содержит список книгАвтор: Автор книги
Возможная диаграмма классов (без учета подробностей о членах и методах) была бы примерно такой:
Конечно, эксперт по Java, вероятно, придумал бы более разумный дизайн, используя некоторые интеллектуальные шаблоны проектирования .
Теперь я хотел бы проиллюстрировать, как применение принципа DOP № 1 естественным образом приводит к более простому дизайну без использования каких-либо шаблонов проектирования. Мы собираемся разделить каждый класс нашей системы на два класса:
- Класс кода с статическими методами только
- Класс данных, содержащий только члены
В результате получается диаграмма, состоящая из двух непересекающихся диаграмм:
- Классы данных слева
- Классы кода справа
Разве вы не согласны с тем, что полученная диаграмма менее сложна, чем предыдущая?
Классная мысль заключается в том, что для применения принципа № 1 не требуется быть экспертом по Java. Конечно, сочетание шаблонов интеллектуального проектирования и принципа DOP № 1 привело бы к еще лучшему дизайну.
Преимущества применения принципа DOP # 2 о неизменности данных в Java широко обсуждались. В принципе, все сводится к:
- Безопасность потока
- Отсутствие скрытых побочных эффектов
- Легкость ловли
- Предотвращение мутации идентичности
Интересный вопрос заключается в следующем: как мы представляем неизменяемые данные в Java. В основном существует три подхода:
- Неизменяемые классы (шаблонный код, избегаемый с помощью аннотаций Java)
- Записи данных (доступны начиная с Java 14)
- Постоянные хэш-карты
Представление данных с помощью неизменяемых классов
Неизменяемые классы, не имеют методов, и члены не могут быть изменены.
Написание вручную для каждого неизменяемого класса нашей системы соответствующих конструкторов, геттеров, equals() , hashCode() и toString() включает в себя много шаблонного кода. Мы могли бы избежать шаблонного кода, используя аннотацию Java, такую как @value аннотация из Project Lombok .
Вот как мы могли бы представить данные каталога нашей системы управления библиотеками, используя @value аннотацию:
@value public class AuthorData {
String name;
List bookIds;
}
@value public class BookData {
String title;
String isbn;
Integer publicationYear;
List authorIds;
}
@value public class CatalogData {
String items;
Map booksByIsbn;
Map authorByIds;
}
В качестве примера, вот как мы могли бы создать экземпляр данных каталога с помощью одной книги: Watchmen.
var watchmen = new BookData("Watchmen",
"978-1779501127",
1987,
List.of("alan-moore", "dave-gibbons"));
var alanM = new AuthorData("Alan Moore", List.of("978-1779501127"));
var daveG = new AuthorData("Dave Gibbons", List.of("978-1779501127"));
var booksByIsbn = Map.of("978-1779501127", watchmen);
var authorsById = Map.of("alan-moore", alanM,
"dave-gibbons", daveG);
var catalog = new CatalogData("books", booksByIsbn, authorsById);
И мы выводим в верхнем регистре идентификатор первого автора Watchmen следующим образом:
catalog.booksByIsbn().get("978-1779501127")
.get(0).toUpperCase(); // "ALAN-MOORE"
Представление данных с помощью записей
Разработчики языка Java признают необходимость обеспечения неизменяемого представления данных на уровне языка. Java 14 представила концепцию записи, которая предоставляет первоклассные средства для моделирования агрегатов только для данных.
Вот как будет выглядеть наша модель данных с записями:
public record AuthorData (String name,
List bookIds) {}
public record BookData (String title,
String isbn,
Integer publicationYear,
List authorIds) {}
public record CatalogData (String items,
Map booksByIsbn,
Map authorByIds) {}
Записи создаются как неизменяемые классы:
var watchmen = new BookData("Watchmen",
"978-1779501127",
1987,
List.of("alan-moore", "dave-gibbons"));
var alanM = new AuthorData("Alan Moore", List.of("978-1779501127"));
var daveG = new AuthorData("Dave Gibbons", List.of("978-1779501127"));
var booksByIsbn = Map.of("978-1779501127", watchmen);
var authorsById = Map.of("alan-moore", alanM,
"dave-gibbons", daveG);
var catalog = new CatalogData("books", booksByIsbn, authorsById);
И мы выводим в верхнем регистре идентификатор первого автора Watchmen следующим образом:
catalog.booksByIsbn().get("978-1779501127")
.get(0).toUpperCase(); // "ALAN-MOORE"
Подробнее о записях Java читайте в этой статье .
Постоянные строковые карты
Теперь начинается эзотерическая часть, которая может заставить вас чувствовать себя некомфортно как разработчика Java.
Вместо представления данных с помощью макета, который статически определен в нашей кодовой базе, мы могли бы представлять данные с помощью хэш-карт, вообще не указывая макет данных.
Преимущество такого подхода заключается в том, что он делает доступ к данным и манипулирование данными гибкими. Конечно, он должен пожертвовать гибкостью для безопасности типов . Моя цель здесь не в том, чтобы убедить вас в том, что именно так вы должны представлять данные в Java. Моя скромная цель состоит в том, чтобы предположить, что динамический подход к данным применим в Java. Надеемся, это побудит экспертов Java изучить, имеет ли смысл продвигать подход к динамическим данным в Java.
Давайте сначала посмотрим, как мы могли бы создать экземпляр наших данных каталога, используя собственные неизменяемые строковые карты Java и списки:
var watchmen = Map.of("title", "Watchmen",
"isbn", "978-1779501127",
"publicationYear",1987,
"authorIds", List.of("alan-moore", "dave-gibbons"));
var alanM = Map.of("name", "Alan Moore",
"bookIds", List.of("978-1779501127"));
var daveG = Map.of("name", "Dave Gibbons",
"bookIds", List.of("978-1779501127"));
var booksByIsbn = Map.of("978-1779501127", watchmen);
var authorsById = Map.of("alan-moore", alanM,
"dave-gibbons", daveG);
var catalog = Map.of("items", "book",
"booksByIsbn", booksByIsbn,
"authorsById", authorsById);
Ограничение неизменяемых карт Java заключается в том, что мы не можем эффективно их обновлять. Для создания новой версии данных каталога (например, обновления года издания книги) потребуется скопировать всю карту целиком. К счастью, существует такая штука в области информатики, которая называется постоянные структуры данных это позволяет эффективно обновлять неизменяемые структуры данных как с точки зрения памяти, так и с точки зрения вычислений.
Существует библиотека Java с именем Paguro , которая обеспечивает эффективные постоянные структуры данных в Java.
Создание экземпляра нашего каталога с помощью Paguro немного более подробно, поскольку нам приходится переносить пары ключ-значение в карты с помощью кортежей:
var watchmen = map(tup("title", "Watchmen"),
tup("isbn", "978-1779501127"),
tup("publicationYear", 1987),
tup("authorIds", vec("alan-moore", "dave-gibbons")));
var alanM = map(tup("name", "Alan Moore"),
tup("bookIds", List.of("978-1779501127")));
var daveG = map(tup("name", "Dave Gibbons"),
tup("bookIds", List.of("978-1779501127")));
var booksByIsbn = map(tup("978-1779501127", watchmen));
var authorsById = map(tup("alan-moore", alanM),
tup("dave-gibbons", daveG));
var catalog = map(tup("items", "book"),
tup("booksByIsbn", booksByIsbn),
tup("authorsById", authorsById));
С помощью строковых карт (как Paguro, так и Java) мы не можем легко получить доступ к вложенным данным в нашем каталоге:
catalog.get("booksByIsbn").get("978-1779501127j")
.get("authorIds").get(0).toUpperCase(); // throws an exception
Проблема в том, что внутри карты каталога у нас есть значения разных типов:
элементы– это строкакниги По IsbnиИдентификаторам авторовявляются картами
Чтобы иметь возможность получить доступ к значению, связанному с books По Isbn в виде карты, мы должны выполнить статическое приведение:
var booksByIsbn = (Map)catalog.get("booksByIsbn"); booksByIsbn.get("978-1779501127") // returns a map
И мы должны делать это несколько раз, пока не доберемся до интересующей нас ценности:
((String) ((Map) ((Map )catalog.get("booksByIsbn")) .get("978-1779501127")) .get("authorIds") .get(0)) .toUpperCase(); // "ALAN-MOORE"
Я же говорил тебе, что это будет эзотерично!
Мы могли бы немного облегчить неудобство этого подхода, добавив методы получения в нашу карту для каждого типа значения (аналогично Apache Wicket value maps ). Тогда было бы немного менее неудобно обращаться к значению во вложенной карте, поскольку приведение скрыто в получателе:
catalog.getAsMap("booksByIsbn")
.getAsMap("978-1779501127")
.getAsList("authorIds")
.getAsString(0)
.toUpperCase(); // "ALAN-MOORE"
Мы могли бы продвинуться еще на один шаг и реализовать вложенные средства получения значений (аналогичные get-in в Clojure или Lodash get в JavaScript). Тогда мы могли бы получить доступ к вложенному значению очень кратким способом:
catalog.getInAsString(vec("booksByIsbn",
"978-1779501127",
"authorIds",
0))
.getUpperCase(); // "ALAN_MOORE"
Позвольте мне завершить эту статью упоминанием потенциальных преимуществ , которые обеспечит подход к динамическим данным, если он будет принят сообществом Java.
Слабая зависимость между кодом и данными
Когда фрагмент кода манипулирует данными, представленными общим способом, он не обязательно должен включать класс, который определяет расположение данных. Единственная информация, которая требуется, – это название полей, с которыми нужно работать.
Информационный путь
Когда мы представляем все данные системы в общем виде, каждая часть информации системы доступна через ее информационную базу: список ключей и индексов, которые описывают путь к информации.
Сериализация без отражения
Когда данные представлены с помощью хэш-карт и списков, мы можем сериализовать их (например, сериализация JSON) естественным способом без использования отражения или какой-либо пользовательской аннотации.
Манипулирование данными с помощью функций общего назначения
Когда данные представлены в общем виде, мы можем свободно манипулировать ими с помощью богатого набора функций общего назначения. Позвольте мне привести два кратких примера:
Переименование ключей
Предположим, мы хотим отправить информацию о книге по проводу с небольшим изменением: поле title следует переименовать в bookTitle . При нединамическом подходе к данным нам пришлось бы создать другой класс Книга С Названием Книги (было бы трудно придумать хорошее название!).
В динамическом подходе к данным мы могли бы написать функцию общего назначения renameKey() . Самое классное, что ключ переименования() не будет связан с данными книги. Как следствие, мы могли бы использовать Переименовать ключ () чтобы переименовать поле данных автора.
Объединение данных
Предположим, мы хотели бы обогатить информацию о книгах данными из Amazon и GoodReads. При нединамическом подходе нам, вероятно, потребуется создать классы или записи для Amazon Book В , GoodReadsBookInfo и Enrichedbookinfo . В любом случае, нам пришлось бы написать пользовательский код, который объединяет информацию из Amazon и GoodReads.
В динамическом подходе к данным мы могли бы использовать функцию общего назначения merge , которая работает на произвольной карте.
В этой статье предполагалось, что можно было бы применить принципы программирования, ориентированного на данные, в Java.
- Код отделен кодом от данных
- Данные являются неизменяемыми
- Доступ к данным является гибким
Принципы № 1 и № 2 кажутся вполне естественными для разработчиков Java (особенно с добавлением записей Java). Однако принцип № 3 кажется гораздо менее естественным.
Я надеюсь, что, проиллюстрировав преимущества динамического подхода к данным, я немного мотивировал сообщество Java. Теперь пришло время экспертам по Java взять его оттуда и открыть ( надеюсь, в ближайшем будущем ) каков наилучший способ охватить Программирование, ориентированное на данные, на Java .
Оригинал: “https://dev.to/viebel/data-oriented-programming-in-java-1kf1”