W3docs

Принципы чистого кода в Java

Принципы чистого кода в Java: короткие методы, понятные имена, единственная ответственность и минимальная изменяемость.

Чистый код — это код, который следующий человек (часто вы сами, спустя полгода) сможет прочитать, понять и изменить без страха. Компилятор принимает почти что угодно; чистый код обращён к другой аудитории — людям, которые его сопровождают. Ни один из принципов ниже не является изобретением, специфичным для Java, но Java предоставляет конкретные инструменты — короткие методы, поля final, записи, исключения, осмысленные типы — для их применения. В этой главе разобраны те из них, которые приносят пользу каждый день.

Имена, раскрывающие намерение

Хорошее имя отвечает на вопрос зачем существует значение, а не только какой у него тип. Если имя требует комментария для объяснения, значит, имя выбрано неверно. Избегайте одиночных букв (кроме коротких циклов), избегайте аббревиатур, понятных только вам, и предпочитайте длинное описательное имя короткому и загадочному — IDE всё равно автодополнит его.

// Unclear: what is d? what unit? what is the magic 86400000?
int d = (t2 - t1) / 86400000;

// Clear: the names and a constant carry the meaning
long MILLIS_PER_DAY = 24L * 60 * 60 * 1000;
long elapsedDays = (endMillis - startMillis) / MILLIS_PER_DAY;

boolean-переменные лучше всего читаются как вопросы или состояния: isActive, hasNext, shouldRetry. Методы, которые что-то делают, получают имена-глаголы (calculateTotal); методы, которые что-то возвращают, получают имена-существительные или геттеры (total, getTotal). Для изучения языковых соглашений, стоящих за этими выборами, смотрите соглашения об именовании в Java и лучшие практики именования в Java.

Короткие методы с единственной ответственностью

Метод должен делать одно дело на одном уровне абстракции. Когда вы ловите себя на написании комментария вроде // now validate the input, этот блок обычно просится стать отдельным, хорошо именованным методом. Короткие методы проще именовать, тестировать и повторно использовать — а место вызова читается как предложение.

// Before: one method juggling validation, calculation, and formatting
String receipt(Order order) {
  if (order == null || order.items().isEmpty())
    throw new IllegalArgumentException("empty order");
  int total = 0;
  for (var i : order.items()) total += i.price() * i.qty();
  return "Total: $" + total / 100 + "." + (total % 100);
}

// After: each step is a named method; receipt() now reads top-down
String receipt(Order order) {
  requireNonEmpty(order);
  int total = totalCents(order);
  return formatCents(total);
}

Защитные предложения сохраняют методы плоскими. Обрабатывайте граничные случаи и возвращайте управление досрочно, вместо того чтобы обёртывать основной путь в всё углубляющиеся блоки if.

Предпочитайте неизменяемость и минимальную изменяемость

Изменяемое разделяемое состояние — это корень большинства проблем с параллелизмом и многих просто непонятных ошибок. По умолчанию используйте поля и локальные переменные final; ослабляйте это ограничение только при наличии веской причины. Записи (records) в Java делают неизменяемые объекты-значения однострочными, генерируя канонический конструктор, equals, hashCode и toString.

// An immutable value object with validation in the compact constructor
record Money(long cents, String currency) {
  Money {
    if (cents < 0) throw new IllegalArgumentException("cents must be >= 0");
  }
  Money plus(Money other) { return new Money(cents + other.cents, currency); }
}

Обратите внимание: plus возвращает новый объект Money, а не изменяет this. Неизменяемые объекты безопасно передавать между потоками, безопасно использовать в качестве ключей Map и невозможно повредить после создания. Чтобы погрузиться глубже, смотрите Java records, неизменяемые классы и лучшие практики неизменяемости; раздел ключевое слово final описывает блокировку полей и переменных.

Падайте быстро и используйте исключения, а не коды ошибок

Проверяйте аргументы на границе и бросайте исключение немедленно, когда что-то не так, — чтобы сбой проявился рядом со своей причиной, а не тремя слоями глубже в виде запутанного NullPointerException. Используйте исключения для сигнализации об ошибках; не возвращайте null или магические сторожевые значения, о проверке которых должен помнить каждый вызывающий код.

ПаттернИзбегайтеПредпочтите
Отсутствующее значениеreturn null;Optional<T> или throw
Неверный аргументreturn -1;throw new IllegalArgumentException(...)
Невозможное состояниетихое умолчаниеthrow new IllegalStateException(...)
Освобождение ресурсовручной finallytry-with-resources
static User findUser(String id) {
  Objects.requireNonNull(id, "id must not be null");   // fail fast
  return repository.lookup(id)
      .orElseThrow(() -> new NoSuchElementException("no user: " + id));
}

Objects.requireNonNull превращает расплывчатый NPE где-то в глубине в точное сообщение прямо на входе. Чтобы смоделировать «может отсутствовать» без null, читайте Java Optional; для проектирования типов ошибок смотрите лучшие практики исключений и пользовательские исключения.

DRY, но без избыточных абстракций

DRY (Don't Repeat Yourself) означает, что единица знания живёт в одном месте. Когда одна и та же константа или вычисление встречается дважды, извлеките их. Но не делайте противоположную ошибку: два фрагмента, которые сегодня выглядят одинаково, завтра могут разойтись. Дублированный код дешевле исправить, чем неверную абстракцию. Извлекайте, когда смысл общий, а не просто синтаксис.

// Knowledge duplicated: the threshold lives in two places
if (order.total() >= 5000) freeShip = true;     // here
if (cart.total() >= 5000) showBadge = true;     // and here

// One source of truth
static final int FREE_SHIPPING_THRESHOLD_CENTS = 5000;
boolean qualifiesForFreeShipping(int totalCents) {
  return totalCents >= FREE_SHIPPING_THRESHOLD_CENTS;
}

Практический пример: чистая корзина покупок

Эта программа объединяет принципы в одном небольшом запускаемом файле: неизменяемая запись LineItem с самовалидацией, однофункциональные методы с именами, раскрывающими намерение, именованные константы вместо магических чисел, защитное предложение и равенство по значению. Никаких комментариев для объяснения что это делает — имена сами по себе это объясняют.

java— editable, runs on the server

Что стоит вынести из запуска:

  • Вывод читается точно так же, как моделируемая предметная область — 2 x Notebook @ $12.50 = $25.00 — потому что каждый метод и поле названы по своему назначению. Вам не понадобился ни один комментарий, чтобы проследить математику корзины; имена, раскрывающие намерение, сами выполнили роль документации.
  • Промежуточная сумма составляет $30.97, а доставка — $5.99, а не бесплатно: защитное предложение shippingCents сравнило промежуточную сумму с именованной константой FREE_SHIPPING_THRESHOLD_CENTS (5000), а 3097 меньше неё. Магическое число живёт ровно в одном месте, поэтому правило невозможно сделать противоречивым.
  • value equality: true доказывает, что record предоставил нам equals по значению бесплатно — два отдельно созданных объекта Pen равны, потому что равны их данные. Написание этой пары equals/hashCode вручную — это шаблонный код, который вам больше не нужно поддерживать.
  • rejected bad item: quantity must be >= 0 демонстрирует принцип быстрого отказа в действии: компактный конструктор проверил аргумент и бросил IllegalArgumentException в момент создания, поэтому значение -1 для quantity никогда не может войти в систему как тихие плохие данные.
  • Каждый вспомогательный метод — subtotalCents, shippingCents, formatCents — делает одно дело, поэтому main читается сверху вниз как история. Короткие методы с единственной ответственностью — вот что делает всю программу просматриваемой, а не стеной вложенной логики.

Практика

Практика
Почему создание 'LineItem' как записи с валидацией в компактном конструкторе считается чище, чем обычный изменяемый класс с сеттерами?
Почему создание 'LineItem' как записи с валидацией в компактном конструкторе считается чище, чем обычный изменяемый класс с сеттерами?
Was this page helpful?