# Чистый код B: DRY, KISS, разделение ответственности, комментарии и рефакторинг<no value>

## Что мы изучим?

- **DRY** — Don't Repeat Yourself («не повторяйся»).
- **KISS** — Keep It Simple, Stupid («будь проще»).
- **Separation of Concerns** — разделение ответственности между частями программы.
- **Комментарии** — когда комментарий помогает, а когда вредит.
- **Рефакторинг** (refactoring) — улучшение структуры кода без изменения его поведения.

## Зачем это нужно?

В теме о компьютере и операционной системе мы написали `project_check.py` с четырьмя почти одинаковыми блоками. Что будет, когда понадобится проверить ещё 20 файлов? Или поменять сообщение с `OK` на `✓`? Придётся трогать 20 мест, и в одном из них вы обязательно забудете.

Сегодняшние принципы отвечают на вопрос: **как писать код, который легко менять?** В реальности большая часть времени разработчика уходит на изменение существующего кода, а не на написание нового.

## Объяснение с основ

### DRY — Don't Repeat Yourself

**Каждое знание должно находиться только в одном месте.** Когда одна и та же логика встречается в нескольких местах:

- Исправление бага нужно сделать во всех местах, и одно всегда забывают.
- Код длиннее, и трудно увидеть, что уникально в каждой части.

**Инструменты исправления:** функция (повторяющаяся логика), цикл (действие над многими элементами), константа (повторяющееся значение).

**Тревожный сигнал:** если вы копируете код (Copy-Paste) и меняете в нём маленькую деталь, вероятно, нужна функция с параметром.

### KISS — Keep It Simple

**Самое простое работающее решение обычно лучшее.** «Хитрый» код (одна сложная строка, трюки) трудно читать, трудно отлаживать и трудно менять. Цель кода — быть понятым, а не впечатлить.

### Разделение ответственности

**Concern** = область ответственности. В каждой программе есть несколько разных областей:

- **Ввод** — откуда приходит информация (клавиатура, файл, HTTP-запрос).
- **Логика** — что мы с ней делаем (вычисления, решения, правила).
- **Вывод** — куда уходит информация (экран, файл, HTTP-ответ).

Когда они разделены, одну можно заменить, не трогая остальные. **Это именно структура сервера в темах бэкенда и баз данных:** маршрут (Route, HTTP-ввод/вывод) отделён от кода доступа к базе данных (Database).

SRP (из предыдущего урока) говорит об одной функции. Separation of Concerns говорит о **всей программе**: какие части существуют и как они разделены.

### Комментарии

**Код объясняет «что» и «как». Комментарий объясняет «почему».**

| Хороший комментарий | Ненужный / вредный комментарий |
|---|---|
| Объясняет, **почему** мы выбрали определённый способ | Повторяет то, что уже говорит код |
| Предупреждает о неожиданном поведении | Компенсирует плохое имя |
| Указывает на источник (закон, стандарт, документ) | «Мёртвый код», превращённый в комментарий |
| | Старый комментарий, который уже неверен, — хуже всего |

### Рефакторинг

**Рефакторинг** = изменение **структуры** кода **без изменения его поведения**. Вы переименовываете, разбиваете функции, убираете повторяющийся код. В итоге код делает ровно то же самое, только понятнее.

**Важное правило:** **маленькие шаги и проверка после каждого шага.** Измените одно, запустите, сравните с исходным выводом и продолжайте.

## Синтаксис

```python
# DRY: repeated code  →  one function + loop
def check(item):
    ...

for item in items:
    check(item)

# Comment: explain WHY, not WHAT
# Tax authority requires rounding up to whole shekels (regulation 12b)
total = math.ceil(total)

# Docstring: a string right under def that describes the function
def calculate_shipping(weight_kg):
    """Return shipping cost in NIS for a package of the given weight."""
    ...
```

**Docstring** — строка в тройных кавычках (`"""..."""`), которая стоит в первой строке функции и описывает, что она делает. VS Code показывает её, когда вы наводите курсор на вызов функции.

## Первый пример кода

**DRY в действии.** Помните `project_check.py` из темы о компьютере, операционной системе и сети? **До:**

```python
import os

base = "../shop"

if os.path.exists(base + "/data"):
    print("data -> OK")
else:
    print("data -> MISSING")

if os.path.exists(base + "/logs"):
    print("logs -> OK")
else:
    print("logs -> MISSING")

if os.path.exists(base + "/app.py"):
    print("app.py -> OK")
else:
    print("app.py -> MISSING")
```

**После:**

```python
import os

PROJECT_DIR = "../shop"
REQUIRED_PATHS = ["data", "logs", "app.py", "data/products.txt"]

def check_path(relative_path):
    full_path = PROJECT_DIR + "/" + relative_path
    if os.path.exists(full_path):
        status = "OK"
    else:
        status = "MISSING"
    print(relative_path, "->", status)

for path in REQUIRED_PATHS:
    check_path(path)
```

**Вывод (то же поведение):**

```text
data -> OK
logs -> OK
app.py -> OK
data/products.txt -> OK
```

## Разбор кода построчно

| Строка | Объяснение |
|---|---|
| `PROJECT_DIR = "../shop"` | Путь определён **один раз**. Если проект переедет в другую папку, меняется одна строка. |
| `REQUIRED_PATHS = [...]` | **Данные** (что проверять) отделены от **логики** (как проверять). Добавить проверку = добавить имя в список. |
| `def check_path(relative_path):` | Логика, которая повторялась четыре раза, написана **один раз**. То, что менялось между блоками (имя пути), стало **параметром**. |
| `status = ...` и затем один `print` | Вместо двух почти одинаковых `print` — переменная со статусом и одна печать. Это тоже DRY в малом масштабе. |
| `for path in REQUIRED_PATHS:` | Цикл запускает проверку для каждого элемента. |

## Ещё один пример

**KISS + разделение ответственности + комментарии.** Программа, которая вычисляет стоимость доставки. **До** — «хитро», всё вперемешку и с ненужными комментариями:

```python
w = float(input("w: "))  # get w
# calculate the price
print("Price:", (w <= 1) * 20 + (1 < w <= 5) * 35 + (w > 5) * (35 + (w - 5) * 4))  # print
```

**После:**

```python
BASE_PRICE_SMALL = 20
BASE_PRICE_MEDIUM = 35
PRICE_PER_EXTRA_KG = 4
SMALL_MAX_KG = 1
MEDIUM_MAX_KG = 5

def read_weight():
    return float(input("Package weight (kg): "))

def calculate_shipping(weight_kg):
    """Return the shipping price in NIS for a package of the given weight."""
    if weight_kg <= SMALL_MAX_KG:
        return BASE_PRICE_SMALL
    if weight_kg <= MEDIUM_MAX_KG:
        return BASE_PRICE_MEDIUM
    # Above the medium tier the courier charges per extra kg (price list 2026)
    extra_kg = weight_kg - MEDIUM_MAX_KG
    return BASE_PRICE_MEDIUM + extra_kg * PRICE_PER_EXTRA_KG

def show_price(price):
    print("Shipping price:", price, "NIS")

weight = read_weight()
price = calculate_shipping(weight)
show_price(price)
```

**Запуск:**

```text
Package weight (kg): 7.5
Shipping price: 45.0 NIS
```

**Разбор:**

- **KISS:** трюк `(w <= 1) * 20` работает (потому что `True` считается как 1, а `False` как 0), но с первого взгляда его никто не поймёт. Простая цепочка `if` читается сразу.
- **Разделение ответственности:** `read_weight` (ввод), `calculate_shipping` (логика), `show_price` (вывод). Завтра вес придёт из HTTP-запроса? Меняется только `read_weight`. Логика не меняется.
- **Комментарии:** `# get w` и `# print` удалены, потому что повторяли код. Остался один комментарий, который объясняет **почему** (прайс-лист курьерской компании), — информацию, которую нельзя понять из самого кода.
- **Docstring** описывает, что возвращает функция и в каких единицах (шекели, NIS).

## Типичная ошибка

**Ошибка 1: комментарий вместо хорошего имени.**

```python
d = 30  # number of days until the subscription expires
```

Лучше: `days_until_expiry = 30` — совсем без комментария. Если для объяснения переменной нужен комментарий, имя недостаточно хорошее.

**Ошибка 2: комментарий, который лжёт.**

```python
# Discount is 10%
price = price * 0.85
```

Кто-то обновил код и забыл про комментарий. Теперь читатель не знает, чему верить. Python не проверяет комментарии, поэтому они «гниют». Это ещё одна причина писать мало комментариев, но хороших.

**Ошибка 3: DRY слишком рано.** Два куска кода, которые похожи **случайно**, но представляют разные бизнес-правила, не обязательно объединять. Например, «НДС 17%» и «комиссия 17%» — совпадение, а не одно и то же знание. Если объединить их в одну константу, изменение одного сломает другое.

**Ошибка 4: «большой» рефакторинг за раз.** Вы меняете 10 вещей, запускаете, что-то ломается, и вы понятия не имеете, что именно. **Один шаг, запуск, сравнение, следующий шаг.**

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## Практика

Упражнения опираются только на то, что пройдено в этом уроке и в предыдущих. Сначала попробуйте сами, и только потом открывайте решение.

### Упражнение 1: DRY — чек без копирования

```python
print("Pen", 5, "x", 4, "=", 5 * 4)
print("Notebook", 12, "x", 2, "=", 12 * 2)
print("Bag", 60, "x", 1, "=", 60 * 1)
print("Total:", 5 * 4 + 12 * 2 + 60 * 1)
```

Перепишите код так, чтобы данные хранились в списках, а одна функция с циклом печатала чек. Вывод должен остаться:

```text
Pen 5 x 4 = 20
Notebook 12 x 2 = 24
Bag 60 x 1 = 60
Total: 104
```

{{< solution >}}
```python
def line_total(price, quantity):
    return price * quantity


def print_receipt(names, prices, quantities):
    total = 0
    for i in range(len(names)):
        amount = line_total(prices[i], quantities[i])
        print(names[i], prices[i], "x", quantities[i], "=", amount)
        total = total + amount
    print("Total:", total)


print_receipt(["Pen", "Notebook", "Bag"], [5, 12, 60], [4, 2, 1])
```

```text
Pen 5 x 4 = 20
Notebook 12 x 2 = 24
Bag 60 x 1 = 60
Total: 104
```

Каждое вычисление теперь встречается **один раз**. Четвёртый товар = ещё одно значение в списках, без новой строки кода.
{{< /solution >}}

### Упражнение 2: KISS — слишком «хитрый» код

```python
def parity(n):
    return (n % 2 == 0) * "even" + (n % 2 == 1) * "odd"

print(parity(4))
print(parity(7))
```

Код работает, но его трудно понять. Перепишите `parity` самым простым и читаемым способом.

**Ожидаемый вывод:**

```text
even
odd
```

{{< solution >}}
```python
def parity(n):
    if n % 2 == 0:
        return "even"
    return "odd"


print(parity(4))
print(parity(7))
```

```text
even
odd
```

**KISS** — Keep It Simple. Обычный `if` читается сразу, без размышлений о том, как работает умножение `True` на строку.
{{< /solution >}}

### Упражнение 3: Комментарии — почему, а не что

Для каждого комментария — хороший он, ненужный или вредный? Объясните.

```text
1.  count = count + 1   # add 1 to count
2.  # 30 days: the refund policy of the shop
    REFUND_DAYS = 30
3.  total = price * 2   # multiply price by 3
4.  # retry 3 times — the payment API sometimes fails on the first call
```

{{< solution >}}
1. **Ненужный** — повторяет то, что уже говорит код.
2. **Хороший** — объясняет, **почему** 30, — информацию, которой нет в коде.
3. **Вредный** — лжёт (код умножает на 2). Необновлённый комментарий хуже, чем никакого.
4. **Хороший** — объясняет, **почему** код делает что-то странное на вид.

Правило: код говорит **что**, комментарий говорит **почему**.
{{< /solution >}}

### Упражнение 4: Разделение ответственности — вычисление отдельно от ввода и вывода

```python
def bmi():
    weight = float(input("Weight: "))
    height = float(input("Height: "))
    print("BMI:", round(weight / (height * height), 1))
```

Функция делает три вещи: читает ввод, вычисляет и печатает. Поэтому вычислением нельзя воспользоваться без клавиатуры. Разбейте её на три функции, по одной на каждую ответственность:

1. `read_number(message)` — только читает число от пользователя и возвращает его.
2. `calculate_bmi(weight, height)` — только вычисляет и возвращает, без `input` и без `print`.
3. `print_bmi(bmi)` — только печатает уже вычисленный результат.

Проверьте вычисление и печать без клавиатуры:

```py
bmi = calculate_bmi(70, 1.75)
print_bmi(bmi)
```

**Ожидаемый вывод:**

```text
BMI: 22.9
```

{{< solution >}}
```python
def read_number(message):
    return float(input(message))


def calculate_bmi(weight, height):
    return round(weight / (height * height), 1)


def print_bmi(bmi):
    print("BMI:", bmi)


bmi = calculate_bmi(70, 1.75)
print_bmi(bmi)
```

```text
BMI: 22.9
```

В настоящей программе три функции соединяются, и каждая строка запускает одну ответственность:

```py
weight = read_number("Weight: ")
height = read_number("Height: ")
bmi = calculate_bmi(weight, height)
print_bmi(bmi)
```

- `input` есть только в `read_number`, а `print` — только в `print_bmi`. У `calculate_bmi` нет ни ввода, ни вывода, поэтому её можно вызвать с данными откуда угодно: из ввода, из файла или в тесте.
- `print("BMI:", calculate_bmi(70, 1.75))` — код короче, но печать по-прежнему склеена с вычислением в одной строке, и ни одна функция не отвечает за показ. Хотите показать результат иначе? С разделением заменяется только `print_bmi`.
{{< /solution >}}

### Упражнение 5: Рефакторинг по шагам

```python
t = 12
if t < 15:
    print(12, "cold")
elif t < 28:
    print(12, "pleasant")
else:
    print(12, "hot")
t = 24
if t < 15:
    print(24, "cold")
elif t < 28:
    print(24, "pleasant")
else:
    print(24, "hot")
```

Выполните рефакторинг: функция `classify(temperature)`, которая возвращает классификацию, константы для границ и цикл по списку температур `[12, 24, 33]`.

**Ожидаемый вывод:**

```text
12 cold
24 pleasant
33 hot
```

{{< solution >}}
```python
COLD_LIMIT = 15
HOT_LIMIT = 28


def classify(temperature):
    if temperature < COLD_LIMIT:
        return "cold"
    if temperature < HOT_LIMIT:
        return "pleasant"
    return "hot"


for temperature in [12, 24, 33]:
    print(temperature, classify(temperature))
```

```text
12 cold
24 pleasant
33 hot
```

Шаги: (1) вынести функцию, (2) перенести 15 и 28 в константы, (3) заменить повторяющийся код циклом — и запускать после **каждого** шага, чтобы убедиться, что вывод не изменился.
{{< /solution >}}

<!-- exercises:end -->

## Итоги

- **DRY** — каждое знание в одном месте. Повторяющийся код → функция, цикл или константа.
- **KISS** — простое и читаемое лучше «хитрого».
- **Разделение ответственности** — ввод, логика и вывод в отдельных частях. Это структура любого сервера.
- **Комментарии** объясняют **почему**. Сам код (с хорошими именами) объясняет **что**. Лживый комментарий хуже, чем никакого.
- **Рефакторинг** — улучшайте структуру без изменения поведения, маленькими шагами с запуском после каждого шага.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## Официальная документация

- [PEP 20 — The Zen of Python](https://peps.python.org/pep-0020/)
- [PEP 8 — Comments](https://peps.python.org/pep-0008/#comments)
- [Python Tutorial — Defining Functions](https://docs.python.org/3/tutorial/controlflow.html#defining-functions)

<!-- docs:end -->
