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

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

Итак, с чего начать? Эта статья содержит 5 простых вещей, которые вы можете сделать прямо сейчас, чтобы сделать ваш код более читабельным и действительно красивым.

1. Удалите закомментированный код.

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

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

# lots of
#
# commented out lines of code
do_something_important()
#
# even more
# commented out code

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

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

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

2. Извлечение магических чисел и строк

Во время разработки часто бывает проще написать строковые литералы и числа непосредственно в коде. Однако оставление их есть рецепт для проблем в будущем.

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

Другая проблема заключается в том, что голое число, подобное тому, что в этом примере, не говорит вам, почему оно имеет значение или для чего оно предназначено.

def update_text(text):
    if len(text) > 80:
        wrap(text)

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

MAX_LINE_LENGTH = 80
def update_text(text):
    if len(text) > MAX_LINE_LENGTH:
        wrap(text)

Когда дело доходит до фактического внесения изменений, многие IDE помогут вам извлечь литералы с помощью опции «Извлечь константу».

3. Удалите дублирование

DRY (Не повторяйтесь) — один из основополагающих принципов разработки программного обеспечения. Дублирование затрудняет понимание кода и часто приводит к ошибкам, когда дублированный код начинает расходиться.

Подъем кода

Один из простых способов устранить дублирование — если вы заметите возможность подъема кода. Здесь код повторяется в обеих ветвях условного оператора, поэтому его можно вывести наружу.

if sold > DISCOUNT_AMOUNT:
    total = sold * DISCOUNT_PRICE
    label = f'Total: {total}'
else:
    total = sold * PRICE
    label = f'Total: {total}'

Взяв присваивание label за пределы условного оператора, мы удалили повторяющуюся строку кода и прояснили, что на самом деле контролирует условное выражение, то есть общее.

if sold > DISCOUNT_AMOUNT:
    total = sold * DISCOUNT_PRICE
else:
    total = sold * PRICE
label = f'Total: {total}'

Затем это позволяет нам провести дальнейший рефакторинг, если мы захотим, заменив условное выражение выражением if. Если вам нужен инструмент, который может выполнять такие рефакторинги автоматически, попробуйте Sourcery.

total = sold * DISCOUNT_PRICE if sold > DISCOUNT_AMOUNT else sold * PRICE
label = f'Total: {total}'

Извлечение функций

Чаще код дублируется в разных частях функции или в двух разных функциях.

Здесь вам нужно будет извлечь код в другую функцию, дать ей осмысленное имя, а затем вызвать ее вместо извлеченного кода.

class Game:

    # ...

    def was_correctly_answered(self):
        if self.not_penalised():
            print('Answer was correct!!!!')
            self.purses[self.current_player] += 1
            print(self.players[self.current_player] +
                ' now has ' +
                str(self.purses[self.current_player]) +
                ' Gold Coins.')

            winner = self._did_player_win()
            self.current_player += 1
            if self.current_player == len(self.players):
                self.current_player = 0

            return winner
        else:
            self.current_player += 1
            if self.current_player == len(self.players):
                self.current_player = 0
            return True


    def wrong_answer(self):
        print('Question was incorrectly answered')
        print(self.players[self.current_player] + " was sent to the penalty box")
        self.in_penalty_box[self.current_player] = True

        self.current_player += 1
        if self.current_player == len(self.players):
            self.current_player = 0
        return True

Этот код взят (слегка изменен) из trivia kata, опубликованного под лицензией GPLv3. Просматривая его, можно увидеть один явный фрагмент дублированного кода, который появляется в трех местах:

self.current_player += 1
if self.current_player == len(self.players):
    self.current_player = 0

Это должно быть извлечено в функцию. Многие IDE позволяют выбирать фрагменты кода и извлекать их автоматически, а некоторые, такие как PyCharm, также сканируют ваш код, чтобы увидеть, какие части можно заменить вызовом новой функции. Давайте извлечем этот метод и назовем его next_player, так как он перемещает current_player к следующему допустимому значению.

class Game:

    # ...

    def was_correctly_answered(self):
        if self.not_penalised():
            print('Answer was correct!!!!')
            self.purses[self.current_player] += 1
            print(self.players[self.current_player] +
                ' now has ' +
                str(self.purses[self.current_player]) +
                ' Gold Coins.')

            winner = self._did_player_win()
            self.next_player()

            return winner
        else:
            self.next_player()
            return True


    def wrong_answer(self):
        print('Question was incorrectly answered')
        print(self.players[self.current_player] + " was sent to the penalty box")
        self.in_penalty_box[self.current_player] = True

        self.next_player()
        return True


    def next_player(self):
        self.current_player += 1
        if self.current_player == len(self.players):
            self.current_player = 0

В этом коде предстоит еще много рефакторинга, но он определенно улучшился. Он читается более четко, и если функция next_player() должна измениться, нам нужно изменить код только в одном месте, а не в трех.

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

4. Разделите большие функции

Чем длиннее функция, тем сложнее ее читать и понимать. Важным аспектом написания хороших функций является то, что они должны делать одну вещь и иметь имя, которое точно отражает то, что они делают — максима, известная как Принцип единой ответственности. Более длинные функции более склонны выполнять множество разных действий.

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

Чтобы разделить функцию, вам нужно будет извлечь ее части в другие функции, как описано в предыдущем разделе.

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

def make_tea(kettle, tap, teapot, tea_bag, cup, milk, sugar):
    kettle.fill(tap)
    kettle.switch_on()
    kettle.wait_until_boiling()
    boiled_kettle = kettle.pick_up()
    teapot.add(tea_bag)
    teapot.add(boiled_kettle.pour())
    teapot.wait_until_brewed()
    full_teapot = teapot.pick_up()
    cup.add(full_teapot.pour())
    cup.add(milk)
    cup.add(sugar)
    cup.stir()
    return cup

Здесь мы видим, что вызовы разделились на три группы — те, которые вызывают методы для чайника, чайника и затем чашки. Исходя из наших знаний в данной области (здесь может помочь британское гражданство), они также соответствуют трем этапам приготовления чашки чая: кипячение чайника, заваривание чая, затем наливание его в чашку и подача.

Давайте начнем с конца и выделим строки, которые относятся к наливанию чая и подаче его на стол. Вы можете сделать это вручную или с помощью функции «Извлечь метод» вашей IDE.

def make_tea(kettle, tap, teapot, tea_bag, cup, milk, sugar):
    kettle.fill(tap)
    kettle.switch_on()
    kettle.wait_until_boiling()
    boiled_kettle = kettle.pick_up()
    teapot.add(tea_bag)
    teapot.add(boiled_kettle.pour())
    teapot.wait_until_brewed()
    full_teapot = teapot.pick_up()
    return pour_tea(cup, full_teapot, milk, sugar)


def pour_tea(cup, full_teapot, milk, sugar):
    cup.add(full_teapot.pour())
    cup.add(milk)
    cup.add(sugar)
    cup.stir()
    return cup

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

def make_tea(kettle, tap, teapot, tea_bag, cup, milk, sugar):
    boiled_kettle = boil_water(kettle, tap)
    full_teapot = brew_tea(boiled_kettle, tea_bag, teapot)
    return pour_tea(cup, full_teapot, milk, sugar)


def boil_water(kettle, tap):
    kettle.fill(tap)
    kettle.switch_on()
    kettle.wait_until_boiling()
    return kettle.pick_up()


def brew_tea(boiled_kettle, tea_bag, teapot):
    teapot.add(tea_bag)
    teapot.add(boiled_kettle.pour())
    teapot.wait_until_brewed()
    return teapot.pick_up()


def pour_tea(cup, full_teapot, milk, sugar):
    cup.add(full_teapot.pour())
    cup.add(milk)
    cup.add(sugar)
    cup.stir()
    return cup

Глядя на функцию верхнего уровня make_tea, вы теперь можете прочитать три верхних этапа приготовления чая, как небольшую историю. Если вас интересуют детали любого этапа, вы можете перейти к соответствующему методу.

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

5. Переместите объявления локальных переменных ближе к местам их использования.

При чтении и понимании кода вы должны сохранять переменные, с которыми сталкиваетесь, и их состояние в кратковременной памяти. Большинство взрослых могут хранить около 7 вещей одновременно.

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

def assess_fruit(self, fruit):
    happiness = 0
    hunger = time_since_breakfast() / size_of_breakfast()
    some_other_code()
    # work out some things
    do_some_other_things()
    if is_snack_time() and isinstance(fruit, Apple):
        yumminess = fruit.size * fruit.ripeness ** 2
        happiness += hunger * yumminess
    return happiness

Читая этот код, вы должны помнить о переменной hunger от начала до конца функции, так как ее можно использовать или изменить где угодно.

def assess_fruit(self, fruit):
    happiness = 0
    some_other_code()
    # work out some things
    do_some_other_things()
    if is_snack_time() and isinstance(fruit, Apple):
        hunger = time_since_breakfast() / size_of_breakfast()
        yumminess = fruit.size * fruit.ripeness ** 2
        happiness += hunger * yumminess
    return happiness

Рефакторинг для перемещения его в область, где он используется, решает эту проблему. Теперь нам нужно только подумать о hunger в соответствующем контексте.

Первоначально опубликовано на https://sourcery.ai.