Clean Code

Как писать код, который не стыдно показать

1. Переменные vs Комментарии

Комментарий — признак плохого имени или избыточной логики

❌ Плохо

def calc(x, y):
    # x - user, y - order
    if x['premium'] and y['sum'] > 100:
        return 0.2
    return 0.1

Почему нужно объяснять?

✅ Хорошо

def calculate_discount(user, order):
    if user.is_premium and order.total > 100:
        return 0.2
    return 0.1

Никаких комментариев — всё ясно

2. Маленькие функции

Одна задача — одна функция

❌ Длинная функция

def process_data(data):
    cleaned = []
    for item in data:
        if item is not None and item > 0:
            cleaned.append(item * 1.1)
    total = sum(cleaned)
    avg = total / len(cleaned) if cleaned else 0
    print(f"Total: {total}, Avg: {avg}")
    return {"total": total, "avg": avg}

✅ Разбивка

def clean_data(data):
    return [x * 1.1 for x in data if x is not None and x > 0]

def calculate_stats(cleaned):
    total = sum(cleaned)
    avg = total / len(cleaned) if cleaned else 0
    return {"total": total, "avg": avg}

def report(stats):
    print(f"Total: {stats['total']}, Avg: {stats['avg']}")

🔹 Легко тестировать
🔹 Легко переиспользовать
🔹 Легко читать и понимать

3. Избегайте null

Null — источник ошибок. Используйте объекты вместо него.

❌ С null

def get_user(id):
    return db.find(id)  # может быть None

user = get_user(123)
send_email(user.email)  # AttributeError!

✅ Объект по умолчанию

class User:
    @staticmethod
    def anonymous():
        return User(name="Guest", email="no@email")

def get_user(id):
    found = db.find(id)
    return found or User.anonymous()

user = get_user(123)
send_email(user.email)  # всегда безопасно

4. Flat vs Nested

Ранний выход лучше, чем глубокая вложенность

❌ Вложенность

if user:
    if user.active:
        if user.has_permission:
            execute_action()
        else:
            log("No permission")
    else:
        log("Inactive")
else:
    log("User not found")

✅ Ранний выход

if not user:
    log("User not found")
    return
if not user.active:
    log("Inactive")
    return
if not user.has_permission:
    log("No permission")
    return

execute_action()

5. Тесты: fast & independent

Медленные тесты — это анти-паттерн

❌ Интеграционный тест как unit

def test_train_model():
    model = train_on_real_dataset()  # 5 минут
    assert model.accuracy > 0.8

⏱️ Никто не запустит регулярно

✅ Unit-тест

def test_linear_predict():
    model = LinearModel(weight=2)
    result = model.predict(3)
    assert result == 6

⚡ Выполняется за миллисекунды

6. Command ≠ Query

Функция либо меняет состояние, либо возвращает значение

❌ Нарушение

def get_results():
    self.cache.clear()  # side effect!
    return db.query(...)

Вызываешь "get" — а система очищается

✅ Чистое разделение

def clear_cache():
    self.cache.clear()

def get_results():
    return db.query(...)

Поведение предсказуемо

7. Код по домену, не по типу

Группируйте по смыслу, а не по файловому типу

❌ По типу

models/
  user.py
  order.py
services/
  user_service.py
  order_service.py
utils/
  validation.py  # используется везде

✅ По домену

user/
  models.py
  service.py
  validators.py
order/
  models.py
  service.py
  discount_calculator.py

Всё, что относится к юзеру — в одной папке

8. Чистые функции

Один ввод → один вывод. Без side effects.

❌ Не чистая

results = []

def normalize(data):
    global results
    results.append(sum(data))  # side effect
    return [x / sum(data) for x in data]

✅ Чистая

def normalize(data):
    total = sum(data)
    return [x / total for x in data], total

Тестируема, параллелизуема, надёжна

9. Не передавайте булевы флаги

Они делают поведение неочевидным

❌ С флагом

def save(model, force_update=False):
    if force_update:
        db.update(model)
    else:
        db.insert(model)

save(model, True) — что это значит?

✅ Две функции

def create(model):
    db.insert(model)

def update(model):
    db.update(model)

Ясно, просто, легко поддерживать

10. Keep It Simple

Простой код — лучший код

❌ Слишком умно

result = [f(x) for x in data if p(x)] or [default]

Придётся думать, чтобы понять.

✅ Просто

filtered = []
for item in data:
    if passes_check(item):
        filtered.append(transform(item))

if not filtered:
    filtered = [default_value]

Читается как рассказ

“Каждый отрезок кода должен быть настолько простым и очевидным, чтобы очевидных ошибок в нём не было.”
— Brian Kernighan

Практика: Рефакторинг "грязного" кода

Улучшим код шаг за шагом

❌ Шаг 1: Исходный код (кошмар)

def proc_data(d, f):
    r = []
    for i in d:
        if i != None:
            if f == 'norm':
                r.append(i * 0.9)
            elif f == 'boost':
                r.append(i * 1.2)
    t = sum(r)
    a = t / len(r) if r else 0
    print(f"Total: {t}, Avg: {a}")
    return {'total': t, 'avg': a}

data = [10, -5, None, 20, 0, 15]
result = proc_data(data, 'boost')

Что не так? Переменные без смысла, флаги, side effects...

✅ Шаг 2: Значимые имена

def process_data(data, mode):
    result = []
    for item in data:
        if item is not None:
            if mode == 'norm':
                result.append(item * 0.9)
            elif mode == 'boost':
                result.append(item * 1.2)
    total = sum(result)
    avg = total / len(result) if result else 0
    print(f"Total: {total}, Avg: {avg}")
    return {'total': total, 'avg': avg}

🔹 proc_dataprocess_data
🔹 d, f, r → понятные имена

✅ Шаг 3: Ранний выход + плоская структура

def process_data(data, mode):
    result = []
    for item in data:
        if item is None:
            continue
        if mode == 'norm':
            result.append(item * 0.9)
        elif mode == 'boost':
            result.append(item * 1.2)
    total = sum(result)
    avg = total / len(result) if result else 0
    print(f"Total: {total}, Avg: {avg}")
    return {'total': total, 'avg': avg}

🔹 Убрали лишнюю вложенность через continue

✅ Шаг 4: Убираем side effect (print)

def process_data(data, mode):
    result = []
    for item in data:
        if item is None:
            continue
        if mode == 'norm':
            result.append(item * 0.9)
        elif mode == 'boost':
            result.append(item * 1.2)
    total = sum(result)
    avg = total / len(result) if result else 0
    return {'total': total, 'avg': avg}

# Отдельно — вывод
stats = process_data(data, 'boost')
print(f"Total: {stats['total']}, Avg: {stats['avg']}")

🔹 Функция больше не зависит от вывода

✅ Шаг 5: Избавляемся от условий — Strategy Pattern

def normalize(value):
    return value * 0.9

def boost(value):
    return value * 1.2

TRANSFORMATIONS = {
    'norm': normalize,
    'boost': boost
}

def process_data(data, mode):
    transform = TRANSFORMATIONS.get(mode)
    if not transform:
        raise ValueError("Unknown mode")
    
    result = [transform(item) for item in data if item is not None]
    total = sum(result)
    avg = total / len(result) if result else 0
    return {'total': total, 'avg': avg}

🔹 Нет if/elif
🔹 Легко добавить новую операцию

✅ Шаг 6: Почти идеал (чистая функция)

from typing import List, Dict

def clean_and_transform(data: List[float], 
                       transform_func) -> List[float]:
    return [transform_func(x) for x in data if x is not None]

def calculate_stats(values: List[float]) -> Dict[str, float]:
    total = sum(values)
    avg = total / len(values) if values else 0
    return {'total': total, 'avg': avg}

# Использование
values = clean_and_transform(data, boost)
stats = calculate_stats(values)

🔹 Одна функция — одна задача
🔹 Типы, читаемость, тестируемость

🎯 Итог рефакторинга

Мы превратили неподдерживаемый код в чистый, модульный, расширяемый.

  • Значимые имена
  • Маленькие функции
  • Нет side effects
  • Композиция вместо условий
  • Типы и документируемость