Введение

Я читал книги «Чистый код» (CC) и «Практическое объектно-ориентированное проектирование в Ruby» (POODR). Одной из общих тем является важность содержания вашего кода в чистоте. Вы очищаете свой код сейчас, чтобы его было легче поддерживать и изменять в будущем. Обе книги содержат рекомендации о том, как это сделать.

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

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

Задний план

Прежде чем я перейду к примерам, я хотел бы предоставить некоторую предысторию. Я буду обсуждать программу Tic Tac Toe, над которой я работал. По сути, программа позволяет пользователю играть в игру Tic Tac Toe (TTT) на доске 3x3, используя консольный ввод и вывод. Пользователь может выбрать игру против другого пользователя (человек против человека или HvH). Пользователь также может играть против компьютера (человек против человека или HvC).

Вот краткий обзор классов, которые я использую:

  • Доска: представляет доску TTT. Также проверяет ходы на доске.
  • ScoreTracker: сообщает, выиграл ли игрок.
  • IOHandler: обрабатывает ввод и вывод (получение ввода от пользователя и отображение вывода на консоли).
  • Контроллер: создает необходимые объекты и играет в игру TTT.

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

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

Когда вам не терпится изменить свой код, иногда лучше посидеть и подождать новой информации. Эта информация может поступать в виде требований или спецификаций от вашего клиента (или «заказчика», или «конечного пользователя»). Иногда изменение, которое желает клиент, является именно той информацией, которая необходима вам для уточнения раздела вашего кода.

В случае с моей программой TTT я первоначально отображал подсказки для игрока-человека следующим образом:

Player 1, please enter your move: t
   | x |    
---+---+---
   |   |   
---+---+---
   |   |   
Player 2, please enter your move: b
   | x |    
---+---+---
   |   |   
---+---+---
   | o |

В этой версии игроку-человеку, который ходит первым, автоматически назначается «Игрок 1», и я использовал номера игроков (вместе с общим количеством сделанных ходов), чтобы определить, кто из игроков в итоге выиграл.

Однако моему клиенту это не понравилось. Он обратил внимание на то, что создание ментальной карты из игрока «1», имеющего ход «x», и игрока «2», имеющего ход «o», сбивает с толку. Мой клиент предложил вместо этого отображать «Player x» и «Player o». Это приведет к следующему результату:

Player x, please enter your move: t
   | x |    
---+---+---
   |   |   
---+---+---
   |   |
Player o, please enter your move: b
   | x |    
---+---+---
   |   |   
---+---+---
   | o |

Я согласился и принялся за дело.

Изменения кода

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

Я покажу, как я изменил метод play_game из моего класса Controller. Вот упрощенная версия метода:

def play_game
  # display initial output
  # while a player hasn't won yet and there are still moves left, 
  # keep playing a round of the game
  while (!has_player_won) && (num_available_moves > 0)
    player_num = set_player_num_based_on_move(move_number)
    play_round(player_num, playing_computer)
    player_char = set_player_char(player_num)
    has_player_won = ScoreTracker.has_player_won?(player_char)
    move_number += 1
    num_available_moves = board.get_available_moves.length
  end
  # display message when the game is over
end

По сути, этот метод отображает начальный вывод для пользователя, играет в игру TTT, а затем отображает сообщение в конце. Часть, которую я изменил, — это цикл while в середине.

Обратите внимание, что изначально я установил player_num, или номер игрока, в зависимости от того, какой это ход (player_num зависит от того, является ли ход четным или нечетным номером). Позже player_char или символ игрока («x» или «o») также устанавливается на основе номера игрока.

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

def play_game
  # display initial output
  
  while (!has_player_won) && (num_available_moves > 0)
    player_char = set_player_char_based_on_move(move_number)
    play_round(player_token, playing_computer)
    has_player_won = ScoreTracker.has_player_won?(player_char)
    move_number += 1
    num_available_moves = board.get_available_moves.length
  end
  # display message when the game is over
end

Обратите внимание, что хотя код внутри цикла while короче всего на 1 строку, его немного легче читать, так как переменной player_num больше нет. Этот цикл еще не идеален, но это улучшение, и пример того, как я начал чистить свой код.

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

Размышления

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

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

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

  • Код должен быть понятным с первого взгляда (и визуально приятным).
  • Функции должны быть короткими (не более 4–5 строк).
  • Каждый класс и каждый метод в этом классе должны выполнять только одно действие (принцип единой ответственности).

Это то, что я искал, когда изучал свой класс Controller. В частности, меня очень беспокоил (и до сих пор) метод play_game в Controller. Вот как на самом деле выглядит play_game на момент написания этой статьи:

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

Итак, проблема, с которой я столкнулся, заключалась в том, как мне очистить этот метод? Как я могу создать абстракции, чтобы сделать его более выразительным? Как я могу упростить это?

И именно здесь мне помогло требование отображать Player x и Player o. Это дало мне небольшой, конкретный способ изменить код, и я смог удалить 1 строку и упростить логику. Это изменение также настроило меня на более значительные изменения в будущем, которые зависят от упрощения, которое было сделано ранее.

Итак, резюмируя:

  • Используйте принципы дизайна в качестве руководства при просмотре кода
  • Когда вы сталкиваетесь с кодом, который нужно изменить, используйте конкретную информацию, указывающую на конкретное изменение, которое вы можете внести.
  • Сделайте это изменение

Со временем повторение этого цикла оставит у вас чистый код, который должен выглядеть примерно так: