Принципы чистого кода в 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(...) |
| Освобождение ресурсов | ручной finally | try-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 с самовалидацией, однофункциональные методы с именами, раскрывающими намерение, именованные константы вместо магических чисел, защитное предложение и равенство по значению. Никаких комментариев для объяснения что это делает — имена сами по себе это объясняют.
Что стоит вынести из запуска:
- Вывод читается точно так же, как моделируемая предметная область —
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читается сверху вниз как история. Короткие методы с единственной ответственностью — вот что делает всю программу просматриваемой, а не стеной вложенной логики.