JavaOpt: Общие рекомендации

JavaOpt: Общие рекомендации


  1. Сборка должна выполняться быстро. Равно как и тесты - в один клик - если они будут отрабатывать долго никто их выполнять не будет
  2. Сборки должна быть универсальны. Тестирование желательно на максимально возможном и разнообразном окружении
  3. Частые мелкие изменения предпочтительнее
  4. Блоки типа:
1
2
3
Thing thing = Thing.DEFAULT;
if(predicate.test(...)) thing = Thing.ANOTHER;
thing.doSomething();

Или

1
2
3
4
Thing thing;
if(predicate.test(...)) thing = ...;
else thing = ...;
thing.doSomething();

лучше избегать. И преобразовать в условие типа:

1
2
Thing thing = predicate.test(...) ? ... : ...;
thing.doSomething();

Или даже так:

1
generateThing().doSomething();
  1. Для анализа бутылочного горлышка следует использовать утилиты для профилирования типа Honest Profiler, async-profiler, perf-map-agent. Они строят Flame-графы, по которым хорошо видны участки с задержками в работе приложения.
    Так же можно воспользоваться JProfiler, YourKit и т.д.
  2. Предпочтения по GC:
  • там где используется большое количество многопоточного исполнения, блокирующие коллекции, замки и т.д., хорошо подходят Concurrent GCs: Concurrent Mark Sweep, Garbage-First. Блокировки отдельных потоков проходят редко и почти незаметно.
  • если в коде преобладают мало-живущие объекты (т.е. которые живут очень недолго), а это предпочтительнее, то хорошо подойдут Copying GCs. Они отлично работают с immutable объектами и оптимизируют память.
  1. Там где возможно, полезно добавлять @NotNull или @Nullable
  2. Для объединения стримов используется flatMap:
1
2
// 1,2,3 -> 0,1,2,1,2,3,2,3,4
Stream.of(1, 2, 3).flatMap(x -> Stream.of(x - 1, x, x + 1).collect(Collectors.toList());
  1. Использовать Java идиомы где это возможно
  2. Для рефакторинга старой системы можно использовать принцип наложения, при котором новый функционал сразу пишется по новым принципам, а старый постепенно замещается рефакторинговым кодом. При этом, рефакторинговый код должен вводиться в эксплуатацию.
  3. Рекомендации по API:
  • Он должен быть интуитивно понятным по составу операций. Легко читаем и понятен даже без документации (хотя она и крайне рекомендуема)
  • Выбирать правильные, понятным имена для методов
  • Должно быть минималистично и включать в себя только то что реально будет востребовано
  • Должно быть компактным и хорошо декомпозированным
  • Аргументов немного, разного типа, максимально примитивных типов, принимать varargs где возможно
  1. На проекте должен должен быть “договор” о кодировании, который должны соблюдать все разработчики. Единый способ форматирования. Для этого существуют специальные утилиты, которые выполняют эти действия и следят за их соблюдением
  2. Рабочий код не должен содержать “заготовку на будущее”. Если есть какой-то функционал, который в перспективе станет частью приложения, его следует выносить в изолированные области (например, в отдельные VCS ветки)
  3. В рабочей версии приложения должен быть только используемый код
  4. Для обучения новых членов команды подходит “парное программирование”. Концепция обучения состоит в том что новый член команды должен уметь аргументировать выбранные решения
  5. Любую задачу всегда рассматриваем с точки зрения архитектуры - а зачем этот код? Какую задачу мы решаем? Есть ли более удачное решение?
    И только ответив на эти вопросы переходим к анализу самого кода:
  • Устраняем синтаксические ошибки
  • Запустится ли приложение?
  • c) Будет ли выполнять поставленную задачу?
  1. Для упрощения навигации можно объединять классы и метода посредством регистрации пустых аннотаций. С помощью таких маркеров, их можно будет легко найти.
  2. Одним из рекомендованных подходов к организации приложения является 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
2
List<String> list = new ArrayList(initial);
list.add(...)

Инициализацию не всегда можно заметить. Так будет значительно нагляднее:

1
2
3
List<String> list = new ArrayList();
list.addAll(initial);
list.add(...);
  1. Если версия Java < 14 и не исползуется Record, то классам можно делать публичные свойства, но не забывать что они должны быть immutable!
1
2
3
4
5
6
7
8
9
10
11
12
13
class Thing {
public final XObject x;
public final YObject y;
public Thing(XObject x, YObject y) {...}
}

class XObject {
public final String x;
}

class YObject {
public final boolean y;1
}
  1. При выборе внешних библиотек следует обращать внимание на:
  • Количество известных уязвимостей
  • Количество участников в разработке
  • Количество сторонних использований
  • Частоту обновлений
  1. Методы, которые “скрывают” checked исключения желательно именовать единообразно. Так, распространенным именованием является Safe:
1
2
3
4
5
6
7
public void doActionSafe() {
try{
doAction();
} catch(Exception e) { ... }
}

private void doAction() throws Exception {...}

При этом, метод, который выбрасывает checked исключение должен поменять модификатор доступа на более огрниченный.

  1. Булевые проверки должны быть короткими. Если провека слишком большая (из 3+ условий), то лучше разделить ее на группы и эти группы определить в методы с говорящими названиями.
  2. Когда это возможно, циклы for-each предпочтительнее нежели for - они занимают меньше места и интуитивно понятнее.
  3. Желательно избегать изменения коллекций во время их чтения (даже если они immutable). В особенности, изменение коллекций может ввести в заблуждение при наличии каких-либо условий для их изменений. Предпочтительнее будет создание новой коллеции, причем, в отдельном методе. Так же, в принципе, можно использовать Iterator с удалением
  4. Необходимо избегать сложных вычислений в циклах, если их можно вычислить еще до цикла и использовать по необходимости внутри цикла. Например:
1
2
3
4
5
for(String string : strings) {
if(Patter.matches(regex, string)) {
...
}
}

Вычисление Regex довольно тяжелая операция и в данном случае ее можно вынести за границы цикла - так она будет выполнена 1 раз:

1
2
3
4
5
6
Pattern pattern = Pattern.compile(regex);
for(String string : strings) {
if(pattern.matcher(string).match()) {
...
}
}

Одной важной особенностью является то, что Pattern не является потоко-безопасным объектом, а это значит что не всегда допустимо использовать его в качестве instance или static переменной (что было бы еще лучше).

  1. Значительно лучше использовать String.format вместо конкатенации строк.
  2. Если есть (проверенная) библиотека, которая выполняет какие-то действия, которые удовлетворяют требованиям, то лучше использовать ее, а не составлять код самостоятельно. Особенно, это касается стандартной 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
2
3
4
5
MyObject myObj = MyObject.initialise();  // given

myObj.doSomeActions(); // when

Assertions.assertTrue(myObject.hasPositiveEffects()); // then

Behavious-Driven библиотеки (типа JGiven) предлагают требования к тестированию несколько выше.

  1. Хорошей практикой является добавление описания к тестам, например, с помощью аннотации @DisplayName
  2. Вместо добавления нескольких тестов с различными параметрами
1
2
3
4
5
6
7
8
@Test
public void isValidAge() {
int[] ages = new int[] {10, 19, 63};
for(int age : ages) {
Person person = new Person("test person", age);
assertThat(person.getAge(), greaterThan(18));
}
}

Значительно лучше переделать тест в параметризированный - в таком случае ошибка при проверке одного параметра не остановит проверки для других параметров:

1
2
3
4
5
6
@ParametrizedTest(name = ”{#index} test of person age {0}”)
@ValueSource(ints = {10, 19, 63})
public void isValidAge(int age) {
Person person = new Person("test person", age);
assertThat(person.getAge(), greaterThan(18));
}
  1. Тестирование методов, имеющих входные параметры (например, те что пользователи вводят самостоятельно), должны проверяться на различные значения:
1
2
3
4
5
int: 0, 1, -1, Integer.MAX_VALUE, Integer.MIN_VALUE
double : 0, 1.0, -1.0, Double.MAX_VALUE, Double.MIN_VALUE
Object[]: null, {}, {null}, {new Object(), null}
List<Object> : null, Collections.emptyList(), Collections.singletonList(null) , Arrays.asList(new Object(), null)
String: null, "", " ", " value ", "a\nt͡ ɬɪŋɑn\nb"
  1. Ранее отмечалось, что наличие boolean в качестве параметра лучше избегать и в этом случае лучше разбивать метод на два (один для true значения, другой для false значения). То же самое относится и к опциональным параметрам - т.е. когда параметры могут принимать null значения и это считается нормальным поведением - лучше разбить метод на два.

  2. Для лямбд следует чаще задействовать reduce операции (в частности, count(), sum(), max() и т.д.). Много полезных reduce операций содержится в Collectors.

  3. Использование Optional не рекомендуется для instance переменных

  4. В рамках проекта следует выработать соглашение по форматированию кода. И перед добавлением его в VCS следует применять его.

  5. Для учета ошибок в приложении можно воспользоваться расширениями ElasticSearch или подобными приложениями для контроля логов. Можно так же использовать специализированные утилиты: airbrake.io для серверной части и sentry.io для клиентской части приложения

 Comments
Comment plugin failed to load
Loading comment plugin