JavaOpt: Общие рекомендации
- Сборка должна выполняться быстро. Равно как и тесты - в один клик - если они будут отрабатывать долго никто их выполнять не будет
- Сборки должна быть универсальны. Тестирование желательно на максимально возможном и разнообразном окружении
- Частые мелкие изменения предпочтительнее
- Блоки типа:
1 | Thing thing = Thing.DEFAULT; |
Или
1 | Thing thing; |
лучше избегать. И преобразовать в условие типа:
1 | Thing thing = predicate.test(...) ? ... : ...; |
Или даже так:
1 | generateThing().doSomething(); |
- Для анализа бутылочного горлышка следует использовать утилиты для профилирования типа Honest Profiler, async-profiler, perf-map-agent. Они строят Flame-графы, по которым хорошо видны участки с задержками в работе приложения.
Так же можно воспользоваться JProfiler, YourKit и т.д. - Предпочтения по GC:
- там где используется большое количество многопоточного исполнения, блокирующие коллекции, замки и т.д., хорошо подходят Concurrent GCs:
Concurrent Mark Sweep
,Garbage-First
. Блокировки отдельных потоков проходят редко и почти незаметно. - если в коде преобладают мало-живущие объекты (т.е. которые живут очень недолго), а это предпочтительнее, то хорошо подойдут Copying GCs. Они отлично работают с immutable объектами и оптимизируют память.
- Там где возможно, полезно добавлять
@NotNull
или@Nullable
- Для объединения стримов используется
flatMap
:
1 | // 1,2,3 -> 0,1,2,1,2,3,2,3,4 |
- Использовать Java идиомы где это возможно
- Для рефакторинга старой системы можно использовать принцип наложения, при котором новый функционал сразу пишется по новым принципам, а старый постепенно замещается рефакторинговым кодом. При этом, рефакторинговый код должен вводиться в эксплуатацию.
- Рекомендации по API:
- Он должен быть интуитивно понятным по составу операций. Легко читаем и понятен даже без документации (хотя она и крайне рекомендуема)
- Выбирать правильные, понятным имена для методов
- Должно быть минималистично и включать в себя только то что реально будет востребовано
- Должно быть компактным и хорошо декомпозированным
- Аргументов немного, разного типа, максимально примитивных типов, принимать varargs где возможно
- На проекте должен должен быть “договор” о кодировании, который должны соблюдать все разработчики. Единый способ форматирования. Для этого существуют специальные утилиты, которые выполняют эти действия и следят за их соблюдением
- Рабочий код не должен содержать “заготовку на будущее”. Если есть какой-то функционал, который в перспективе станет частью приложения, его следует выносить в изолированные области (например, в отдельные VCS ветки)
- В рабочей версии приложения должен быть только используемый код
- Для обучения новых членов команды подходит “парное программирование”. Концепция обучения состоит в том что новый член команды должен уметь аргументировать выбранные решения
- Любую задачу всегда рассматриваем с точки зрения архитектуры - а зачем этот код? Какую задачу мы решаем? Есть ли более удачное решение?
И только ответив на эти вопросы переходим к анализу самого кода:
- Устраняем синтаксические ошибки
- Запустится ли приложение?
- c) Будет ли выполнять поставленную задачу?
- Для упрощения навигации можно объединять классы и метода посредством регистрации пустых аннотаций. С помощью таких маркеров, их можно будет легко найти.
- Одним из рекомендованных подходов к организации приложения является package-by-feature. При этом организация должна быть package-private:
- org.company.app.user.User
- org.company.app.user.UserRepository
- org.company.app.user.UserService
- org.company.app.user.UserUtils
- org.company.app.store.Product
- org.company.app.store.ProductRepository
- org.company.app.store.ProductRack
- org.company.app.store.ProductRackRepository
- org.company.app.store.Storage
- org.company.app.store.StorageService
Модификаторы доступов опускаются, тем самым классы ограничиваются пакетами. Наружу выставляются только критически важные методы и классы.
Выгода - уменьшение внешних зависимостей в рамках приложения
19. Необходимо:
a) Понимать как работает GC. В чем разница и анализировать код на основе этой информации
b) Использовать профайлер
c) Одни и те же кодировки в разных OS могут отличаться. Для реализации транспортабельных приложений необходимо понимать различия
d) Полезно понимание TCP/IP, равно как и протокола HTTP
20. На время разработки полезно включать логгирование запросов/ответов (HTTP, SQL, Streaming и прочих) в соответствующие лог файлы
21. Для ORM можно менять fetch size
(по ум. = 1) - это ускорит выполнение запросов, но может повлечь увеличение потребностей
22. one-to-many к many-to-one опасны. Лучше оставлять их @Lazy
23. При написании кода следует составлять его так чтобы читатель не упустил важные особенности, которые влияют на работу приложения:
1 | List<String> list = new ArrayList(initial); |
Инициализацию не всегда можно заметить. Так будет значительно нагляднее:
1 | List<String> list = new ArrayList(); |
- Если версия Java < 14 и не исползуется
Record
, то классам можно делать публичные свойства, но не забывать что они должны быть immutable!
1 | class Thing { |
- При выборе внешних библиотек следует обращать внимание на:
- Количество известных уязвимостей
- Количество участников в разработке
- Количество сторонних использований
- Частоту обновлений
- Методы, которые “скрывают” checked исключения желательно именовать единообразно. Так, распространенным именованием является Safe:
1 | public void doActionSafe() { |
При этом, метод, который выбрасывает checked исключение должен поменять модификатор доступа на более огрниченный.
- Булевые проверки должны быть короткими. Если провека слишком большая (из 3+ условий), то лучше разделить ее на группы и эти группы определить в методы с говорящими названиями.
- Когда это возможно, циклы
for-each
предпочтительнее нежелиfor
- они занимают меньше места и интуитивно понятнее. - Желательно избегать изменения коллекций во время их чтения (даже если они immutable). В особенности, изменение коллекций может ввести в заблуждение при наличии каких-либо условий для их изменений. Предпочтительнее будет создание новой коллеции, причем, в отдельном методе. Так же, в принципе, можно использовать
Iterator
с удалением - Необходимо избегать сложных вычислений в циклах, если их можно вычислить еще до цикла и использовать по необходимости внутри цикла. Например:
1 | for(String string : strings) { |
Вычисление Regex довольно тяжелая операция и в данном случае ее можно вынести за границы цикла - так она будет выполнена 1 раз:
1 | Pattern pattern = Pattern.compile(regex); |
Одной важной особенностью является то, что Pattern
не является потоко-безопасным объектом, а это значит что не всегда допустимо использовать его в качестве instance или static
переменной (что было бы еще лучше).
- Значительно лучше использовать
String.format
вместо конкатенации строк. - Если есть (проверенная) библиотека, которая выполняет какие-то действия, которые удовлетворяют требованиям, то лучше использовать ее, а не составлять код самостоятельно. Особенно, это касается стандартной Java API. Например:
1 | if(myObject == null) throw new NullPointerException("my object is null"); |
Перевести в:
1 | Objects.requireNotNull(myObject, "my object is null"); |
По такому же принципу - вместо калькуляции наличия объекта в коллеции через цикл, лучше использовать метод Collections.frequency
, который делает то же самое.
33. Если комментарии добавляются к каким-то сложным участкам кода и они там действительно нужны, зачастую бывает полезно наличие примеров данных, которые этим участком кода обрабатываются.
34. Если требуется перехват исключений, то лучше перехватывать более специфические исключения и выполнять для них какую-то особую обработку. Общий перехват типа catch(Exception)
достаточен только для тренировочных проектов когда нужно поставить “заглушку” от исключений. В особенности, нельзя перехватывать Throwable
и Error
!
35. Если исключение выбрасывается, необходимо дать к нему описание + полезно предоставлять данные в качестве instance переменных (но здесь важно передавать примитивы или те данные, которые не являются ресурсами и не будут их удерживать).
36. ClassCastException
довольно частая ошибка, так что перед преобразованием типа необходимо его проверять. Можно использовать if
(variable instanceof MyClass myVariable
) для читаемости кода
37. Пустой блок catch признак не самой лучшей обработки ошибки. Большинство API предоставляют возможность выполнения различных проверок, которые позволят избежать исключений. Даже если подобной проверки в API нет и ошибку придется перехватывать, блок catch
лучше заполнить хотя бы логгированием на низком уровне.
38. Реализация тестирования должна следовать паттерну given-when-then, при котором тестирование должно быть разбито на 3 блока. Для наглядности разбиения на блоки, можно воспользоваться переводом на новые строки:
1 | MyObject myObj = MyObject.initialise(); // given |
Behavious-Driven библиотеки (типа JGiven) предлагают требования к тестированию несколько выше.
- Хорошей практикой является добавление описания к тестам, например, с помощью аннотации
@DisplayName
- Вместо добавления нескольких тестов с различными параметрами
1 |
|
Значительно лучше переделать тест в параметризированный - в таком случае ошибка при проверке одного параметра не остановит проверки для других параметров:
1 |
|
- Тестирование методов, имеющих входные параметры (например, те что пользователи вводят самостоятельно), должны проверяться на различные значения:
1 | int: 0, 1, -1, Integer.MAX_VALUE, Integer.MIN_VALUE |
Ранее отмечалось, что наличие
boolean
в качестве параметра лучше избегать и в этом случае лучше разбивать метод на два (один дляtrue
значения, другой дляfalse
значения). То же самое относится и к опциональным параметрам - т.е. когда параметры могут приниматьnull
значения и это считается нормальным поведением - лучше разбить метод на два.Для лямбд следует чаще задействовать reduce операции (в частности,
count()
,sum()
,max()
и т.д.). Много полезных reduce операций содержится вCollectors
.Использование
Optional
не рекомендуется для instance переменныхВ рамках проекта следует выработать соглашение по форматированию кода. И перед добавлением его в VCS следует применять его.
Для учета ошибок в приложении можно воспользоваться расширениями ElasticSearch или подобными приложениями для контроля логов. Можно так же использовать специализированные утилиты: airbrake.io для серверной части и sentry.io для клиентской части приложения