Перейти к содержимому

1. Clean Code принципы

Clean Code — это не стиль форматирования, это философия. Концепция, которую Роберт Мартин («дядя Боб») описал в своей книге «Clean Code» (2008). Суть простая: код читают люди, а не машины. Компилятору всё равно — он переварит любую мешанину. А вот твой коллега (или ты сам через 3 месяца) — нет.


Самое простое решение — лучшее. Сложность убивает скорость разработки и порождает баги.

// ❌ Overcomplicated
function getUserDisplayName(user: { firstName: string; lastName: string; nickname?: string }): string {
return user.nickname
? `${user.nickname} (${user.firstName} ${user.lastName})`
: `${user.firstName} ${user.lastName}`;
}
// ✅ Simple and clear
function getUserDisplayName(user: User): string {
if (user.nickname) {
return `${user.nickname} (${user.firstName} ${user.lastName})`;
}
return `${user.firstName} ${user.lastName}`;
}

Каждая часть знания должна иметь единственное, однозначное и авторитетное представление в системе.

// ❌ Code duplication
function createUserProfile(name: string, email: string) {
if (!name || name.length < 2) throw new Error('Invalid name');
if (!email || !email.includes('@')) throw new Error('Invalid email');
return { name, email };
}
function updateUserProfile(userId: string, name: string, email: string) {
if (!name || name.length < 2) throw new Error('Invalid name'); // Дублирование!
if (!email || !email.includes('@')) throw new Error('Invalid email'); // Дублирование!
return { userId, name, email };
}
// ✅ Extract validation
function validateUserData(name: string, email: string): void {
if (!name || name.length < 2) throw new Error('Invalid name');
if (!email || !email.includes('@')) throw new Error('Invalid email');
}
function createUserProfile(name: string, email: string) {
validateUserData(name, email);
return { name, email };
}
function updateUserProfile(userId: string, name: string, email: string) {
validateUserData(name, email);
return { userId, name, email };
}

Не пиши код «на будущее». Реши текущую задачу. Преждевременная оптимизация и обобщение — корень многих зол.

// ❌ Premature abstraction
class DataProcessor {
process(data: any, mode: 'xml' | 'json' | 'csv' | 'yaml' | 'toml'): any {
// Поддержка 5 форматов, когда нужен только JSON
}
}
// ✅ Solve what you need now
class JsonDataProcessor {
process(data: string): object {
return JSON.parse(data);
}
}

Код должен читаться как проза. Имена переменных, функций и классов должны объяснять намерение.

// ❌ Cryptic
const d = new Date();
const u = db.getU(id);
function calc(x: number, y: number): number {
return x * y * 0.2;
}
// ✅ Expressive
const currentDate = new Date();
const activeUser = database.getUserById(userId);
function calculateSalesTax(price: number, quantity: number): number {
const TAX_RATE = 0.2;
return price * quantity * TAX_RATE;
}

Функция должна делать одно и делать это хорошо. Если функцию сложно назвать — скорее всего она делает слишком много.

// ❌ Function does too much
async function processOrder(orderId: string): Promise<void> {
const order = await fetchOrder(orderId);
const user = await fetchUser(order.userId);
const discount = user.isPremium ? 0.15 : 0;
const total = order.items.reduce((sum, item) => sum + item.price, 0) * (1 - discount);
await sendEmail(user.email, `Your order total is ${total}`);
await updateInventory(order.items);
await saveOrderTotal(orderId, total);
}
// ✅ Each function has one responsibility
async function processOrder(orderId: string): Promise<void> {
const order = await fetchOrder(orderId);
const user = await fetchUser(order.userId);
const total = calculateOrderTotal(order, user);
await Promise.all([
notifyUserAboutOrder(user, total),
updateInventory(order.items),
saveOrderTotal(orderId, total),
]);
}
function calculateOrderTotal(order: Order, user: User): number {
const subtotal = order.items.reduce((sum, item) => sum + item.price, 0);
const discount = getUserDiscount(user);
return subtotal * (1 - discount);
}
function getUserDiscount(user: User): number {
return user.isPremium ? 0.15 : 0;
}

Хороший код сам себя объясняет. Комментарий — признак того, что код недостаточно выразителен. Но иногда они нужны: для объяснения «почему» (не «что»), для сложных алгоритмов, для публичных API.

// ❌ Comment explains what (should be obvious from code)
// Get user by id
const user = getUserById(id);
// ❌ Outdated comment (worse than no comment)
// Apply 10% discount for premium users
const discount = user.isPremium ? 0.15 : 0;
// ✅ Comment explains WHY
// We use setTimeout because PaymentProvider requires a 100ms delay
// between consecutive requests to avoid rate limiting (see docs/payment.md)
setTimeout(() => processPayment(order), 100);

Не возвращай null/undefined там, где можно выбросить исключение. Ошибки должны быть явными.

// ❌ Silent failure
function findUser(id: string): User | null {
return users.find(u => u.id === id) || null;
}
// ✅ Explicit about failure
function findUser(id: string): User {
const user = users.find(u => u.id === id);
if (!user) {
throw new UserNotFoundError(`User with id ${id} not found`);
}
return user;
}
// Custom error classes make handling explicit
class UserNotFoundError extends Error {
constructor(message: string) {
super(message);
this.name = 'UserNotFoundError';
}
}

Magic numbers и magic strings — зло. Они ничего не говорят о своём назначении.

// ❌ Magic numbers
if (user.age >= 18 && user.subscriptionTier === 2) {
applyDiscount(0.15);
}
// ✅ Named constants
const MINIMUM_ADULT_AGE = 18;
const PREMIUM_SUBSCRIPTION_TIER = 2;
const PREMIUM_DISCOUNT_RATE = 0.15;
if (user.age >= MINIMUM_ADULT_AGE && user.subscriptionTier === PREMIUM_SUBSCRIPTION_TIER) {
applyDiscount(PREMIUM_DISCOUNT_RATE);
}

Правила размера — не жёсткие, но ориентируйся на:

  • Функция: до 20-30 строк
  • Файл/класс: до 200-300 строк
  • Максимальная вложенность: 3-4 уровня

Если превышаешь — повод задуматься о рефакторинге.


  1. Возьми любую функцию из своего кода длиннее 40 строк и разбей на несколько меньших функций с говорящими именами.

  2. Найди в своём проекте все magic numbers и magic strings и замени их именованными константами.

  3. Напиши функцию formatPhoneNumber(phone: string): string, которая приводит номер к формату +7 (XXX) XXX-XX-XX. Добейся чтобы она читалась как документация сама себя.

  4. Рефактор: функция ниже нарушает DRY. Исправь:

function getAdminGreeting(name: string): string {
const now = new Date();
const hour = now.getHours();
if (hour < 12) return `Доброе утро, Admin ${name}!`;
if (hour < 18) return `Добрый день, Admin ${name}!`;
return `Добрый вечер, Admin ${name}!`;
}
function getUserGreeting(name: string): string {
const now = new Date();
const hour = now.getHours();
if (hour < 12) return `Доброе утро, ${name}!`;
if (hour < 18) return `Добрый день, ${name}!`;
return `Добрый вечер, ${name}!`;
}
  1. Почитай главу 2 «Meaningful Names» из книги «Clean Code» Роберта Мартина — она есть в открытом доступе.