Статьи

Рефакторинг тренировки: неустанно зеленый

Изображение предоставлено Стивеном Деполо (Flickr)

Изображение предоставлено Стивеном Деполо (Flickr)

Мы не практикуем много — не так, как это делают спортсмены и музыканты.

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

Мы, с другой стороны, делаем это по ходу дела.

Рефакторинг — это процесс изменения программной системы таким образом, что он не изменяет внешнее поведение кода, но улучшает его внутреннюю структуру. Мартин Фаулер

Как вы поправляетесь в рефакторинге?

Ну, вы читаете книги по рефакторингу и делаете больше рефакторинга. В принципе.

Это не так полезно, как, скажем, «практиковать это упражнение на независимость, используя метроном, пока вы не сможете играть без сбоев в течение 120 секунд. Начните с 30 ударов в минуту (ударов в минуту) и выполняйте упражнение с шагом 5 ударов в минуту, пока вы не сможете последовательно играть его со скоростью 180 ударов в минуту » .

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

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

Практика не делает совершенным, практика делает постоянным. Ларри Гелвикс

Рефакторинг не повторяет

Слишком часто мы делаем скачки, а не шаги, при рефакторинге. Тем не менее, в то время как мы взломали что-то, что, как мы знаем, у нас получится (в конце концов), тесты не пройдут.

То есть, если мы даже запускаем их.

Одной из проблем рефакторинга является преемственность: как разделить работу рефакторинга на безопасные шаги и как упорядочить эти шаги. —Кент Бек

Безопасный шаг — это тот, который не приводит к сбою тестов.

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

Рефакторинг под зеленым

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

Размер шага ограничен тем, что ваш редактор может вернуть с помощью одной отмены .

Есть два правила:

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

Набор тестов минимален, утверждая, что строковое представление по умолчанию экземпляра Checkerboard возвращает сетку с чередующимися черными (B) и белыми (W) квадратами. Рассматриваемая доска настолько мала, насколько это возможно, но при этом определяет клетчатый узор.

 gem 'minitest', '~> 5.3' require 'minitest/autorun' require_relative 'checkerboard' class CheckerboardTest < Minitest::Test def test_small_board expected = <<-BOARD BW WB BOARD assert_equal expected, Checkerboard.new(2).to_s end end 

Реализация жестко кодирует строковое представление сетки 2 × 2.

 class Checkerboard def initialize(_) end def to_s "BW\nW B\n" end end 

Когда рефакторинг завершен, должна быть возможность вызывать Checkerboard.new любого размера и получить правильно отформатированную шахматную доску.

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

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

С чего начать?

У текущего алгоритма есть некоторые проблемы.

 def to_s "BW\nW B\n" end 

С одной стороны, это не масштабируется. Кроме того, это смешивает данные и представление.

В идеале сетка должна создаваться отдельно, а метод to_s будет манипулировать сеткой, чтобы обеспечить строковое представление. Новые строки проясняют, что здесь есть две вещи. Два ряда Это пахнет как массив.

Как вы переходите от строки к массиву, не терпя неудачу где-то по пути?

Как вы отключаете больницу от электросети, не убивая нескольких пациентов?

Избыточность.

Положите что-нибудь на место, что позволит вам безопасно перейти на другой ресурс.

Избыточная уловка

Напишите совершенно новую реализацию и вставьте ее прямо перед старой реализацией.

 def to_s ["BW\n", "WB\n"].join "BW\nW B\n" end 

Запустите тест.

Новая реализация еще не проверена, но выполнена, что обеспечивает проверку синтаксиса.

Удалите исходную реализацию и снова запустите тест.

 def to_s ["BW\n", "WB\n"].join end 

Если бы это не удалось, единственная отмена вернула бы рабочий код обратно.

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

 def to_s ["BW", "WB"].map {|row| row + "\n"}.join end 

Мы так привыкли думать о дублировании как о враге. Однако в рефакторинге дублирование — это краткосрочные, недорогие инвестиции, которые приносят отличную прибыль.

Уловка установки и замены

Трудно добраться до отдельных клеток. Было бы проще, если бы они были в собственном массиве. Уловка setup-and-swap добавляет весь код, необходимый для окончательного изменения за один маленький шаг.

 def to_s rows = [] rows << "BW" rows << "WB" ["BW", "WB"].map {|row| row + "\n"}.join end 

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

 def to_s rows = [] rows << "BW" rows << "WB" rows.map {|row| row + "\n"}.join end 

Каждый ряд по-прежнему жестко запрограммирован, но может быть независимо преобразован с помощью другой установки и замены.

 row = ["B", "W"].join(" ") rows << "BW" row = ["B", "W"].join(" ") rows << row def to_s rows = [] row = ["B", "W"].join(" ") rows << row row = ["W", "B"].join(" ") rows << row rows.map {|row| row + "\n"}.join end 

Это сдвинуло пустое пространство от отдельных клеток, но мы можем добиться большего. Объединение принадлежит внутри цикла.

Это особенно сложно, так как у нас есть два объединения, которые нужно свести в одно место. Если мы переместим один, тест не пройден. Если мы удалим оба, тест не пройден. Если мы сначала join в row внутри цикла, тест не пройден.

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

Уловка нулевого метода

Нет причин, по которым у String не может быть метода join !

 class String def join(_) self end end class Checkerboard # ... end 

Это упрощает внесение изменений, ничего не нарушая.

 def to_s rows = [] row = ["B", "W"].join(" ") rows << row row = ["W", "B"].join(" ") rows << row rows.map {|row| row.join(" ") + "\n"}.join end 

Теперь row в цикле может обрабатывать как массивы, так и строки, и мы можем удалить исходные join вместе с методом null.

 def to_s rows = [] row = ["B", "W"] rows << row row = ["W", "B"] rows << row rows.map {|row| row.join(" ") + "\n"}.join end 

Два назначения row похожи, но не идентичны. Используйте уловку избыточности, чтобы отделить части, которые отличаются от частей, которые остаются теми же.

 def to_s rows = [] row = [] row << "B" row << "W" rows << row row = [] row << "W" row << "B" rows << row rows.map {|row| row.join(" ") + "\n"}.join end 

Это решение будет масштабироваться, только если мы введем цикл. Ну, два на самом деле.

Уловка ветра

Есть два блока, которые имеют одинаковую настройку и одинаковое окончание. Назовите эти Чанк А и Чанк Б.

 # chunk a # chunk b 

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

 2.times do if condition A # chunk a else # chunk b end end 

Тогда общий код можно вывести из условного:

 2.times do # common code if condition A # variation a else # variation b end # common code end 

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

 def to_s rows = [] 2.times {|y| row = [] if y == 0 row << "B" row << "W" else row << "W" row << "B" end rows << row } rows.map {|row| row.join(" ") + "\n"}.join end 

По-прежнему много общего в том, как ящики сгребают в ряд. Единственная разница — это порядок. Снова примените уловку от ветра.

 def to_s rows = [] 2.times {|y| row = [] 2.times {|x| if y == 0 if x == 0 row << "B" else row << "W" end else if x == 0 row << "W" else row << "B" end end } rows << row } rows.map {|row| row.join(" ") + "\n"}.join end 

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

 def to_s rows = [] 2.times {|y| row = [] 2.times {|x| if x == y row << "B" else row << "W" end } rows << row } rows.map {|row| row.join(" ") + "\n"}.join end 

x == y действительно действительно только для шахматной доски 2 × 2. В большем шахматном поле это дает диагональную полосу.

Есть несколько правильных подходов. Первый:

 if (x.even? && y.even?) || (x.odd? && y.odd?) # it's black else # it's white end 

Другой более лаконичен:

 if (x+y).even? # it's black else # it's white end 

Этот алгоритм будет работать для шахматной доски любого размера, при условии, что мы зациклимся достаточно раз. Мы все время передавали аргумент, который нам нужен, новому экземпляру Checkerboard . Используйте уловку setup-and-swap, чтобы сделать эти данные доступными для остальной части экземпляра, а затем замените магические числа вызовами size .

 attr_reader :size def initialize(size) @size = size end def to_s rows = [] size.times {|y| row = [] size.times {|x| if (x+y).even? row << "B" else row << "W" end } rows << row } rows.map {|row| row.join(" ") + "\n"}.join end 

Разумность проверить решение, добавив второй тест, который доказывает, что оно работает.

 def test_chess_board expected = <<-BOARD BWBWBWBW WBWBWBWB BWBWBWBW WBWBWBWB BWBWBWBW WBWBWBWB BWBWBWBW WBWBWBWB BOARD assert_equal expected, Checkerboard.new(8).to_s end 

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

Заключительные мысли

Подождите. В самом деле? Вы бы запатентовали String только для того, чтобы вы могли внести изменения за один шаг, а не за два?

Нет На самом деле, нет. Это было бы смешно.

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

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

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

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

В конце концов, вы всегда можете рефакторинг.