Чистый код B: DRY, KISS, разделение ответственности, комментарии и рефакторинг
- Распознавать повторяющийся код и заменять его функцией или циклом (DRY)
- Предпочитать простое, читаемое решение «хитрому» (KISS)
- Разделять ввод, логику и вывод (Separation of Concerns)
- Знать, когда писать комментарий, а когда код должен объяснять себя сам
- Выполнять рефакторинг маленькими шагами, сохраняя то же поведение
Что мы изучим?
- 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 говорит о всей программе: какие части существуют и как они разделены.
Комментарии
Код объясняет «что» и «как». Комментарий объясняет «почему».
| Хороший комментарий | Ненужный / вредный комментарий |
|---|---|
| Объясняет, почему мы выбрали определённый способ | Повторяет то, что уже говорит код |
| Предупреждает о неожиданном поведении | Компенсирует плохое имя |
| Указывает на источник (закон, стандарт, документ) | «Мёртвый код», превращённый в комментарий |
| Старый комментарий, который уже неверен, — хуже всего |
Рефакторинг
Рефакторинг = изменение структуры кода без изменения его поведения. Вы переименовываете, разбиваете функции, убираете повторяющийся код. В итоге код делает ровно то же самое, только понятнее.
Важное правило: маленькие шаги и проверка после каждого шага. Измените одно, запустите, сравните с исходным выводом и продолжайте.
Синтаксис
# 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 из темы о компьютере, операционной системе и сети? До:
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")После:
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)Вывод (то же поведение):
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 + разделение ответственности + комментарии. Программа, которая вычисляет стоимость доставки. До — «хитро», всё вперемешку и с ненужными комментариями:
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После:
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)Запуск:
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: комментарий вместо хорошего имени.
d = 30 # number of days until the subscription expiresЛучше: days_until_expiry = 30 — совсем без комментария. Если для объяснения переменной нужен комментарий, имя недостаточно хорошее.
Ошибка 2: комментарий, который лжёт.
# Discount is 10%
price = price * 0.85Кто-то обновил код и забыл про комментарий. Теперь читатель не знает, чему верить. Python не проверяет комментарии, поэтому они «гниют». Это ещё одна причина писать мало комментариев, но хороших.
Ошибка 3: DRY слишком рано. Два куска кода, которые похожи случайно, но представляют разные бизнес-правила, не обязательно объединять. Например, «НДС 17%» и «комиссия 17%» — совпадение, а не одно и то же знание. Если объединить их в одну константу, изменение одного сломает другое.
Ошибка 4: «большой» рефакторинг за раз. Вы меняете 10 вещей, запускаете, что-то ломается, и вы понятия не имеете, что именно. Один шаг, запуск, сравнение, следующий шаг.
Практика
Упражнения опираются только на то, что пройдено в этом уроке и в предыдущих. Сначала попробуйте сами, и только потом открывайте решение.
Упражнение 1: DRY — чек без копирования
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)Перепишите код так, чтобы данные хранились в списках, а одна функция с циклом печатала чек. Вывод должен остаться:
Pen 5 x 4 = 20
Notebook 12 x 2 = 24
Bag 60 x 1 = 60
Total: 104Показать решение
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])Pen 5 x 4 = 20
Notebook 12 x 2 = 24
Bag 60 x 1 = 60
Total: 104Каждое вычисление теперь встречается один раз. Четвёртый товар = ещё одно значение в списках, без новой строки кода.
Упражнение 2: KISS — слишком «хитрый» код
def parity(n):
return (n % 2 == 0) * "even" + (n % 2 == 1) * "odd"
print(parity(4))
print(parity(7))Код работает, но его трудно понять. Перепишите parity самым простым и читаемым способом.
Ожидаемый вывод:
even
oddПоказать решение
def parity(n):
if n % 2 == 0:
return "even"
return "odd"
print(parity(4))
print(parity(7))even
oddKISS — Keep It Simple. Обычный if читается сразу, без размышлений о том, как работает умножение True на строку.
Упражнение 3: Комментарии — почему, а не что
Для каждого комментария — хороший он, ненужный или вредный? Объясните.
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Показать решение
- Ненужный — повторяет то, что уже говорит код.
- Хороший — объясняет, почему 30, — информацию, которой нет в коде.
- Вредный — лжёт (код умножает на 2). Необновлённый комментарий хуже, чем никакого.
- Хороший — объясняет, почему код делает что-то странное на вид.
Правило: код говорит что, комментарий говорит почему.
Упражнение 4: Разделение ответственности — вычисление отдельно от ввода и вывода
def bmi():
weight = float(input("Weight: "))
height = float(input("Height: "))
print("BMI:", round(weight / (height * height), 1))Функция делает три вещи: читает ввод, вычисляет и печатает. Поэтому вычислением нельзя воспользоваться без клавиатуры. Разбейте её на три функции, по одной на каждую ответственность:
read_number(message)— только читает число от пользователя и возвращает его.calculate_bmi(weight, height)— только вычисляет и возвращает, безinputи безprint.print_bmi(bmi)— только печатает уже вычисленный результат.
Проверьте вычисление и печать без клавиатуры:
bmi = calculate_bmi(70, 1.75)
print_bmi(bmi)Ожидаемый вывод:
BMI: 22.9Показать решение
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)BMI: 22.9В настоящей программе три функции соединяются, и каждая строка запускает одну ответственность:
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.
Упражнение 5: Рефакторинг по шагам
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].
Ожидаемый вывод:
12 cold
24 pleasant
33 hotПоказать решение
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))12 cold
24 pleasant
33 hotШаги: (1) вынести функцию, (2) перенести 15 и 28 в константы, (3) заменить повторяющийся код циклом — и запускать после каждого шага, чтобы убедиться, что вывод не изменился.
Итоги
- DRY — каждое знание в одном месте. Повторяющийся код → функция, цикл или константа.
- KISS — простое и читаемое лучше «хитрого».
- Разделение ответственности — ввод, логика и вывод в отдельных частях. Это структура любого сервера.
- Комментарии объясняют почему. Сам код (с хорошими именами) объясняет что. Лживый комментарий хуже, чем никакого.
- Рефакторинг — улучшайте структуру без изменения поведения, маленькими шагами с запуском после каждого шага.