Clean Code
Clean Code — код, который легко читать, понимать и изменять. Чистый код выглядит так, будто его автор заботился о качестве.
Зачем нужно
Код читают в 10 раз чаще, чем пишут. Чистый код снижает когнитивную нагрузку, ускоряет onboarding новых разработчиков, уменьшает количество багов и делает рефакторинг безопасным.
Где используется
- Повседневное написание кода
- Code review
- Рефакторинг legacy-кода
- Командная разработка
Предпосылки
Именование
Переменные — существительные, описывающие данные
// ПЛОХО
const d = new Date();
const list = getUsers;
const flag = true;
const temp = calcPrice(items);
// ХОРОШО
const currentDate = new Date();
const activeUsers = getActiveUsers;
const isLoggedIn = true;
const totalPrice = calculateTotalPrice(cartItems);
Функции — глаголы, описывающие действие
// ПЛОХО
function data() { /* ... */ }
function process(x) { /* ... */ }
function handle() { /* ... */ }
// ХОРОШО
function fetchUserProfile(userId) { /* ... */ }
function calculateShippingCost(order) { /* ... */ }
function validateEmailFormat(email) { /* ... */ }
Булевы — вопрос с ответом да/нет
// ПЛОХО
const open = true;
const status = false;
const read = true;
// ХОРОШО
const isOpen = true;
const hasPermission = false;
const wasRead = true;
const canEdit = user.role === 'admin';
Константы — UPPER_SNAKE_CASE
const MAX_RETRY_COUNT = 3;
const API_BASE_URL = 'https://api.example.com';
const DEFAULT_PAGE_SIZE = 20;
const MILLISECONDS_PER_DAY = 86_400_000;
Функции
Размер функции
Функция должна делать одну вещь и помещаться на экране (до ~20 строк).
// ПЛОХО: функция делает 5 вещей
async function registerUser(data) {
// 1. Валидация
if (!data.email) throw new Error('Email required');
if (!data.password) throw new Error('Password required');
if (data.password.length < 8) throw new Error('Password too short');
if (!/[A-Z]/.test(data.password)) throw new Error('Need uppercase');
if (!/[0-9]/.test(data.password)) throw new Error('Need number');
// 2. Проверка дубликата
const existing = await db.users.findOne({ email: data.email });
if (existing) throw new Error('Email taken');
// 3. Хэширование
const salt = await bcrypt.genSalt(10);
const hash = await bcrypt.hash(data.password, salt);
// 4. Сохранение
const user = await db.users.create({
email: data.email,
password: hash,
createdAt: new Date
});
// 5. Отправка email
await mailer.send({
to: user.email,
subject: 'Добро пожаловать',
html: `<h1>Привет, ${user.email}!</h1>`
});
return user;
}
// ХОРОШО: каждая функция — одна задача
async function registerUser(data) {
validateRegistrationData(data);
await ensureEmailNotTaken(data.email);
const hashedPassword = await hashPassword(data.password);
const user = await createUser(data.email, hashedPassword);
await sendWelcomeEmail(user);
return user;
}
function validateRegistrationData({ email, password }) {
if (!email) throw new ValidationError('Email обязателен');
if (!password || password.length < 8) {
throw new ValidationError('Пароль: минимум 8 символов');
}
}
async function ensureEmailNotTaken(email) {
const existing = await db.users.findOne({ email });
if (existing) throw new ConflictError('Email уже зарегистрирован');
}
Количество аргументов
Идеально: 0-2. Допустимо: 3. Больше — используйте объект.
// ПЛОХО: 5 аргументов — невозможно запомнить порядок
function createUser(name, email, age, role, isActive) { /* ... */ }
createUser('Alice', 'alice@mail.com', 30, 'admin', true);
// ХОРОШО: объект с именованными свойствами
function createUser({ name, email, age, role = 'user', isActive = true }) {
// ...
}
createUser({ name: 'Alice', email: 'alice@mail.com', age: 30, role: 'admin' });
Форматирование
// Единообразный стиль (используйте Prettier/ESLint)
// Вертикальная группировка — связанный код вместе
// Импорты
import { validateEmail } from './validators.js';
import { UserRepository } from './repositories.js';
import { sendEmail } from './email.js';
// Константы
const MAX_LOGIN_ATTEMPTS = 5;
const LOCKOUT_DURATION_MS = 15 * 60 * 1000;
// Основная логика
export async function loginUser(email, password) {
const user = await UserRepository.findByEmail(email);
if (!user) throw new AuthError('Неверные учётные данные');
if (isLockedOut(user)) {
throw new AuthError('Аккаунт заблокирован. Попробуйте позже');
}
const isValid = await verifyPassword(password, user.passwordHash);
if (!isValid) {
await incrementFailedAttempts(user);
throw new AuthError('Неверные учётные данные');
}
await resetFailedAttempts(user);
return generateToken(user);
}
// Вспомогательные функции
function isLockedOut(user) {
return (
user.failedAttempts >= MAX_LOGIN_ATTEMPTS &&
Date.now() - user.lastFailedAt < LOCKOUT_DURATION_MS
);
}
Code Smells (запахи кода)
| Запах | Проблема | Решение |
|---|---|---|
| Магические числа | if (status === 3) — что такое 3? |
Константа: STATUS_ACTIVE = 3 |
| Длинная функция | >30 строк, делает много вещей | Разбить на подфункции |
| Длинный список параметров | fn(a, b, c, d, e) |
Объект параметров |
| Вложенные условия | 4+ уровня if/else | Guard clauses, early return |
| Комментарий вместо рефакторинга | // Это делает X |
Вынести в функцию doX |
| Мёртвый код | Закомментированный код | Удалить (есть git) |
| Повторяющийся код | Copy-paste | Вынести в функцию (DRY) |
| God Object | Класс на 500+ строк | Разделить по SOLID SRP |
// Запах: вложенные условия
function getPrice(user, product) {
if (user) {
if (user.isPremium) {
if (product.onSale) {
return product.price * 0.7;
} else {
return product.price * 0.9;
}
} else {
if (product.onSale) {
return product.price * 0.85;
} else {
return product.price;
}
}
}
return product.price;
}
// Рефакторинг: guard clause + таблица решений
function getDiscount(user, product) {
if (!user) return 0;
const discounts = {
premium_sale: 0.30,
premium_regular: 0.10,
regular_sale: 0.15,
regular_regular: 0,
};
const tier = user.isPremium ? 'premium' : 'regular';
const sale = product.onSale ? 'sale' : 'regular';
return discounts[`${tier}_${sale}`] || 0;
}
function getPrice(user, product) {
const discount = getDiscount(user, product);
return product.price * (1 - discount);
}
Рефакторинг: основные приёмы
// 1. Extract Function — вынести блок в функцию
// До:
orders.forEach(order => {
const subtotal = order.items.reduce((sum, i) => sum + i.price * i.qty, 0);
const tax = subtotal * 0.2;
const shipping = subtotal > 5000 ? 0 : 500;
order.total = subtotal + tax + shipping;
});
// После:
function calculateOrderTotal(order) {
const subtotal = calculateSubtotal(order.items);
const tax = calculateTax(subtotal);
const shipping = calculateShipping(subtotal);
return subtotal + tax + shipping;
}
orders.forEach(order => {
order.total = calculateOrderTotal(order);
});
// 2. Replace Magic Number with Named Constant
// До:
if (password.length < 8) { /* ... */ }
setTimeout(retry, 3000);
// После:
const MIN_PASSWORD_LENGTH = 8;
const RETRY_DELAY_MS = 3000;
if (password.length < MIN_PASSWORD_LENGTH) { /* ... */ }
setTimeout(retry, RETRY_DELAY_MS);
// 3. Replace Conditional with Guard Clause (см. пример выше)
Частые ошибки
- Оптимизация читаемости ради краткости. Короткий код не значит чистый — читаемый код важнее
- Переименование без смысла.
data→infoне улучшает код.data→userProfiles— улучшает - Рефакторинг без тестов. Сначала тесты, потом рефакторинг
- Перфекционизм. Чистый код — это процесс, не конечное состояние. Делайте чуть лучше каждый раз
Практика
- Возьмите функцию длиннее 20 строк и разбейте на 3-4 маленькие
- Замените все магические числа в файле на именованные константы
- Настройте ESLint + Prettier в проекте для автоматического форматирования
- Проведите code review своего кода: прочитайте каждое имя — понятно ли без контекста?
Связанные темы
Ресурсы
- Clean Code (Robert C. Martin) — классическая книга
- Refactoring (Martin Fowler) — каталог приёмов
- refactoring.guru — интерактивный каталог
- ESLint — линтер для JavaScript
- Prettier — автоформатирование