Недавно я начал работать над проектом, чья история Git восходит к 2019 году, а коммиты были разбиты на несколько месяцев, после чего последовал длительный перерыв. Не говоря уже об отсутствии рабочего процесса и наличии ни разу не объединенных веток.

Исходный код отражал свободную структуру Git. Несмотря на то, что код работал — пайплайны были завершены, все тесты пройдены и в конце сборки был получен файл колеса, который могли использовать наши заинтересованные стороны — сам исходный код нарушал многие принципы SOLID и другие передовые методы, которые сделали сложно добавлять новые функции, отлаживать и даже читать.

Я много раз сталкивался с таким кодом. Я всегда изо всех сил пытался понять, что он делает. Если часть кода читабельна, я немного борюсь. Если это не читается, я борюсь еще больше.

Имея это в виду, я некоторое время пытался рефакторить «нечитаемый» код не потому, что я эксперт по чистому коду или принципам SOLID, а потому, что считаю себя очень медленным учеником.

Поэтому вот несколько советов о том, как изменить код, чтобы вы могли с большей уверенностью прикасаться к нему, но не ломая его, чтобы не тратить больше времени на исправление того, что вы до сих пор не понимаете до конца.

1. Пишите тесты

Первое, что я делаю, это пытаюсь понять, как работает код с точки зрения функциональности. Если есть тесты, я их проверяю. Если тестов нет, я начинаю включать новые, чтобы понять, что делают методы, в каком порядке они вызываются, каковы их зависимости и так далее.

⚠️ Если тестов вообще нет, очень важно, чтобы вы настроили хотя бы один интеграционный тест и подтвердили правильность ввода. Этот тест поможет вам определить, не сломали ли вы что-нибудь во время рефакторинга.

2. Разделить монолитные скрипты

Если вы не знаете код, очень вероятно, что вы будете много его отлаживать и читать. Таким образом, во время отладки становится намного проще ориентироваться, если вы разделите классы с разными обязанностями и использованием на отдельные файлы .py. Затем вы можете просто пропустить класс, если вы уже знаете все его содержимое во время отладки.

У этого рефакторинга есть только один компромисс: создав новый скрипт, вы потеряете всю историю git в отношении этого класса, если только вы не проверите старый скрипт там, где он был написан изначально. (Кроме того, вы станете автором только что скопированного и вставленного кода.)

⚠️ Не забудьте импортировать содержимое скриптов, где они используются. PyCharm поможет вам определить, где отсутствует оператор импорта. Остерегайтесь кругового импорта во время этого процесса.

3. Определите классы данных

Обычно классы служат единственной цели хранения данных. Эти классы можно легко преобразовать в модели Pydantic. Модели Pydantic гораздо более удобочитаемы и предоставляют множество инструментов, чтобы сделать ваш код более надежным. Обязательно используйте его, когда это возможно. Это сэкономит вам много энергии.

4. Добавьте строки документации

Изучите способы документирования кода Python, выберите способ, создайте стандарт и используйте его везде.

Я решил, что всегда буду документировать, используя Sphinx, и что строки документации всегда будут отформатированы как reStructuredText.

Если я вижу метод, цель которого неясна или в основе которого лежит предположение о его реализации (например, упрощение моделирования), я документирую его.

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

5. Рефакторинг имен переменных и методов

Очень полезно читать код, когда вы знаете, с какими переменными работает ваш код. Дайте осмысленные имена переменным, это поможет читать код и перемещаться по нему.

✅ Обязательно используйте инструменты рефакторинга IDE. PyCharm делает это правильно для вас. Вы просто должны быть уверены, что выбрали хорошее имя.

6. Добавьте подсказки типа

Просто добавьте их.

Если в какой-то момент код нарушает предоставленные вами подсказки типов, вы получите предупреждение (а также сведения о возможной ошибке или предположении, о которых вы раньше не догадывались).

Неправильные подсказки типов не нарушают ваш код.

Пример

Я создал простой пример того, как применять большинство из этих методов рефакторинга.

В настоящее время я работаю над симулятором робототехники на Python/Rust. Здесь мы будем использовать метод, который вычисляет прямую кинематику робота.

Если бы он был плохо написан, он бы выполнял свою работу и был бы немного нечитаемым, например:

def fk(L, q):
    return L*sin(q), L*cos(q)

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

Но давайте подойдем к этому как к человеку, который не разбирается в робототехнике.

1. Создайте тест

Нам просто нужно гарантировать, что мы продолжаем получать то, что выводит код.

import unittest
import numpy as np

def fk(L, q):
    return L * np.sin(q), L * np.cos(q)

class TestRobotics(unittest.TestCase):
    def test_forward_kinematics_1_degree_of_freedom(self):
        length = 1
        angle = np.pi * 30 / 180
        expected = (0.5, 0.866)
        result = fk(length, angle)
        np.testing.assert_almost_equal(expected, result, 2)

if __name__ == '__main__':
    unittest.main()

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

2. Разделить монолитные скрипты

Конечно, в нашем примере метод состоит только из двух строк. Там нечего разбивать. Но вы наверняка можете переместить его в другое место нашего тестового класса, например, в новый скрипт с именем kinematics.py.

3. Определите классы данных

Здесь это неприменимо, потому что у нас нет класса. Но в будущем я приведу еще один пример, включающий этот шаг.

4. Добавьте строки документации

Это очень важно. Наш метод принимает углы в радианах, и это совсем не ясно. Также длина должна быть указана в метрах. Да, наш метод работает только в единицах СИ.

Наш метод также действителен только для плоских роботов с 1 степенью свободы (то есть рычажного механизма).

Наконец, он выводит результаты в виде кортежа. Мы не уверены в привязке к системе координат, но я предполагаю, что она соответствует соглашению о положительной оси Y вверх и положительной оси X вправо, поэтому повороты положительны, если они против часовой стрелки. Тем не менее, мы не можем быть уверены, пока не углубимся в то, как код используется дальше.

Вся эта информация имеет решающее значение для пользователя. Если бы использовалась другая система координат, кто-то из поля также был бы сбит с толку, если бы это нигде не было указано. Например, положительная ось Y может быть направлена ​​вниз или положительные углы по часовой стрелке, как это делает Unity.

Наш задокументированный метод становится:

def fk(L, q):
    """
    Compute the forward kinematics of a planar 1-degree-of-freedom     
    robot. Inputs must be in SI units.
    :param L: Robotic link length
    :param q: Joint angle
    :return: Tuple with the Y and X coordinates
    """
    return L * np.sin(q), L * np.cos(q)

Вы можете задокументировать его в соответствии со стандартом или стилем, который вам нравится больше всего.

5. Рефакторинг переменных и имен методов

Это очень интуитивно понятно и приятно. После этого вы можете почти прочитать код, как если бы это был документ.

def compute_forward_kinematics(L, q):
    """
    Compute the forward kinematics of a planar 1-degree-of-freedom     
    robot. Inputs must be in SI units.
    :param L: Robotic link length
    :param q: Joint angle
    :return: Tuple with the Y and X coordinates
    """
    x = L * np.cos(q)
    y = L * np.sin(q)
    position = (y, x)
return position

Обратите внимание, что я не переименовывал переменные длины и угла. Их имена, которые не доступны для поиска и не несут никакой смысловой нагрузки, противоречат практике чистого кода. Однако их использование очень ограничено, и люди в этой области очень хорошо знакомы с этой записью, используя L и q. Учитывая контекст, нам не обязательно переименовывать их. Наша строка документации также помогает прояснить значение ваших «плохо названных» переменных в любом случае.

6. Добавьте подсказки типа

Наша сигнатура метода становится:

def compute_forward_kinematics(L: float, q: float) -> Tuple[float, float]:

Окончательный фрагмент кода

import unittest
from typing import Tuple
import numpy as np

def compute_forward_kinematics(L: float, q: float) -> Tuple[float, float]:
    """
    Compute the forward kinematics of a planar 1-degree-of-freedom
    robot. Inputs must be in SI units.
    :param L: Robotic link length
    :param q: Joint angle
    :return: Tuple with the Y and X coordinates
    """
    x = L * np.cos(q)
    y = L * np.sin(q)
    position = (y, x)
    return position

class TestRobotics(unittest.TestCase):
    def test_forward_kinematics(self):
        length = 1
        angle = np.pi * 30 / 180
        expected = (0.5, 0.866)
        result = compute_forward_kinematics(length, angle)
        np.testing.assert_almost_equal(expected, result, 2)

if __name__ == '__main__':
    unittest.main()

Сценарий выше должен работать как есть.