Код читают чаще, чем пишут: одну строчку напишут раз, а перечитают её при каждой правке рядом — свои же и коллеги, через полгода и без контекста. Clean Code — набор привычек Роберта Мартина о том, как писать так, чтобы этот второй читатель понял код без археологических раскопок. Это не про красоту ради красоты, а про стоимость изменений: чем понятнее код, тем дешевле его менять.
Разберём главные привычки — не как догму, а как ответы на боль «я не понимаю, что тут происходит».
Каждый кусок, который приходилось подписывать комментарием, уезжает в свою функцию с этим же именем. Сама handleOrder остаётся оглавлением из четырёх вызовов — её читают сверху вниз и не проваливаются в детали.
Имена, которые не надо расшифровывать
Имя — самый частый комментарий в коде. Хорошее имя объясняет смысл без пояснений:
// было — что такое d? в днях? в чём?
int d = (now - created) / 86400;
// стало — имя отвечает на вопрос само
int daysSinceRegistration = (now - created) / SECONDS_IN_DAY;
Несколько правил, которые сразу дают эффект:
- Имя раскрывает намерение.
list→activeUsers,flag→isEmailConfirmed,data→orderPayload. - Без загадочных сокращений.
calcTot()экономит четыре буквы и стоит секунды на каждое чтение;calculateTotal()— не стоит. - Длина под область видимости. Счётчик в трёхстрочном цикле может быть
i; поле класса, живущее по всему модулю, — нет. - Единый словарь. Если в одном месте
user, в другомcustomer, а в третьемclient— читатель гадает, это одно и то же или нет. Договоритесь об одном термине — в DDD это Ubiquitous Language.
Функция делает одно
Правило одно: одна функция — одно дело, на одном уровне абстракции. Функция, которая и достала данные, и посчитала, и отправила письмо, — три функции, слипшиеся в одну.
// так не надо: валидация, скидка, запись в БД и письмо — в одном теле
void handleOrder(Order o) { ... }
Та же обработка по шагам: каждый шаг проверяют и меняют отдельно.
живой пример
public class OrderFlow {
record Order(String id, int amount) {}
public static void main(String[] args) {
handleOrder(new Order("ord-042", 1500));
}
static void handleOrder(Order o) {
validate(o);
int total = applyDiscount(o);
save(o, total);
notifyCustomer(o, total);
}
static void validate(Order o) {
if (o.amount() <= 0) throw new IllegalArgumentException("сумма должна быть больше нуля");
}
static int applyDiscount(Order o) {
return o.amount() >= 1000 ? o.amount() - o.amount() / 10 : o.amount();
}
static void save(Order o, int total) {
System.out.println("сохранили " + o.id() + ", к оплате " + total);
}
static void notifyCustomer(Order o, int total) {
System.out.println("письмо: заказ " + o.id() + " на сумму " + total);
}
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Неделя бесплатно →
Признаки, что функцию пора делить: она не помещается на экран, у неё несколько уровней вложенности if, приходится писать комментарий-заголовок вроде // теперь считаем скидку — этот комментарий и есть имя будущей функции.
Флаг-аргумент: send(message, true) — что значит true? Булев параметр обычно означает, что функция делает две разные вещи; чаще честнее две функции — sendNow(message) и scheduleSend(message).
Комментарии: почему, а не что
Хороший комментарий объясняет то, что код сказать не может, — почему так сделано. Плохой пересказывает что делает код, дублируя его, и со временем начинает врать: код поправили, комментарий забыли.
// плохо: пересказ кода — устареет при первой правке
// увеличиваем счётчик на 1
counter++;
// хорошо: объясняет неочевидное «почему»
// Повторяем ровно 3 раза: у платёжного шлюза лимит 3 попытки на идемпотентный ключ.
retry(3, () -> gateway.charge(token));
Лучший комментарий — тот, который удалось не писать: тянет пояснить кусок — сначала вынесите его в функцию с говорящим именем. Комментарии оправданы для «почему», для предупреждений о неочевидных последствиях и для публичного API. Закомментированный «на всякий случай» код — не комментарий, а мусор: для истории есть git.
Форматирование и структура
Читаемость — это и визуальный порядок. Связанные строки держат рядом, пустой строкой отделяют смысловые блоки, вложенность держат неглубокой. Глубокие if внутри if разворачивают ранним выходом: неподходящие случаи отсекают сверху, а основная логика остаётся слева, без лесенки.
живой пример
public class Guards {
record User(String name, boolean active) {}
public static void main(String[] args) {
User[] users = {new User("Анна", true), new User("Иван", false), null};
for (User u : users) {
System.out.println(nested(u) + " | " + guarded(u));
}
}
static String nested(User u) {
if (u != null) {
if (u.active()) {
return "пишем " + u.name();
}
}
return "пропускаем";
}
static String guarded(User u) {
if (u == null) return "пропускаем";
if (!u.active()) return "пропускаем";
return "пишем " + u.name();
}
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Неделя бесплатно →
Обе отвечают одинаково — это видно в выводе. Разница в том, сколько условий читатель держит в голове, дойдя до главной строки.
Единый стиль в проекте держит автоформаттер, а не споры на ревью.
Дублирование и границы
DRY (Don't Repeat Yourself): одно знание живёт в одном месте. Три скопированных блока с расчётом цены — это три места, где завтра забудут поправить налог. Но и здесь без фанатизма: два внешне похожих куска, которые меняются по разным причинам, — не дубликат, и склеивать их вредно.
Работу с внешним миром — базой, чужим API, файлами — прячут за тонкой границей (репозиторий, клиент), чтобы детали не растекались по коду: тогда замена библиотеки или базы затрагивает одно место, а не половину проекта.
Когда чистота превращается в догму
Clean Code — ориентиры, а не свод законов. Любой можно довести до абсурда:
- Дробление на функции по две строки, когда логика читается лучше в одной, — уже не ясность, а прыжки по файлу.
- «Ни одного комментария» — плохо ровно так же, как «комментарий на каждой строке»: неочевидное «почему» должно быть записано.
Ориентир простой: чистый код — тот, при чтении которого следующий человек не задаёт лишних вопросов. Правило помогает этому — применяйте; читать стало труднее — правило проиграло.
Коротко
- Код читают чаще, чем пишут; чистота — это про стоимость будущих изменений, а не про эстетику.
- Имена раскрывают намерение и не требуют расшифровки; единый словарь важнее краткости.
- Функция делает одно на одном уровне абстракции; булев флаг-аргумент — обычно сигнал разделить её надвое.
- Комментарий объясняет «почему», а не пересказывает «что»; лучший комментарий заменяется выразительным именем.
- Ранние выходы вместо глубокой вложенности, единый формат — на автоформаттере; DRY — одно знание в одном месте, но без склейки того, что меняется по разным причинам.
- Всё это — ориентиры, а не догма: правило, из-за которого код читать труднее, проиграло.
Что почитать дальше
- SOLID: пять принципов проектирования — как чистые функции складываются в чистую структуру классов.
- GRASP: кто за что отвечает — принципы распределения ответственности между объектами.
- DRY, KISS, YAGNI и другие принципы — где у DRY проходит граница и почему простое решение обычно выигрывает.
- Паттерны GoF — готовые решения типичных задач проектирования.