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 (см. пример выше)

Частые ошибки

  1. Оптимизация читаемости ради краткости. Короткий код не значит чистый — читаемый код важнее
  2. Переименование без смысла. datainfo не улучшает код. datauserProfiles — улучшает
  3. Рефакторинг без тестов. Сначала тесты, потом рефакторинг
  4. Перфекционизм. Чистый код — это процесс, не конечное состояние. Делайте чуть лучше каждый раз

Практика

  1. Возьмите функцию длиннее 20 строк и разбейте на 3-4 маленькие
  2. Замените все магические числа в файле на именованные константы
  3. Настройте ESLint + Prettier в проекте для автоматического форматирования
  4. Проведите code review своего кода: прочитайте каждое имя — понятно ли без контекста?

Связанные темы

Ресурсы

  • Clean Code (Robert C. Martin) — классическая книга
  • Refactoring (Martin Fowler) — каталог приёмов
  • refactoring.guru — интерактивный каталог
  • ESLint — линтер для JavaScript
  • Prettier — автоформатирование