1. Clean Code принципы
Design Patterns. Урок: Clean Code принципы
Заголовок раздела «Design Patterns. Урок: Clean Code принципы»Clean Code — это не стиль форматирования, это философия. Концепция, которую Роберт Мартин («дядя Боб») описал в своей книге «Clean Code» (2008). Суть простая: код читают люди, а не машины. Компилятору всё равно — он переварит любую мешанину. А вот твой коллега (или ты сам через 3 месяца) — нет.
Принцип KISS (Keep It Simple, Stupid)
Заголовок раздела «Принцип KISS (Keep It Simple, Stupid)»Самое простое решение — лучшее. Сложность убивает скорость разработки и порождает баги.
// ❌ Overcomplicatedfunction getUserDisplayName(user: { firstName: string; lastName: string; nickname?: string }): string { return user.nickname ? `${user.nickname} (${user.firstName} ${user.lastName})` : `${user.firstName} ${user.lastName}`;}
// ✅ Simple and clearfunction getUserDisplayName(user: User): string { if (user.nickname) { return `${user.nickname} (${user.firstName} ${user.lastName})`; } return `${user.firstName} ${user.lastName}`;}Принцип DRY (Don’t Repeat Yourself)
Заголовок раздела «Принцип DRY (Don’t Repeat Yourself)»Каждая часть знания должна иметь единственное, однозначное и авторитетное представление в системе.
// ❌ Code duplicationfunction 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 validationfunction 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 };}Принцип YAGNI (You Aren’t Gonna Need It)
Заголовок раздела «Принцип YAGNI (You Aren’t Gonna Need It)»Не пиши код «на будущее». Реши текущую задачу. Преждевременная оптимизация и обобщение — корень многих зол.
// ❌ Premature abstractionclass DataProcessor { process(data: any, mode: 'xml' | 'json' | 'csv' | 'yaml' | 'toml'): any { // Поддержка 5 форматов, когда нужен только JSON }}
// ✅ Solve what you need nowclass JsonDataProcessor { process(data: string): object { return JSON.parse(data); }}Именование: говорящие имена
Заголовок раздела «Именование: говорящие имена»Код должен читаться как проза. Имена переменных, функций и классов должны объяснять намерение.
// ❌ Crypticconst d = new Date();const u = db.getU(id);function calc(x: number, y: number): number { return x * y * 0.2;}
// ✅ Expressiveconst 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 muchasync 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 responsibilityasync 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 idconst user = getUserById(id);
// ❌ Outdated comment (worse than no comment)// Apply 10% discount for premium usersconst 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 failurefunction findUser(id: string): User | null { return users.find(u => u.id === id) || null;}
// ✅ Explicit about failurefunction 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 explicitclass UserNotFoundError extends Error { constructor(message: string) { super(message); this.name = 'UserNotFoundError'; }}Числа и строки: используй константы
Заголовок раздела «Числа и строки: используй константы»Magic numbers и magic strings — зло. Они ничего не говорят о своём назначении.
// ❌ Magic numbersif (user.age >= 18 && user.subscriptionTier === 2) { applyDiscount(0.15);}
// ✅ Named constantsconst 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 уровня
Если превышаешь — повод задуматься о рефакторинге.
Практические задания
Заголовок раздела «Практические задания»-
Возьми любую функцию из своего кода длиннее 40 строк и разбей на несколько меньших функций с говорящими именами.
-
Найди в своём проекте все magic numbers и magic strings и замени их именованными константами.
-
Напиши функцию
formatPhoneNumber(phone: string): string, которая приводит номер к формату+7 (XXX) XXX-XX-XX. Добейся чтобы она читалась как документация сама себя. -
Рефактор: функция ниже нарушает 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}!`;}- Почитай главу 2 «Meaningful Names» из книги «Clean Code» Роберта Мартина — она есть в открытом доступе.