
Рефакторинг кода: пример с проверками до и после изменений
Рефакторинг кода меняет его внутреннее устройство при сохранении наблюдаемого поведения. Посмотрим на небольшой функции Python, как отделить такую работу от исправления ошибки или добавления новой возможности. Материал подготовила редакция Mentoring.digital.
Содержание статьи
Что считать сохранённым поведением
Для вызывающего кода важны возвращаемое значение, тип и условия исключений, изменения переданных объектов и другие побочные эффекты. Если программа пишет в файл, вызывает сервис или отправляет сообщение, эти действия тоже входят в проверяемое поведение. В нашем учебном примере побочных действий нет: функция только проверяет аргументы и возвращает стоимость доставки.
Цель изменения конкретная: убрать вложенные ветвления и отделить проверку входа от расчёта. Мы не меняем тариф, допустимые аргументы и сообщения об ошибках. Исправление спорного правила следует делать отдельным изменением с новым ожидаемым результатом.
Исходная функция и условия учебной задачи
Все суммы ниже заданы для учебной задачи и не описывают тарифы реального магазина. Аргумент total должен иметь тип int, но не bool, и быть неотрицательным. Аргумент urgent должен иметь тип bool. При сумме от 5000 базовая доставка бесплатна, ниже порога она стоит 300. Срочность добавляет 200.
def shipping_before(total: int, urgent: bool = False) -> int:
if type(total) is not int:
raise TypeError("total must be int")
if type(urgent) is not bool:
raise TypeError("urgent must be bool")
if total < 0:
raise ValueError("total must be non-negative")
if urgent:
if total >= 5000:
return 200
else:
return 500
else:
if total >= 5000:
return 0
else:
return 300| total | urgent | Ожидаемый результат |
|---|---|---|
| 0 | False | 300 |
| 4999 | True | 500 |
| 5000 | False | 0 |
| 5000 | True | 200 |
В Python bool является подклассом int. Поэтому здесь намеренно используется проверка точного типа type(total) is int: это часть выбранного контракта. Замена на isinstance(total, int) пропустит логическое значение и изменит допустимые входы.
Сначала добавляем проверку, затем меняем структуру
Для начала сохраните исходную функцию в файле refactor_example.py. Рядом разместите новую реализацию. Тест ниже запустите из файла test_shipping.py в той же папке командой python -m unittest -v test_shipping. До правки полезно получить проходящий тест на исходный контракт, чтобы не сравнивать две одинаково ошибочные версии только друг с другом.
import unittest
from refactor_example import shipping_before, shipping_after
class ShippingTest(unittest.TestCase):
def test_contract(self):
cases = [(0, False, 300), (4999, True, 500),
(5000, False, 0), (5000, True, 200)]
for total, urgent, expected in cases:
with self.subTest(total=total, urgent=urgent):
self.assertEqual(shipping_before(total, urgent), expected)
self.assertEqual(shipping_after(total, urgent), expected)
def test_errors(self):
for args in [(-1, False), (True, False), (1.0, False),
("5000", False), (None, False), (10, 1), (-1, 1)]:
errors = []
for function in (shipping_before, shipping_after):
try:
function(*args)
except Exception as error:
errors.append((type(error), str(error)))
self.assertEqual(len(errors), 2)
self.assertEqual(errors[0], errors[1])Первая проверка задаёт известные правильные ответы. Вторая сравнивает ошибки двух версий. Для (-1, 1) срабатывает проверка типа urgent, потому что она расположена до проверки отрицательной суммы. Даже перестановка внешне независимых условий может изменить наблюдаемую ошибку.
Рефакторинг: выделяем проверку и упрощаем расчёт
def validate_order(total: int, urgent: bool) -> None:
if type(total) is not int:
raise TypeError("total must be int")
if type(urgent) is not bool:
raise TypeError("urgent must be bool")
if total < 0:
raise ValueError("total must be non-negative")
def shipping_after(total: int, urgent: bool = False) -> int:
validate_order(total, urgent)
base = 0 if total >= 5000 else 300
surcharge = 200 if urgent else 0
return base + surchargeПосле выделения validate_order расчёт читается как два независимых компонента: базовая стоимость и доплата за срочность. Порог по-прежнему включён в бесплатную базовую доставку. Порядок проверок, классы исключений и сообщения сохранены.
Такое преобразование удобно выполнять небольшими шагами. Сначала вынесите проверки без перестановок и запустите тесты. Затем замените вложенные ветвления вычислением двух величин и повторите проверку. Если тест упал, сравнивать приходится меньший фрагмент изменения.
Как расширить проверку и не выдать её за доказательство
Для сравнения версий можно перебрать ряд допустимых сумм и отдельно проверить значения по обе стороны порога. Редакция выполнила сравнение на 548 сочетаниях суммы и признака срочности, а также на семи ошибочных входах. Все сравнения прошли. Это проверка конечного набора случаев, а не математическое доказательство эквивалентности для любого возможного объекта Python.
values = sorted(set(range(0, 10001, 37)) | {4999, 5000, 5001})
for total in values:
for urgent in (False, True):
assert shipping_before(total, urgent) == shipping_after(total, urgent)Если настоящая функция зависит от времени, базы данных или внешнего API, такого цикла недостаточно. Нужно зафиксировать входные условия и проверить соответствующие взаимодействия, а при наличии интеграционных сценариев повторить их. Не запускайте реальные оплаты и отправки ради тестирования рефакторинга.
Когда стоит остановиться и выделить другую задачу
- Обнаружено неверное бизнес-правило. Зафиксируйте его отдельным случаем и согласуйте новый ответ, вместо незаметного изменения вместе с перестройкой кода.
- Новая версия меняет возвращаемый тип или формат ошибки. Это изменение контракта, даже если строк стало меньше.
- Нельзя воспроизвести важный сценарий. Запишите ограничение и найдите безопасное тестовое окружение до переноса изменения.
- Переименование или выделение метода не объясняет код лучше. Число функций само по себе не является показателем качества.
Результат этого упражнения оценивается по двум признакам: вычисление стало проще читать, а проверенное поведение сохранилось. Чтобы перенести подход на свой проект, подготовьте небольшой фрагмент, его контракт и сценарии. Обсудить их можно на разборе кода с наставником или выбрать обучение код-ревью.
Оценки и отзывы о статье
Отзывы читателей0
Здесь пока нет отзывов. Расскажите, чем вам помогла статья.

