Поддерживаемый код, или maintable-код, — это когда код можно читать, менять и добавлять новые возможности без опасения, что это сломает всю программу. Такой код необязательно работает с максимальной производительностью, но он безопасен и надёжен. Через несколько месяцев вы или другой разработчик сможете открыть его и понять, что происходит внутри приложения.
Почему писать такой код может быть сложно: на старте работы часто кажется, что можно писать как угодно и всё равно будет нормально. Но проект растёт, логика работы усложняется, появляются новые участники проекта. Если заранее не предусмотреть правила для будущего развития, в какой-то момент в работе начнётся хаос.
Maintable-код даёт следующее: багов становится меньше, дорабатывать код проще, а сломать сложнее. Сегодня разбираем, как его писать и как он выглядит.
Зачем нужен чистый код
Чистый код легко читать и понимать без пояснений. Для работы с программой не нужно держать в голове всю бизнес-логику или угадывать принципы работы. Это важно, когда сервис живёт дольше первой черновой версии продукта.
В работе чистый код помогает коммуникации. Разработчик общается не только с машиной, но и с другими коллегами, которые будут этот код читать. Даже если он сам будет читать этот код через полгода, детали реализации легко можно забыть, и разбираться в собственном коде будет сложно, если писать его без соблюдения некоторых принципов.
Проблемы плохого кода появляются не сразу. Сначала всё работает хорошо, но при серьёзных доработках новые изменения часто начинают ломать уже существующие функции программы. Чистый код делает работу безопаснее.
Польза для разработчиков
Программист тратит меньше усилий на понимание скриптов: не нужно разбираться, что делает конкретная функция и почему она так называется. Это снижает усталость и количество ошибок, код становится более отказоустойчивым.
Польза для команд
В команде код чаще читают, чем пишут. Maintable-код ускоряет адаптацию новых сотрудников и практики код-ревью, когда разработчики читают код друг друга и предлагают улучшения. В долгосрочных проектах такие практики необходимы.
Польза для бизнеса
С точки зрения бизнеса, поддерживаемый код — это инвестиция. Он редко даёт мгновенный эффект, но в итоге сильно снижает затраты в долгосрочной перспективе: скорость и предсказуемость разработки растёт, новые возможности добавляются быстрее, количество багов и ошибок снижается. Это напрямую влияет на финансовую выгоду.
Полезный блок со скидкой
Если вам интересно разбираться со смартфонами, компьютерами и прочими гаджетами и вы хотите научиться создавать софт под них с нуля или тестировать то, что сделали другие, — держите промокод Практикума на любой платный курс: KOD (можно просто на него нажать). Он даст скидку при покупке и позволит сэкономить на обучении.
Бесплатные курсы в Практикуме тоже есть — по всем специальностям и направлениям, начать можно в любой момент, карту привязывать не нужно, если что.
Основные принципы чистого кода
Принципы maintable-кода — не строгие правила, а ориентиры для принятия решений. Если фрагмент программы можно написать по-разному, принципы помогут определиться и сделать лучший выбор.
Один из самых известных наборов таких ориентиров — SOLID.
Чтобы проиллюстрировать принципы, мы возьмём простые примеры кода на Python.
Принципы SOLID
SOLID — набор из пяти принципов объектно-ориентированного проектирования. С ними можно сделать код гибким и расширяемым, упростить поддержку и убрать лишние зависимости между разными фрагментами логики.
S — единственная ответственность (Single Responsibility)
Правило: модуль или класс должны делать что-то одно. Если один и тот же кусок кода отвечает за две разные функциональности, принцип единой ответственности нарушается. Если изменить одну часть логики, есть вероятность сломать вторую. Например, плохо, если одна функция одновременно проверяет введённый адрес электронной почты и сохраняет пользователя.
Как это может выглядеть, если всё делать правильно:
# проверяем корректность email
def validate_email(email: str) -> None:
# проверяем наличие символа @
if "@" not in email:
# выбрасываем ошибку
raise ValueError("invalid email")
# сохраняем пользователя
def save_user(email: str) -> None:
# имитируем сохранение
print("user saved:", email)
# вызываем проверку
validate_email("user@example.com")
# вызываем сохранение
save_user("user@example.com")
Здесь одна функция проверяет данные, другая — выполняет действие. Так часто делают при регистрации пользователей или обработке форм.
Запускаем в консоли и смотрим на результат:
user saved: user@example.com
O — принцип открытости/закрытости (Open/Closed Principle)
Код должен быть открыт для возможных улучшений, но закрыт для изменений. Чтобы соблюдать это правило, новые функции нужно создавать без изменения уже существующих частей программы.
Для примера возьмём два класса, которые возвращают цену со скидкой и без. Вместо добавления 2-го метода в уже созданный класс мы сделали отдельный:
# обычная скидка
class RegularDiscount:
# возвращаем цену без изменений
def apply(self, price: int) -> int:
return price
# премиум-скидка
class PremiumDiscount:
# уменьшаем цену на 10 процентов
def apply(self, price: int) -> int:
return int(price * 0.9)
# создаём стратегию скидки
discount = PremiumDiscount()
# применяем скидку
print(discount.apply(1000))
Теперь запустим код и применим скидку в 10 процентов к цене в 1000 единиц:
900
L — принцип подстановки Лисков (Liskov Substitution Principle)
Принцип со сложным названием описывает правило родительских и наследуемых классов. Смысл в том, чтобы классы-наследники вели себя логично.
Родительский класс — это шаблон объекта, который мы создаём как основной. Например, таким классом может быть «человек», который может «работать», «есть» и «спать». Наследуемый класс по умолчанию перенимает все признаки родительского, но может иметь свои уникальные. Класс «программист» может наследоваться от «человека», но дополнительно к функциям «работать», «есть» и «спать» умеет «писать код».
Если допустить ошибку, наследуемые классы могут перенять неправильные методы-функции. Зоологический пример: если создать основной класс для описания птиц Bird и присвоить ему метод «летать», то получится, что все наследники этого класса должны уметь летать. Но это не так, например, для пингвинов и страусов.
Неправильный вариант решения этой проблемы — вставлять в программу дополнительные проверки. Лучше сразу сделать так, чтобы поведение программы было логичным.
Так будет выглядеть правильный код, который следует этому принципу:
# базовый класс птицы
class Bird:
# птица умеет есть
def eat(self):
print("eating")
# летающая птица
class FlyingBird(Bird):
# летающая птица умеет летать
def fly(self):
print("flying")
# создаём утку
duck = FlyingBird()
# вызываем общий метод
duck.eat()
# вызываем полёт
duck.fly()
Посмотрите: мы создаём общий класс Bird. Внутри мы даём ему только одну функцию, которая наверняка будет у всех его объектов-наследников: он может есть. Теперь, если создать наследника duck от этого класса и добавить ему ещё один метод, наследный объект будет уметь делать действие родителя.
Проверяем, как работает программа:
eating
flying
I — принцип разделения интерфейса (Interface Segregation)
Лучше сделать несколько простых интерфейсов, чем один сложный. В чём смысл: не надо смешивать в одном классе много разных методов, часть из которых может быть не нужна.
Что делаем в примере: объявляем метод у класса и делаем его пустым. Для каждого конкретного класса метод можно переопределить при необходимости:
# интерфейс чтения данных
class Reader:
# объявляем метод чтения
def read(self) -> str:
pass
# реализация только чтения
class FileReader(Reader):
# возвращаем данные
def read(self) -> str:
return "data from file"
# создаём объект
reader = FileReader()
# используем только нужный метод
print(reader.read())
Так клиент может использовать только то, чем он пользуется. Проверяем, как всё работает:
data from file
D — принцип инверсии зависимостей (Dependency Inversion)
Правило: бизнес-логика не должна быть привязана к конкретному способу сохранения данных.
Это упрощает тестирование и замену реализаций. Например, можно легко подменить реальную базу на фейковую в тестах.
Вот как это может проявиться. Программист создаёт функцию для регистрации пользователя. Сегодня пользователь сохраняется в список, завтра — в базу данных, ещё через неделю — в API другого сервиса. Если логика регистрации жёстко завязана на какой-то один способ сохранения, код придётся переписывать.
Вот упрощённый пример плохой функции, которая сразу выполняет действие. В нашем случае она просто выводит его на экран, но это имитирует конкретную работу. На месте оператора print() могло быть сохранение в список или базу данных:
# регистрируем пользователя
def register_user(email: str):
# сохраняем пользователя сразу здесь
print("saved:", email)
Лучше сделать так:
# регистрируем пользователя
def register_user(email: str, save_func):
# используем переданную функцию сохранения
save_func(email)
Обратите внимание, что функция регистрации register_user вызывает другую функцию для сохранения пользователя save_func, но не знает механизма её работы.
Методики написания чистого кода
Методики — простые привычки, которые можно применять на любом проекте. Со временем они закрепляются и делают программу сильнее.
Структурирование и именование
Названия переменных, функций и классов должны отвечать на вопрос: «Что делает эта часть кода?». Такие имена помогают при чтении и доработке проекта, когда нужно разобраться в схеме работы.
Если использовать понятные имена, дополнительные комментарии не нужны:
# базовая цена товара
base_price = 1000
# размер скидки
discount = 200
# итоговая цена
final_price = base_price - discount
Структура проекта тоже имеет значение. Если файлы и папки отражают содержание, ориентироваться в программе проще. В структуре ниже сразу видно, какие скрипты обрабатывают работу с пользователями, заказами и оплатами:
project/
├── users/
│ ├── service.py
│ └── repository.py
├── orders/
│ ├── service.py
│ └── repository.py
├── payments/
└── service.py
Функции и их особенности
Функции должны быть компактными и должны выполнять одно действие. Признак слишком сложной функции — для неё сложно придумать конкретное имя.
Пример функции с предсказуемым поведением — проверка прав администратора.
# проверяем, является ли пользователь администратором
def has_access(is_admin: bool) -> bool:
# администратор всегда имеет доступ
if is_admin:
return True
# иначе доступ запрещён
return False
# проверяем доступ
print(has_access(True))
Запускаем и проверяем, что всё работает:
True
Комментарии и их использование
Комментарии — одна из форм документации проекта.
Для написания комментариев тоже есть правило: они должны объяснять не «что здесь написано», а «почему именно так». Без пояснения условие ниже можно не сразу понять:
# используем <=, потому что заказ можно отменить в течение ровно 24 часов
if hours_passed <= 24:
cancel_order()
С комментарием понятно, что это осознанное бизнес-правило.
Форматирование кода
Единый стиль форматирования особенно важен в команде и больших проектах, чтобы при чтении не тратить время на особенности написания кода у каждого отдельного программиста. Ещё это делает код предсказуемым.
Полезные инструменты для этого — автоформаттеры и линтеры.
Практические советы по написанию поддерживаемого кода
Вот ещё несколько советов, которые влияют на качество кода в перспективе. Они помогают держать код в форме по мере роста проекта. Ещё их можно применять постепенно, улучшая уже существующий сервис.
Избегайте избыточности (DRY)
По-простому это означает: не повторяйтесь. Если одна и та же логика просто копируется в нескольких местах, рано или поздно они начнут расходиться. Вместо этого один раз напишите функцию или метод и используйте их в любой части программы.
Простой пример: эта проверка может использоваться в разных местах кода. Достаточно подставить на место аргумента value при вызове переменную, которую хотите проверить:
# проверяем, что значение положительное
def require_positive(value: int):
if value <= 0:
raise ValueError("must be positive")
Соблюдайте принципы модульности
Изменение кода в одной части не должно ломать другие. Поэтому разные модули кода должны быть как можно слабее связаны внутри.
Опасно делать так, если расчёты скидки и стоимости товара будут вызывать друг друга. Если функциональность кода пересекается, получаются взаимозависимости и потенциальные угрозы: поломка в одном фрагменте сервиса вызовет другие по цепочке.
Это правило хорошо отражено в событийной архитектуре, когда одна большая программа выглядит как модульный код с большим количеством микросервисов, которые напрямую друг с другом они не связаны. Вместо этого они по отдельности реагируют на разные события в системе.
Для примера поделим две разные задачи на две разные функции. Одна считает сумму, другая — проводит оплату:
# рассчитываем итоговую сумму
def calculate_total(price: int, tax: int) -> int:
return price + tax
# выполняем оплату
def pay(amount: int):
print("paid:", amount)
# считаем сумму
total = calculate_total(1000, 100)
# оплачиваем
pay(total)
Тестирование и юнит-тесты
Тесты — это страховка от поломок. Они позволяют менять код и быстро понимать, что что-то пошло не так.
Тесты полезны и чаще всего необходимы для бизнес-логики, потому что фиксируют рабочее поведение системы. Например, вы проводите рефакторинг — корректировку и оптимизацию кодовой базы. С тестами во время этого процесса вы можете контролировать все существующие функции.
Рефакторинг кода
Рефакторинг — не переписывание с нуля, а небольшие улучшения. Поведение программы при этом не меняется.
Рефакторинг лучше делать регулярно. Если откладывать, накапливается технический долг: временные и устаревшие решения. Если техдолг высокий, изменения становятся сложными и долгими.
Вредные практики, которых следует избегать
Для закрепления посмотрим на тему с другой стороны. Некоторые практики могут быть удобными временно, но позже способны вызвать проблемы. Лучше избавляться от них сразу, как только видите.
Чрезмерные комментарии. Когда код покрыт комментариями к каждой строке, читать его становится сложнее. К тому же они устаревают быстрее кода. Если логика сложная, лучше упростить сам код.
Исключение — учебные примеры, которые должны объяснять работы программ максимально подробно и просто.
Сложные конструкции и вложенности. Это про условия выполнения if, for и try. Если внутри одного есть ещё много других уровней таких проверок, читать код становится сложно: нужно держать в уме все предыдущие условия.

Если код более модульный, зависимость фрагментов друг от друга падает, а отказоустойчивость и надёжность, наоборот, возрастают.
Неоптимальные имена. Хорошее имя экономит время при чтении и снижает количество комментариев. Если переменной или функции сложно дать имя — возможно, она делает слишком много или живёт не там, где должна.
Бонус для читателей
Если вам интересно погрузиться в мир ИТ и при этом немного сэкономить, держите наш промокод на курсы Практикума. Он даст вам скидку при оплате, поможет с льготной ипотекой и даст безлимит на маркетплейсах. Ладно, окей, это просто скидка, без остального, но хорошая.
