Один из часто рассматриваемых паттернов — паттерн Builder. В основном рассматриваются варианты реализации «классического» варианта этого паттерна:
MyClass my = MyClass.builder().first(1).second(2.0).third(«3»).build();
Паттерн прост и понятен как табурет, но чувствуется какая-то недосказанность — то минимальный вариант объявляется антипаттерном, то более сложные случаи игнорируются. Хотелось бы исправить этот момент, рассмотрев предельные случаи и определив минимальную и максимальную границы сложности этого паттерна.
Итак, расссмотрим их:
Минимальный builder или Реабилитация double brace initialization
Сначала рассмотрим минимальный builder, про который часто забывают — double brace initialization (
http://stackoverflow.com/questions/1958636/what-is-double-brace-initialization-in-java, http://c2.com/cgi/wiki?DoubleBraceInitialization). Используя double brace initialization мы можем делать следующее:
Не смотри обучающие ролики!
new MyClass() >
Нарушение совместимости equals
Что такое «совместимость equals»? Дело в том что стандартный equals примерно такой:
И при сравнении с унаследованным классом equals будет возвращать false. Но мы создаём анонимный унаследованный класс и вмешиваемся в цепочку наследования.
В результате обычно double brace initialization используют для инициализации составных структур. Например:
new TreeMap() >
Тут используются методы, а не прямой доступ к полям и совместимость по equals обычно не требуется. Так как же мы можем использовать такой ненадёжный хакоподобный метод? Да очень просто — выделив для double brace initialization отдельный класс билдера.
Код такого билдера содержит только определения полей с установленными значениями по умолчанию и методы построения, отвечающие за проверку параметров и вызов конструкторов:
public static class Builder < public int first = -1 ; public double second = Double.NaN; public String third = null ; public MyClass create() < return new MyClass( first , second, third ); >>
new MyClass.Builder()>.create()
- Builder не вмешивается в цепочку наследования — это отдельный класс.
- Builder не течёт — его использование прекращается после создания объекта.
- Builder может контролировать параметры — в методе создания объекта.
Для использовании наследования, Builder разделяется на две части (один с полями, другой — с методом создания) следующим образом:
public class MyBaseClass < protected static class BuilderImpl < public int first = -1 ; public double second = Double.NaN; public String third = null ; >public static class Builder extends BuilderImpl < public MyBaseClass create() < return new MyBaseClass( first , second, third ); >> . > public class MyChildClass extends MyBaseClass < protected static class BuilderImpl extends MyBaseClass.BuilderImpl < public Object fourth = null; >public static class Builder extends BuilderImpl < public MyChildClass create() < return new MyChildClass( first , second, third , fourth ); >> . >
Если нужны обязательные параметры, они будут выглядеть так:
Сравнение Python и Java #программирование #shorts #айти
public static class Builder < public double second = Double.NaN; public String third = null ; public MyClass create(int first) < return new MyClass( first , second, third ); >>
new MyClass.Builder()>.create(1)
Это настолько просто, что можно использовать хоть как построитель параметров функций, например:
String fn = new fn()>.invoke();
Перейдём к сложному.
Максимально сложный Mega Builder
- не позволять использовать недопустимые комбинации параметров
- не позволять строить объект если не заполнены обязательные параметров
- не допускать повторной инициализации параметров
Нам понадобится интерфейс для присвоения каждого параметра и возврата нового билдера. Он должен выглядеть как-то так:
public interface TransitionNAME
При этом NAME должен быть разным для каждого интерфейса — ведь их потом надо будет объединять.
Также понадобится и getter, чтобы мы могли получить значение после такого присвоения:
public interface GetterNAME
Поскольку нам понадобится связка transition-getter, определим transition-интерфейс следующим образом:
public interface TransitionNAME
Это также добавит статического контроля в описаниях.
Примерно понятно, наборы каких интерфейсов мы собираемся перебирать. Определимся теперь, как это сделать.
Возьмём такой же как в предыдущем примере 1-2-3 класс и распишем для начала все сочетания параметров. Получим знакомое бинарное представление:
first second third — — — — — + — + — — + + + — — + — + + + — + + +
Для удобства представим это в виде дерева следующим образом:
first second third — — — / + — — /+ + + — /+/+ + + + /+/+/+ + — + /+/-/+ — + — /-/+ — + + /-/+/+ — — + /-/-/+
Промаркируем допустимые сочетания, например так:
first second third — — — / * + — — /+ * + + — /+/+ * + + + /+/+/+ + — + /+/-/+ * — + — /-/+ — + + /-/+/+ * — — + /-/-/+ *
Удалим лишние узлы — терминальные недопустимые узлы и пустые узлы. В общем случае это циклический процесс, продолжающийся пока есть узлы для удаления, но в данном случае у нас только один терминальный недопустимый узел.
first second third — — — / * + — — /+ * + + — /+/+ * + — + /+/-/+ * — + — /-/+ — + + /-/+/+ * — — + /-/-/+ *
Как же реализовать это?
Нам нужно, чтобы каждое присвоение элемента приводило к сокращению оставшихся вариантов использования. Для этого каждое присвоение элемента через transition-интерфейс должно возвращать новый класс builder-а плюс getter-интерфейс для этого transition минус этот transition-интерфейс.
public interface Get_first < int first (); >public interface Get_second < double second(); >public interface Get_third < String third (); >public interface Trans_first < T first (int first ); >public interface Trans_second < T second(double second); >public interface Trans_third
Табличку с этим рисовать неудобно, сократим идентификаторы:
public interface G_1 extends Get_first <> public interface G_2 extends Get_second<> public interface G_3 extends Get_third <> public interface T_1 extends Trans_first <> public interface T_2 extends Trans_second <> public interface T_3 extends Trans_third <>
Нарисуем табличку переходов:
public interface B extends T_1, T_2, T_3 <> // — — — / * public interface B_1 extends T_2, T_3 <> // + — — /+ * public interface B_1_2 extends <> // + + — /+/+ * public interface B_1_3 extends <> // + — + /+/-/+ * public interface B_2 extends T_1, T_3 <> // /-/+ public interface B_2_3 extends <> // — + + /-/+/+ * public interface B_3 extends T_1, T_2 <> // — — + /-/-/+ *
Определим Built интерфейс:
public interface Built
Промаркируем интерфейсы, где уже можно построить класс интерфейсом Built, добавим getter-ы и определим получившийся Builder-интерфейс:
// транзит // | можем строить // геттеры | | // | | | // ————- ———————————- —— // // first first first // | second | second | second // | | third| | third | | third // | | | | | | | | | public interface B extends T_1, T_2, T_3, Built <> // — — — / * public interface B_1 extends G_1, T_2, T_3, Built <> // + — — /+ * public interface B_1_2 extends G_1, G_2, Built <> // + + — /+/+ * public interface B_1_3 extends G_1, G_3, Built <> // + — + /+/-/+ * public interface B_2 extends G_2, T_1, T_3 <> // /-/+ public interface B_2_3 extends G_2, G_3, Built <> // — + + /-/+/+ * public interface B_3 extends G_3, T_1, T_2, Built <> // — — + /-/-/+ * public interface Builder extends B <>
Этих описаний достаточно, чтобы по ним можно было в run-time соорудить proxy, надо только подправить получившиеся определения, добавив в них маркерные интерфейсы:
public interface Built extends BuiltBase <> public interface Get_first extends GetBase < int first (); >public interface Get_second extends GetBase < double second(); >public interface Get_third extends GetBase < String third (); >public interface Trans_first extends TransBase < T first (int first ); >public interface Trans_second extends TransBase < T second(double second); >public interface Trans_third extends TransBase
Теперь надо получить из Builder-классов значения чтобы создать реальный класс. Тут возможно два варианта — или создавать методы для каждого билдера и статически-типизированно получать параметры из каждого builder-а:
public MyClass build(B builder) < return new MyClass(-1 , Double.NaN , null); >public MyClass build(B_1 builder) < return new MyClass(builder.first(), Double.NaN , null); >public MyClass build(B_1_2 builder) < return new MyClass(builder.first(), builder.second(), null); >.
или воспользоваться обобщённым методом, определённым примерно следующим образом:
public MyClass build(BuiltValues values) < return new MyClass( // значения из values ); >
Но как получить значения?
Во-первых у нас есть по-прежнему есть набор builder-классов у которых есть нужные getter-ы. Соответственно надо проверять есть ли реализация нужного getter и если есть — приводить тип к нему и получать значение:
(values instanceof Get_first) ? ((Get_first) values).first() : -1
Конечно, можно добавить метод получения значения, но оно будет нетипизированным, так как мы не сможем получить тип значения из существующих типов:
Object getValue(final Class < ? extends GetBase>key);
(Integer) values.getValue(Get_first.class)
Для того чтобы получить тип, пришлось бы создавать дополнительные классы и связки наподобие:
public interface TypedGetter < ClassgetterClass(); >; public static final Classed GET_FIRST = new Classed(Get_first.class);
Тогда метод получения значения мог бы быть определён следующим образом:
public T get(TypedGetter typedGetter);
Но мы попытаемся обойтись тем что есть — getter и transition интерфейсами. Тогда, без приведений типов, вернуть значение можно только вернув getter-интерфейс или null, если такой интерфейс не определён для данного builder:
T get(Class key);
(null == values.get(Get_second.class)) ? Double.NaN: values.get(Get_second.class).second()
Это уже лучше. Но можно ли добавить значение по-умолчанию в случае отсутствия интерфейса, сохранив тип? Конечно, возможно возвращать типизированный getter-интерфейс, но всё равно придётся передавать нетипизированное значение по умолчанию:
T get(Class key, Object defaultValue);
Но мы можем воспользоваться для установки значения по умолчанию transition-интерфейсом:
T getDefault(Class < ? super T>key);
И использовать это следующим образом:
values.getDefault(Get_third.class).third(«1»).third()
Это всё что можно типобезопасно соорудить с существующими интерфейсами. Создадим обобщённый метод инициализации иллюстрирующий перечисленные варианты использования и проинициализируем результирующий билдер:
Теперь можно его вызывать:
builder() .build(); builder().first(1) .build(); builder().first(1).second(2) .build(); builder().second(2 ).first (1).build(); builder().first(1) .third(«3»).build(); builder().third («3»).first (1).build(); builder() .second(2).third(«3»).build(); builder().third («3»).second(2).build(); builder() .third(«3»).build();
Скачать код и посмотреть на работу context assist можно отсюда.
В частности:
Код рассматриваемого примера: MyClass.java
Пример с generic-типами: MyParameterizedClass.java
Пример не-статического builder: MyLocalClass.java.
Итого
- Double brace initialization не будет хаком или антипаттерном, если добавить немного билдера.
- Гораздо проще пользоваться динамическими объектами + типизированными дескрипторами доступа (см. в тексте пример с TypedGetter) чем использовать сборки интерфейсов или другие варианты статически-типизированных объектов, поскольку это влечёт за собой необходимость работы с reflection со всеми вытекающими.
- С использованием аннотаций возможно удалось бы упростить код proxy-генератора, но это усложнило бы объявления и, вероятно, ухудшило бы выявление несоответствий в compile-time.
- Ну и наконец, в данной статье мы окончательно и бесповоротно определили минимальную и максимальную границу сложности паттерна Builder — все остальные варианты находятся где-то между ними.
Источник: habr.com
Сложные программы на java
Комментарии
Популярные По порядку
Не удалось загрузить комментарии.
ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ
6 книг по Java для программистов любого уровня
Подборка материалов по Java. Если вы изучаете его, то обязательно найдете для себя что-то полезное и неважно на какой стадии изучения вы находитесь.
Какие алгоритмы нужно знать, чтобы стать хорошим программистом?
Данная статья содержит не только самые распространенные алгоритмы и структуры данных, но и более сложные вещи, о которых вы могли не знать. Читаем и узнаем!
Изучаем алгоритмы: полезные книги, веб-сайты, онлайн-курсы и видеоматериалы
В этой подборке представлен список книг, веб-сайтов и онлайн-курсов, дающих понимание как простых, так и продвинутых алгоритмов.
Источник: proglib.io
Пишем Java веб-приложение на современном стеке. С нуля до микросервисной архитектуры. Часть 1
На сегодняшний день в мире разработки на Java существует огромное количество библиотек и технологий, в которых новичку очень легко запутаться. В этом руководстве я постараюсь простым языком описать все шаги, возникающие проблемы и пути их решения. Начинать будем с самого простого и постепенно наращивать функциональность.
Spring Boot
Spring Boot — один из самых популярных универсальных фреймворков для построения веб-приложений на Java. Создадим в среде разработки Gradle Project. Для облегчения работы воспользуемся сайтом https://start.spring.io, который поможет сформировать build.gradle.
Для начала нам необходимо выбрать следующие зависимости:
- Spring Web — необходим для создания веб-приложения;
- Spring Data JPA — для работы с базами данных;
- PostgreSQL Driver — драйвер для работы с PostgreSQL;
- Lombok — библиотека, позволяющая уменьшить количество повторяющегося кода.
В результате генерации build.gradle должно получиться что-то похожее:
plugins < id ‘org.springframework.boot’ version ‘2.4.3’ id ‘io.spring.dependency-management’ version ‘1.0.11.RELEASE’ id ‘java’ >group ‘org.example’ version ‘1.0-SNAPSHOT’ configurations < compileOnly < extendsFrom annotationProcessor >> repositories < mavenCentral() >dependencies < implementation ‘org.springframework.boot:spring-boot-starter-web’ compileOnly ‘org.projectlombok:lombok:1.18.22’ annotationProcessor ‘org.projectlombok:lombok:1.18.22’ testImplementation ‘org.junit.jupiter:junit-jupiter-api:5.7.0’ testRuntimeOnly ‘org.junit.jupiter:junit-jupiter-engine:5.7.0’ implementation ‘org.springframework.boot:spring-boot-starter-data-jpa’ implementation ‘org.springframework.boot:spring-boot-starter-web’ runtimeOnly ‘org.postgresql:postgresql’ >test
Тот же результат можно получить и в самой IntelliJ Idea: File → New → Project → Spring Initializr.
Опишем самый простой контроллер, чтобы удостовериться, что проект работает:
Запустим проект в среде разработки или через терминал: ./gradlew bootRun .
Разработчик автотестов java Открытие , Удалённо , По итогам собеседования
Результат работы можно проверить в браузере перейдя по адресу http://localhost:8080/hello?name=World или с помощью консольной утилиты curl:
curl «http://localhost:8080/hello?name=World» Hello, World
Наш сервис запускается и работает, пора переходить к следующему шагу.
Представим, что нам требуется разработать некий сервис для интернет-магазина по продаже книг. Это будет rest-сервис, который будет позволять добавлять, редактировать, получать описание книги. Хранить данные будем в БД Postgres.
Docker
Для хранения данных нам потребуется база данных. Проще всего запустить инстанс БД с помощью Docker. Docker позволяет запускать приложение в изолированной среде выполнения — контейнере. Поддерживается всеми операционными системами.
Выкачиваем образ БД и запускаем контейнер:
docker pull postgres:12-alpine docker run -d -p 5432:5432 —name db -e POSTGRES_USER=admin -e POSTGRES_PASSWORD=password -e POSTGRES_DB=demo postgres:12-alpine
Lombok
Создадим data-класс «книга». Он будет иметь несколько полей, которые должны иметь getters, конструктор и должна быть неизменяемой (immutable). Среда разработки позволяет автоматически генерировать конструктор и методы доступа к полям, но чтобы уменьшить количество однотипного кода, будем использовать Lombok.
После сборки проекта можно посмотреть, как выглядит класс после компиляции. Воспользуемся стандартной утилитой, входящей в состав JDK:
javap -private build/classes/java/main/com/example/BookStore/model/Book public final class com.example.bookstore.model.Book
Lombok очень упрощает читаемость подобного рода классов и очень широко используется в современной разработке.
Spring Data JPA
Для работы с БД нам потребуется Spring Data JPA, который мы уже добавили в зависимости проекта. Дальше нам нужно описать классы Entity и Repository. Первый соответствует таблице в БД, второй необходим для загрузки и сохранения записей в эту таблицу.
Чемпионат по машинному обучению, искусственному интеллекту и большим данным
14 июля – 18 августа 2021, уже закончилось, Онлайн, Беcплатно
Класс Repository будет выглядеть совсем просто — достаточно объявить интерфейс и наследоваться от CrudRepository:
public interface BookRepository extends CrudRepository
Никакой реализации не требуется. Spring всё сделает за нас. В данном случае мы сразу получим функциональность CRUD — create, read, update, delete. Функционал можно наращивать — чуть позже мы это увидим. Мы описали DAO-слой.
Теперь нам нужен некий сервис, который будет иметь примерно следующий интерфейс:
public interface BookService < Book getBookById(Long id);// получить книгу по id ListgetAllBooks();// получить список всех книг void addBook(Book book);// добавить книгу >
Это так называемый сервисный слой. Реализуем этот интерфейс:
При создании объекта класса Spring опять всё возьмёт на себя — сам создаст объект BookRepository и передаст его в конструктор. Имея объект репозитория мы можем выполнять операции с БД:
bookRepository.findById(id); //прочитать запись из БД по первичному ключу id bookRepository.findAll(); //прочитать все записи из БД и вернуть их в виде списка bookRepository.save(bookEntity); //сохранить объект в БД
Метод findById возвращает объект типа Optional . Это такой специальный тип который может содержать, а может и не содержать значение. Альтернативный способ проверки на null , но позволяющий более изящно написать код. Метод orElseThrow извлекает значение из Optional , и, если оно отсутствует, бросает исключение, которое создается в переданном в качестве аргумента лямбда-выражении. То есть объект исключения будет создаваться только в случае отсутствия значения в Optional .
MapStruct
Смотря на код может показаться, что класс Book не нужен, и достаточно только BookEntity, но это не так. Book — это класс сервисного слоя, а BookEntity — DAO. В нашем простом случае они действительно повторяют друг друга, но бывают и более сложные случаи, когда сервисный слой оперирует с несколькими таблицами и соответственно DAO-объектами.
Если присмотреться, то и тут мы видим однотипный код, когда мы перекладываем данные из BookEntity в Book и обратно. Чтобы упростить себе жизнь и сделать код более читаемым, воспользуемся библиотекой MapStruct. Это mapper, который за нас будет выполнять перекладывание данных из одного объекта в другой и обратно. Для этого добавим зависимости в build.gradle:
dependencies
Создадим mapper, для этого необходимо объявить интерфейс, в котором опишем методы для конвертации из BookEntity в Book и обратно:
После сборки проекта, в каталоге build/generated/sources/annotationProcessor появится сгенерированный исходный код mapper, избавив нас от необходимости писать однотипные десятки строк кода:
Воспользуемся мэппером и перепишем DefaultBookService. Для этого нам достаточно добавить добавить final-поле BookMapper, которое Lombok автоматически подставит в аргумент конструктора, а spring сам инстанциирует и передаст параметром в него:
Нам также потребуется конвертировать объект AddBookRequest в объект Book. Создадим для этого BookToDtoMapper:
Теперь объявим контроллер, на эндпоинты которого будут приходить запросы на создание и получение книг, добавив зависимости BookService и BookToDtoMapper. При необходимости аналогично объекту AddBookRequest можно описать Response-объект, добавив соответствующий метод в мэппер, который будет конвертировать Book в GetBookResponse. Контроллер будет содержать 3 метода: методом POST мы будем добавлять книгу, методом GET получать список всех книг и книгу по идентификатору, который будем передавать в качестве PathVariable.
Осталось создать файл настроек приложения. Для Spring boot по умолчанию это application.properties или application.yml . Мы будем использовать формат properties. Необходимо указать настройки для соединения с БД (выше мы задавали пользователя и его пароль при старте docker-контейнера):
spring.datasource.url=jdbc:postgresql://localhost:5432/demo spring.datasource.username=admin spring.datasource.password=password spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true
Настройка spring.jpa.hibernate.ddl-auto=update указывает hibernate необходимость обновить схему когда это нужно. Так как мы не создавали никаких схем, то приложение сделает это автоматически. В процессе промышленной разработки схемы баз данных постоянно меняются, и часто используются инструменты для версионирования и применения этих изменений, например Liquibase.
Запустим наше приложение и выполним запросы на добавление книг:
curl -X POST —location «http://localhost:8080/books» -H «Content-Type: application/json» -d «< «author» : «Joshua Bloch», «title» : «Effective Java», «price» : 54.99 >» curl -X POST —location «http://localhost:8080/books» -H «Content-Type: application/json» -d «< «author» : «Kathy Sierra», «title» : «Head First Java», «price» : 12.66 >» curl -X POST —location «http://localhost:8080/books» -H «Content-Type: application/json» -d «< «author» : «Benjamin J. Evans», «title» : «Java in a Nutshell: A Desktop Quick Reference», «price» : 28.14 >»
После выполнения запросов в таблице books должны появиться записи. Чтобы удостовериться в этом, можно использовать любой удобный клиент БД. Для примера сделаем это, используя консольный клиент, входящий в состав docker-контейнера. При создании контейнера, мы указали его имя ‘db’ (если имя не задавалось, то можно вывести список всех запущенных контейнеров командой docker container ls , и дальше использовать идентификатор нужного контейнера). Для доступа к шелл-оболочке выполним:
docker exec -ti db sh
Запустим клиент БД и выполним sql-запрос:
psql —username=admin —dbname=demo psql (12.6) Type «help» for help. demo=# SELECT * FROM books; id | author | price | title —-+——————-+——-+———————————————— 1 | Joshua Bloch | 54.99 | Effective Java 2 | Kathy Sierra | 12.66 | Head First Java 3 | Benjamin J. Evans | 28.14 | Java in a Nutshell: A Desktop Quick Reference (3 rows)
Получим список всех книг:
curl -X GET —location «http://localhost:8080/books» -H «Accept: application/json» [ < «id»: 1, «author»: «Joshua Bloch», «title»: «Effective Java», «price»: 54.99 >, < «id»: 2, «author»: «Kathy Sierra», «title»: «Head First Java», «price»: 12.66 >, < «id»: 3, «author»: «Benjamin J. Evans», «title»: «Java in a Nutshell: A Desktop Quick Reference», «price»: 28.14 >]
Получим книгу через запрос к api нашего сервиса, указав идентификатор книги:
curl -X GET —location «http://localhost:8080/books/2» -H «Accept: application/json»
List findAllByAuthorContaining(String author);
В документации можно подробнее прочитать об именовании методов. Здесь мы указываем findAll — найти все записи, ByAuthor — параметр обрамляется %. При вызове этого метода (например с аргументом ‘Bloch’) будет сгенерирован следующий запрос:
SELECT * FROM books WHERE author LIKE ‘%Bloch%’;
Далее добавим метод в BookService и DefaultBookService:
А в контроллере немного модифицируем метод получения списка книг таким образом, что при передаче get-параметра author мы искали по автору, а если параметр не передётся, то используется старая логика и выводится список всех книг:
Теперь можно выполнить поиск:
curl -X GET —location «http://localhost:8080/books?author=Bloch» -H «Accept: application/json» [ < «id»: 1, «author»: «Joshua Bloch», «title»: «Effective Java», «price»: 54.99 >]
Итак, мы написали веб-сервис и познакомились с очень распространенными библиотеками. В следующей части продолжим улучшать наше приложение, добавив в него авторизацию, а также напишем отдельный микросервис, который будет выписывать токены доступа.
Код проекта доступен на GitHub.
Источник: tproger.ru