Мы не практикуем много — не так, как это делают спортсмены и музыканты.
Конечно, они имеют преимущество перед нами, поскольку у них есть тысячи упражнений и прогрессий, которые они могут использовать для улучшения любого аспекта своего ремесла.
Мы, с другой стороны, делаем это по ходу дела.
Рефакторинг — это процесс изменения программной системы таким образом, что он не изменяет внешнее поведение кода, но улучшает его внутреннюю структуру. Мартин Фаулер
Как вы поправляетесь в рефакторинге?
Ну, вы читаете книги по рефакторингу и делаете больше рефакторинга. В принципе.
Это не так полезно, как, скажем, «практиковать это упражнение на независимость, используя метроном, пока вы не сможете играть без сбоев в течение 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
только для того, чтобы вы могли внести изменения за один шаг, а не за два?
Нет На самом деле, нет. Это было бы смешно.
Эти уловки являются гиперфокусными преувеличениями. Вариации на них могут быть полезны в реальном мире, но самым большим результатом выполнения этого упражнения несколько раз является сдвиг в перспективе.
Внезапно вы обнаружите, что находите хитрые, творческие способы внесения изменений без необходимости ломать сайт или иметь дело с долгоживущими ветвями.
Вы также можете обнаружить, что ваше чувство масштаба изменяется. Маленькие шаги, как правило, меньше, чем мы думаем.
И, наконец, вы можете обнаружить, что ваша терпимость к гротескному коду возрастает.
В конце концов, вы всегда можете рефакторинг.