Примечание: эти рекомендации - адаптированный под публикацию результат 5-летних проверок выпускных работ участников нашей стажировки "Enterprise Java-разработчик". Часть из них относится только к выполнению тестового задания при устройстве на работу: Java-приложение с REST API. Часть - к разработке на Java. И часть - к разработке любых приложений. Надеюсь, что каждый найдет что-то полезное. Буду рад обсуждению спорных тем в комментариях.
Общие рекомендации при реализации тестового задания:
Не изобретай велосипедов! Грубая ошибка - пытаться сделать стандартные вещи по-своему, чаще всего криво. На проекте все должно быть единообразно. Найди небольшой проект с кодом, который сделан красиво и правильно (хотя бы Spring Pet Clinic) и сделай все максимально в этом стиле.
Рекомендую писать проект на востребованном на рынке стеке: на Java это Spring Boot + Spring Data JPA (работа с БД) + Swagger/OpenAPI 3.0 (REST документация).
Представьте себе, что ПМ (лид, архитектор) дал вам ТЗ и некоторое время недоступен. У вас, конечно, есть много мыслей, для чего нужно приложение, как исправить ТЗ, дополнить его и сделать правильно. НО НЕ НАДО ИХ РЕАЛИЗОВЫВАТЬ В КОДЕ. Нужно сделать все строго по ТЗ, максимально просто, удобно для доработок и для использования со стороны клиента.
Совершенство достигнуто не тогда, когда нечего добавить, а тогда, когда нечего отнять
Антуан де Сент-Экзюпери
Рекомендации по разделам:
1: ТЗ (Тех.задание)
1.1: Читай ТЗ ОЧЕНЬ внимательно, НЕ надо ничего своего туда домысливать и творчески изменять
1.2: Учитывай, что пользователей может быть много, а админов - мало (если в ТЗ есть такие роли)
1.3: Сначала сделай основные сценарий по ТЗ. Все остальное (если очень хочется, 3 раза подумай) - потом
2. API
2.1: API продумывай не с точки зрения программиста и объектов, а с точки зрения того, кто им будет пользоваться (клиента, UI)
2.2: Тщательно считай количество запросов в API для отображения нужной информации
2.3: Из потребностей приложения (клиента) реализуй только очевидные сценарии. Необходимо и достаточно: ВСЕ НЕОБХОДИМОЕ для клиента и НИЧЕГО ЛИШНЕГО. Процесс творческий, приходит с опытом
-
2.4: Делаем REST API в соответствии с концепцией REST (url в общем имеют вид
{ресурс}/{id_ресурсa}[/{подресурс}/{id_подресурсa}][параметры]
). Имена ресурсов во множественном числе! Самая распространенная и грубая ошибка - не придерживаться этих простых правил 2.5: Разделение на роли я предпочитаю на уровне URL. Сразу и однозначно видно, какой API у админа, какое у пользователя (API админа начинается, например, с /admin/...)
2.6: На управление (CRUD) разными ресурсами должны быть ОТДЕЛЬНЫЕ контроллеры. Не надо все, что может админ, сваливать в одну кучу
2.7: Проверьте в Swagger, что в POST и PUT нет ничего лишнего, а в GET есть все необходимые данные
2.8. Исключите из Swagger служебные контроллеры, которые не относятся к API
3: Код:
3.1: Строго соблюдайте соглашения Java по именованию: пакеты только маленькими буквами, методы начинаются с маленькой буквы, классы с большой. Незнания Java Core - тестовое задание сразу в корзину
-
3.2 В проекте (и тестовом задании на работу), в отличие от учебного, оставляйте только необходимый для работы по ТЗ приложения код, ничего лишнего
3.2.1: НЕ надо делать разные профили базы и работы с ней
3.2.2: НЕ надо делать абстрактных контроллеров на всякий случай
3.2.3: НЕ обязательно делать сервисы, если там нет ничего, кроме делегирования в репозитории
3.2.4: НЕ нужны локализация, UI (если по ТЗ нужен только REST API), типы ошибок, Json View
3.3: Название пакетов, имен классов для
model/to/web
стандартные (например,model/domain
). НЕ надо придумывать своих собственных правил3.4: Вместо
return ResponseEntity.ok(entity)
в контроллерах пишитеreturn entity
. Проще!
4: Модель
4.1: Если в приложении есть БД, обычно там хранятся все введенные пользователем и админом данные (история). Они не удаляются и не переписываются заново
4.2: Не делайте в модели объектов, которые не будут использоваться в коде (например, не надо двунаправленных связей, если достаточно однонаправленных)
4.3: Еще раз про hashCode/equals в Entity: не делайте в модели сравнение по полям!
4.4: ORM работает с объектами. Иногда, для упрощения логики, fk_id как поля допустимы
5: Архитектура
-
5.1: Можно:
или подключить Spring Data Rest. Контроллеры генерируются автоматически по репозиториям, требуется настройка ресурсов в кастомных контроллерах
или делать без Spring Data Rest
Нельзя смешивать эти подходы вместе (делать собственные контроллеры в дополнение к тем, что автогенерируются). Я рекомендую 2-й вариант, без Spring Data Rest. Обязательно посмотрите в Swagger, какие контроллеры получились в результате.
5.2: Не размещайте бизнес-логику приложения и преобразования в Transfer Objects (TO) в слое доступа к DB
5.3: Не смешивайте TO и Entity вместе (в частности не передавать Entity в конструктор TO). Они должны быть независимыми друг от друга. Есть разные варианты реализации приложений, c использованием TO и без. Тестовое задание делаем максимально просто.
6. Доступ к БД
6.1: Используйте Spring Data JPA. Методы Repository можно вызывать напрямую из сервиса или из контроллера.
6.2: Если приложению в объекте требуется только его id, используйте reference (
getById
в последних версиях spring-data-jpa)6.3: В DATA-JPA 2.x используются
Optional
. Попробуйте работать с ними, это безопасный способ работать с null-значениями (используйтеorElseThrow
)-
6.4: Не делайте при обновлении записи ради экономии пары строчек кода так:
if(updateCondition)
repository.delete(entity)
}
repository.save(entity)Обновление записи базы должно быть через
UPDATE
.
7: База Данных
7.1: Берите без установки (embedded, например H2 или HSQLDB) и создавайте в памяти! Ваше приложение должно сразу запуститься, без всяких настроек и переменных окружения
7.2: Тщательно считайте количество обращений в базу на каждый запрос. Особенно при запросах от пользователей, которых может быть очень много. Также на сложность запросов от них, чтобы не положить базу
7.3: Сделайте индексы к таблицам. Попробуйте обеспечить UNIQUE. Следите за порядком полей в индексе, от этого зависит индексирование запросов.
7.4: При популировании данных, связанных с датами можно использовать
now()
, чтобы всегда были актуальные исходные данные7.5: Поля базы case insensitive, не пишите camelStyle (для которых нужны кавычки)
7.6: Таблицы я предпочитаю именовать в единственном числе. Исключение -
users
,orders
и другие зарезервированные слова7.7:
date
/timestamp
- зарезервированные слова, лучше избегать их при именовании полей
8: Security
8.1: Проверьте, станет ли код проще с
@AuthenticationPrincipal
8.2: Я предпочитаю четкое разделение ролей на основе URL. Например, для админа URL содержит
/admin
9: Кэширование
9.1: Кэширование желательно для частых и редко меняющихся запросов от пользователей. Тщательно продумайте, что надо кэшировать (самые частые запросы), а что нет (большие или редко запрашиваемые данные)
9.2: Проверьте соответствие ключей к кэшу (параметры кэшируемого метода) с конфигурацией кэша
10: Валидация
10.1: Одних аннотаций валидации недостаточно. Должны быть
@Valid/ @Validation
10.2: Проверяйте входные данные Primary Key при
create/update
в контроллерах. Public API you should be conservative when you reply, but accept liberally
11: Дополнительно
11.1: JUnit-тесты очень желательны. Можно не делать 100% покрытие, только основные сценарии
11.2: Уделяйте внимание обработке ошибок. Чтобы кастомизировать обработку в Spring Boot, можно наследоваться от
ResponseEntityExceptionHandler
.
12: Readme.md
12.1: В начале
readme
должно быть ТЗ или ссылка на него - будет понятно о чем проект12.2: Если задание на English, описание пиши также на English (то же самое относится к языку резюме: вакансия на English предполагает резюме на English)
12.3: Помести в
readme
ссылку на Swagger UI с креденшелами для выполнения запросов12.4: Проверяют задания люди с опытом в Java: не надо писать инструкций, как устанавливать Java и Maven
13: Git
13.1: Должна быть история комитов с внятными комментариями. Это смотрят.
13.2: Не комить служебные файлы: логи, DB, настройки IDEA и пр., это грубая ошибка.
13.3: Все служебные файлы должны быть в
.gitignore
Проверка
-
Попробуй подергать свое API по всем типичным сценариям ТЗ
Удобно использовать? Можно сделать проще?
Сколько раз пришлось вызвать API для типичного сценария?
Сколько запросов к базе было сделано? Можно ли сократить (например с
FETCH/Graph
или через кэширование)?Еще раз - проверь все запросы в Swagger, смотри на формат запросов и данные в ответе. Все должно работать, есть все данные и нет ничего лишнего
API ДОЛЖЕН соответствовать принципам REST (см. ссылки выше)
ОБЯЗАТЕЛЬНО: запусти
mvn test
- ошибок быть не должноОБЯЗАТЕЛЬНО: запусти приложение без всяких предварительных настроек (базы, переменных окружения, ..), лучше на другом компьютере. Приложение должно запускаться и работать!
Спасибо за внимание!
Буду рад, если часть советов пригодится и вашим замечаниям и дополнениям.
Комментарии (18)
taluyev
29.09.2021 01:156.4: Обновление в базе делается через
update
- не стоит смешивать JPA и DAO. апдейт делать не надо и сейв загруженного POJO также не надр - достаточно лишь изменить поля на необходимые значения, остальную работу сделает JPA. JPA и есть "паттерн".
gkislin Автор
29.09.2021 08:57Спасибо за плодовитость комментариев:)
6.4 Я в курсе, что в загруженных entity при изменении полейsave
не нужен. Речь идет о логике, когда ради экономии пары строчек Java люди объединяют save и update примерно так:if(updateCondition)
repository.delete(entity)
}
repository.save(entity)
taluyev
29.09.2021 01:197.5: Поля базы case insensitive, не пишите camelStyle - в обратных одинарных кавычках для мариядэбэ или майскл кейс сенситив, для оракла в двойных кавычках уейс сенситив, для мсскл в квадратных скобках кейс сенситив. поля можно указать в маппинге сущьностей. Поля кейс сенситив...
taluyev
29.09.2021 01:21+17.6: Таблицы я предпочитаю именовать в единственном числе. Исключение - users, т. к. user - зарезервированное слово - order и другие также лучше в множественном.
taluyev
29.09.2021 01:2910.2: Проверяйте входные данные Primary Key при
create/update
в контроллерах. - этим может заниматься база данных, а Джава (слой данных) возбуждать исключение и возврвщать клиенту, например "бэд реквест". Без блокировки не существующих записей (да, это можно сделать) это не сделать консистентно, либо нужно изменять уровень изоляции транзакций, а это уменьшает масштабируемость приложения. Проще на это не обращать внимание, за вас сделает эту работу база данных и джпа/спринг.gkislin Автор
29.09.2021 09:03Это неверно. При POST надо проверять
id==null
объекта, а при PUT соответствие id в URL и в объекте в теле запроса (null допускается). База этого не сделает и проверка должна быть именно в контроллере- он отвечает за проверку входных данных
taluyev
29.09.2021 01:3511.1: JUnit-тесты очень желательны. Можно не делать 100% покрытие, только основные сценарии - junit тесты надо писать без поднятия контекста. С поднятием контекста - это лучше относить к интеграционным тестам. Почему? Потому, что тесты с контестом работают медленно, лучше мказать медленно поднимается контекст. Тесты дата-лейера тем более это интеграционные тесты. Как писать на спринге и не поднимать его в юнмт тестах? надо использовать библиотеку моков, например мокито.
gkislin Автор
29.09.2021 09:06В Spring Boot есть большое количество подходов к тестированию: https://javaops.ru/view/bootjava/lesson06#test
Все они имеют право на использование.
kacetal
29.09.2021 14:31Если тестов много то это не так заметно, так как спринг может кешировать контекст.
taluyev
29.09.2021 01:37ОБЯЗАТЕЛЬНО: запусти
mvn test
- ошибок быть не должно - mvn verify тоже ошибки быть не должно при запуске интеграционных тестов.
dlazerka
Эмм, не совсем современно, хоть и популярно. Spring тянет с собой медленный reflection stack (все ключи бинов в итоге приводятся к строке). Современно -- это Dagger 2, который генерирует dependency injection во время компиляции, так что JIT проще, и можно скомпилировать код.
Вообще современно сейчас деплоить приложения в контейнерах легковесных, где довольно важно startup time. Отчасти из-за этого, по моим наблюдениям, Java в последнее время стали всё больше компилировать с помощью GraalVM. А там dependency injection ahead-of-time очень даже помогает.
Для веб сервера Jetty конечно очень популярен, не спорю, но я бы сказал сейчас современно писать на обёртках Netty (из-за async processing), например http4k или ktor для Котлина. Они тоже компилируются с GraalVM.
Для all-around frameworks (по крайней мере в Долине) популярны Dropwizard и Micronaut.
А JPA уже точно устарело. JOOQ самый популярный из современных, я бы сказал. Опять же таки из-за кодогенерации во время билда, как и Dagger.
gkislin Автор
Спасибо за комментарий! Поправил на "востребованном на рынке стеке ". Действительно, если современный воспринимать как самый передовой, то это не Spring Boot. А популярность фреймворка легко оценить для России в вакансиях на hh или по обзорам: https://www.jrebel.com/blog/2021-java-technology-report, https://snyk.io/jvm-ecosystem-report-2021/ - Spring Boot - 58-62%, Dropwizard 4%-9%, Micronaut 4%-5%. Dagger отсутствует.
Spring Data - это работа с БД по умолчанию в Spring Boot (причем не только с реляционными). Поэтому что "точно устарело" и JOOQ популярнее- категорически не соглашусь. Ну и еще раз именно про тестовые приложения на вакансии - чем у вас все будет стандартнее для разработки и совпадать со стеком компании, тем выше оценка.
ris58h