JavaOpt: Code styling

JavaOpt: Code styling


Методы

  1. Чем меньше метод, тем лучше
  2. Добавление методов приветствуется
  3. Метод должен(!) выполнять 1 единственную задачу, которое можно понять из наименования метода. Маркером для выделения нового метода может быть то, что новый метод может иметь отличное название
  4. Должна читаться последовательность вызова методов - они должны быть как набор инструкций, выстроенных сверху-вниз, на конвейере (или как TODO)
  5. Длинные if-else и switch следует преобразовывать в фабрику, создающие соответствующие реализации:
1
2
3
4
5
6
switch (x.type) {
case TYPE_1: return createCalc1();
case TYPE_2: return createCalc2();
case TYPE_3: return createCalc3();
default: return defaultCalc();
}

Лучше переделать в:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Calc {
double calc();
}

class CalcFactory {
public static Calc buildCalc(CalcType type) {
switch(type) {
case TYPE_1: return new CalcType1();
case TYPE_2: return new CalcType2();
case TYPE_3: return new CalcType3();
default: return new RandomCalc();
}
}
}

Так же, крайне рекомендуется в случае примера выше (и подобных) использовать enum. Особенно, при ограничениях типизации средствами sealed классов.
6. Длинные имена методов (но информативные) - это нормально. Публичным (public) методам все же лучше давать короткие и читабельные.
7. У методов желательно наличие не более 3 параметров. Если требуется больше - лучше объединить их в неизменный (immutable) класс (Record предпочтительнее) с соответствующей реализацией.
8. Изменяемый (mutable) параметр (тот который может поменяться внутри метода) лучше в метод не передавать. В случае если метод все же как-то меняет его, то следует возвращать его в return - это хотя бы будет маркером, сигнализирующем что с объектом может что-то произойти внутри метода!
9. boolean в качестве параметра - плохая практика! Лучше разбить на 2 метода. Особенно плохо это для констукторов - в этом случае лучше использовать фабрики.
10. Иногда в имена методов можно добавить наименования параметров, например:

1
deleteUser(User user, User admin) -> deleteUserByAdmin(user, admin)

Так же хорошей практикой для примера выше будет выделение интерфейса, или наследование:

1
deleteUser(User user, Admin user)

Таким образом намного проще понять сигнатуру метода. Особенно эффективно выполнять такую модификацию в случае если у метода несколько параметров с одинаковым типом.
11. Результаты выполнения действий метода лучше хранить внутри объекта. Например, изменение баланса счета или изменение состояния датчика лучше изменять и хранить в самом объекте (а не в сервисе, который выполняет проксирующую функцию).
12. Наименование метод должен информировать что метод либо что-то делает, либо указывать на то что метод отвечает на какой-то вопрос, например:

1
boolean set(X x);

Из сигнатуры не понятно что данный метод делает, что он возвращает и для чего? Так, например, можно изменить:

1
2
boolean canSet(X x);
void set(X x)

Тогда вызов:

1
if(canSet(x)) set(x);

Впрочем, подобные случаи именования встречаются и в стандартных Java библиотеках.

  1. Вместо бесконечных if-else бывает лучше использовать выкидывание исключений - это позволит сильно сократить код (правда, небольшой ценой CPU и RAM)
  2. Блоки try-catch сильно загромождают код. Если исключение можно изменить, то рекомендуется наследовать его от RuntimeException. Если же модификация исключения не доступна, то лучше добавить private метод и внутри него обернуть метод с потенциальным исключением в try-catch.
  3. Зачастую лямбда-выражение сложно читать, если метод, который в нем вызывается, выкидывает checked-исключение - это влечет за собой необходимость добавления блока try-catch в рамках лямбда-выражения.
1
2
3
4
5
6
7
myList.foreach(item -> {
try {
executeMethod(item);
} catch (Exception ex) {
log.warn(...);
}
});

Для повышения читабельности кода, можно обернуть вызываемый метод в прокси метод, который перехватит checked исключение и выкинет RuntimeException. Часто подобные методы имеют такое же название, что и оригинал, но с постфиксом -Safe:

1
2
3
4
5
6
7
8
9
myList.foreach(this::executeMethodSafe);

private void executeMethodSafe(Obejct item) {
try {
executeMethod(item);
} catch (Exception ex) {
throw new MyCustomRuntimeException(ex);
}
}
  1. Вместо static методов лучше использовать полиморфизм. Это не касается Utility, Helper, фабрик и подобных классов

Классы

  1. Последовательность описания класса желательно соблюдать следующей:
  • public static переменные
  • private static переменные
  • static блоки
  • public -> protected -> private instance переменные
  • instance блоки
  • Конструкторы
  • public -> protected -> private методы в соответствии с очередностью вызова
  1. Метод лучше помечать как private. Если на метод необходимо наложить UnitTest, то достаточно изменить модификатор доступа на protected (целевой класс и тестовый класс должны располагаться в одном пакете)
  2. Желательно проектировать классы максимально минималистичными
  3. Класс должен отвечать 1 задаче - согласно SOLID, Solid Responsibility Principle
  4. В классе желательно сократить количество instance переменных. Чем активнее они будут использоваться - тем лучше
  5. Класс должен быть открыт к расширениям, но закрыт к изменениям - согласно SOLID, Open-Closed Principle
  6. Класс должен зависеть от абстракций, а не от реализаций - согласно SOLID, Depencency Inversion Principle
  7. Полиморфизм вместо if-else
  8. Весь хардкод желательно выносить в static переменные. В идеале - в настраиваемые параметры приложения, которые будут иметь значения по умолчанию, но можно будет изменить при старте

Объекты

  1. Выносить абстракции в интерфейсы. В интерфесах открывать только полезные методы, которые реально будут востребованы
  2. Согласно закон Деметры, следует соблюдать слабую связанность между сущностями - один класс общается только со знакомыми ему типами (классами), а не выполняет многоуровневые обращения к связанным объектам:
1
var owner = someObject.getCredit().getUser().getName();

Следуте переделать в:

1
var owner = someObject.getOwnerName();

Так же, если результат используется где-то в другом методе или классе, то, может, лучше сразу передать управление ему?
3. DTO. Объекты с переменными, но без методов. Из методов могу быть только setter/getter и конструкторы
4. Active Record - это DTO, расширенные навигационными методами, типа save, find, delete, … В них не хранится бизнес-логика. Методы лишь помощники в транспорте объектов (данных). Логику работы объектов лучше хранить на уровне сервисов.
5. Конструкторы максимально облегчить! В них не должны выполняться сложные вычисления и, тем более, обращения к внешним ресурсам. В конструкторы должны заходить только данные, а их подготовку можно вынести, например, в фабрику. Таким образом:

  • проще писать Unit-тесты
  • проще выполнять CDI инъекции
  • выделение памяти не страдает

Ошибки

  1. Все нестандартные поведения лучше оборачивать в исключения
  2. Избегать статусов в виде кодов, строк и т.п. Если работа метода отработала корректно - никакой ошибки; иначе выкидывать Exception
  3. checked исключения больше являются рудиментом Java. Лучше наследовать исключения от RuntimeException. Особенно это актуально для private и protected методов
  4. Исключения должны содержать подробную информацию о месте ошибки, причине, деталях, окружении и т.п.
    При этом, никаких ссылок на сложные объекты или user-типы, лучше не делать
  5. Лучше выстраивать иерархию ошибок (наследованием) - от более обобщенных, к более детализированным. Таким образом можно будет обрабатывать их группами
  6. При работе с внешними библиотеками удобно оборачивать внешние ошибки в собственные - это даст возможность с легкостью переходить с 1 библиотеки на другую
  7. Обработку ошибок выгодно перекладывать на уровень ниже, так чтобы абстракция сама могла решать что делать с ошибкой (например, выполнять какое-то действие по умолчанию).
1
2
3
4
5
6
7
8
byte[] bytes;
try {
bytes = Device.readAllBytes();
} catch (IOException | DeviceException e) {
log.warn(...);
...
bytes = device.getStandardResponseBytes();
}

Таким образом клиент метода всегда пользуется данными, а не обрабатывает ошибки. Можно обернуть в Optional, если какого-то действия по умолчанию быть не может. Данный паттерн именуется Special Case Pattern

  1. Метод не должен возвращать null - это приводит к NullPointerException, или дополнительным проверкам на null

Еще про именование

  1. Названия не должны включать аббревиатуры внешних сервисов, ресурсов, функциональных участков и т.п. Всегда есть более уместная читабельная форма записи (пусть даже она будет несколько более длинной) - смысл имени всегда должен быть понятен интуитивно
  2. Если в методе есть return, то необходимо (интуитивно) понимать что там может находиться и как это использовать
  3. В именах переменных или методов слова типа List, Map, Set, Array, Number, String и т.д. лучше применять только в тех случаях когда тип данных соответствует данной части наименования
  4. В именах переменных не следует писать *Var/*Variable - итак понятно что это переменная. Это же касается и методов (*Method/*Func) и классов (*Class/*Cl) и интерфейсов (*Interface/*Iface/*I). Это же применимо и к тестам - зачем именовать каждый test метод *Test?
  5. *Info, *Data не нужно писать в названиях классов. Лучше добавить пакет
  6. Все имена должны быть читабельными и произносимыми на английском
  7. Поиск (даже стандартными инструментами) по “приблизительному” имени должен давать результат. Необходимо максимально облегчить поиск и навигацию
  8. Никаких префиксов типа vUser, IUser, UserImpl, _username и т.д.
  9. i, j, k, l, m, n, … должны использоваться только в итераторах. Причем, только в числовых. Глубина блоков с итерациями так же имеют некоторые оговорки
  10. Имена классов - существительные, а методов - глаголы (исключения возможны, например для Builder Pattern)
  11. Часто создание объектов удобно выносить в static метод, особенно если имплементация зависит от параметров. Можно так же использовать Builder Pattern, но, в основном, для больших классов
  12. Для однотипных методов лучше заводить однотипные наименования для всего приложения. В противном случае, могут возникать вопросы, например:
1
2
void harvest() {}     //  сбор
void collect() {} // сбор

В чем между ними разница?

1
2
void fetch() {}       //  получение
void retrive() {} // получение

В чем между ними разница?
Это же правило касается и классов.

  1. Часто использование технических или околотехнических терминов поможет в понимании назначения метода или переменной, например: ReportQueue, UserRoleMatrix. Конечно, если имена имеют подобную особенность, они должны оправдывать ожидания
  2. Переменные можно объединять в группы просто именованием: userName, userCity, userPhone, userCaption, userAddress, companyCaption, companyRegistrationDate, companyAddress и т.д. Если группа очень большая следует завести отдельный класс
  3. Если очевидно что классы группируются по какому-то признаку лучше вывести их в отдельный пакет
  4. Если имена в разных областых видимости (scope’ах) совпадают у переменных, то для них лучше завести новые имена, описывающие их уникальным образом. Иначе полиморфизм может лишь усложнить жизнь

Комменты

  1. Много комментов нужно только плохому коду
  2. Вместо комментов лучше нормально и информативно именовать методы. А так же следать декомпозицию больших методов
  3. TODO в комментах всегда полезны!
  4. Если действие в коде неочевидно, то лучше оставить комментарий - почему код именной такой
  5. Для публичных библиотек надо оставлять JavaDocs. Хотя бы для public методов
  6. Коммент следует оставлять не “для себя”, а как способ донести неочевидность кода до другого разработчика (если кодом это сделать невозможно)
  7. Комменты должны быть по месту и точечно! Их переизбыток мешает!
  8. Секции кода не следует дробить комментариями типа
1
2
3
/************************** Device monitoring
/////////////////////////////////////// Sensor advanced methods
// -------------------------------- Network adapters -----------
  1. Удаление кода не следует комментировать! Для этого есть VCS (Git, SVN, Mercurial и т.п.)
  2. Комментировать следует только рядом стоящий код. Не следует комментировать что будет через 10 строк, или то что было в предыдущем методе на 7 строк выше
  3. Комментарий короткий и четкий
  4. Если после прочтения комментария возник вопрос (или даже несколько) - это плохой комментарий!
  5. Комментарии быстро устаревают и их редко меняют. По-этому лучше их избегать или, хотя бы, вовремя удалять
  6. Комментарии должны содержать только техническую информацию по реализации, никакой бизнес-логики
  7. Перед комментарием следует дважды подумать - а стоит ли оно того?!

Форматирование

  1. Рекомендуемая длинна файла - до 200 строк. Рекомендуемая длинна строки - 120 символов
  2. В верхней части файла должна содержаться макс. Полезная информация, раскрывающая задачи файла.
    Если это *.java файл, то наименование файла (а следовательно и главного класса) должно описывать задачи всего файла.
    Посредством пакетов можно успешно группировать классы и модули.
    Чем больше детализации по задаче (файла), том ниже должен быть расположен блок (вложенный класс или метод)
  3. Код надо рассматривать как газету - в заголовке максимально важная информация, описывающая тематику (назначение). Важные блоки лучше разбивать пробелами
  4. Определение переменной максимально близко к первому использованию. Так, если методы будут маленькие, то и переменные будут в верхней их части
  5. Вызываемые методы должны быть максимально близко к месту их вызова. В действительности, желательно сразу после их первого вызова
  6. Позитивные if-else читать значительно проще
  7. “Мертвые” методы и код следует удалять. Для истории есть VCS
  8. В *.java файле только Java код! Все остальное выносить во внешние ресурсы
  9. Избегать дублирования. Даже неочевидного, типа: Optional::isPresent / Optional::isEmpty
  10. Инкременты лучше не дублировать:
1
if(x + 1 > 0) print(x + 1);

Заменить на:

1
2
int nextX = x + 1;
if(nextX > 0) print(nextX);
  1. Enum предпочтительнее констант
 Comments
Comment plugin failed to load
Loading comment plugin