Подобные изменения возникают на разных уровнях:
• Два цикла выглядят похоже. Если вы сделаете их идентичными, вы сможете объединить их в единый цикл.
• Две ветви условного оператора выглядят похоже. Сделав их идентичными, вы сможете избавиться от условного оператора.
• Два метода выглядят похоже. Сделав их идентичными, вы сможете избавиться от одного из них.
• Два класса выглядят похоже. Сделав их идентичными, вы сможете избавиться от одного из них.
Иногда задачу согласования различий удобнее решать в обратном порядке. Иными словами, вы представляете себе самый тривиальный последний этап этой процедуры, а затем двигаетесь в обратном направлении. Например, если вы хотите избавиться от нескольких подклассов, наиболее тривиальный последний шаг можно будет выполнить в случае, если подкласс ничего не содержит. Тогда везде, где используется подкласс, можно будет использовать суперкласс, при этом поведение системы не изменится. Что надо сделать, чтобы очистить подкласс от методов и данных? Для начала метод можно сделать полностью идентичным одному из методов суперкласса. Постепенно переместив все методы и все данные в суперкласс, вы сможете заменить ссылки на подкласс ссылками на суперкласс. После этого подкласс можно уничтожить.
Как можно модифицировать одну часть метода или объекта, состоящего из нескольких частей? Сначала изолируйте изменяемую часть.
Мне приходит в голову аналогия с хирургической операцией: фактически все тело оперируемого пациента покрыто специальной простыней за исключением места, на котором, собственно, осуществляется операция. Благодаря такому покрытию хирург имеет дело с фиксированным набором переменных. Перед выполнением операции врачи сколь угодно долго могут обсуждать, какое влияние на здоровье пациента оказывает тот или иной орган, однако во время операции внимание хирурга должно быть сфокусировано.
Вы можете обнаружить, что после того, как вы изолировали изменение, а затем внесли это изменение в код, результат получился настолько тривиальным, что вы можете отменить изоляцию. Например, если мы обнаружили, что внутри метода findRate() должно присутствовать всего одно действие – возврат значения поля, мы можем вместо обращений к методу findRate() напрямую обратиться к полю. В результате метод findRate() можно будет удалить. Однако подобные изменения не следует выполнять автоматически. Постарайтесь найти баланс между затратами, связанными с использованием дополнительного метода, и пользой, которую приносит дополнительная концепция, добавленная в код.
Для изоляции изменений можно использовать несколько разных способов. Наиболее часто используется шаблон «Выделение метода» (Extract Method), помимо него также используются «Выделение объекта» (Extract Object) и «Метод в объект» (Method Object).
Как можно перейти от одного представления к другому? Временно дублируйте данные.
Как
Вначале рассмотрим версию «от внутреннего к внешнему». В рамках этого подхода вы изменяете вначале внутреннее представление, а затем внешний интерфейс.
1. Создайте переменную экземпляра в новом формате.
2. Инициализируйте переменную нового формата везде, где инициализируется переменная старого формата.
3. Используйте переменную нового формата везде, где используется переменная старого формата.
4. Удалите старый формат.
5. Измените внешний интерфейс так, чтобы использовать новый формат.
Однако в некоторых ситуациях удобнее сначала изменить API. В этом случае рефакторинг выполняется следующим образом.
1. Добавьте параметр в новом формате.
2. Обеспечьте преобразование параметра в новом формате во внутреннее представление, обладающее старым форматом.
3. Удалите параметр в старом формате.
4. Замените использование старого формата на использование нового формата.
5. Удалите старый формат.
Зачем
Проблема миграции данных возникает каждый раз, когда используется шаблон «От одного ко многим» (One to Many). Предположим, что мы хотим реализовать объект TestSuite, используя шаблон «От одного ко многим» (One to Many). Мы можем начать так:
def testSuite(self):
suite = TestSuite()
suite.add(WasRun("testMethod"))
suite.run(self.result)
assert("1 run, 0 failed" == self.result.summary())
Чтобы реализовать этот тест, начнем с одного элемента test:
class TestSuite:
def add(self, test):
self.test = test
def run(self, result):
self.test.run(result)
Теперь мы приступаем к дублированию данных. Вначале инициализируем коллекцию тестов:
TestSuite
def __init__(self):
self.tests = []
В каждом месте, где инициализируется поле test, добавляем новый тест в коллекцию:
TestSuite
def add(self, test):
self.test = test
self.tests.append(test)
Теперь мы используем коллекцию тестов вместо единичного теста. Исходя из существующего набора тестов данное преобразование можно считать рефакторингом (оно не нарушает семантику), так как в составе коллекции присутствует только один элемент.
TestSuite
def run(self, result):
for test in self.tests:
test.run(result)
Теперь можно удалить не используемую переменную экземпляра test:
TestSuite
def add(self, test):
self.tests.append(test)
Поэтапную миграцию данных можно использовать также при переходе между эквивалентными форматами, использующими различные протоколы, например, если речь идет о Java, при переходе от Vector/Enumerator к Collection/Iterator.
Как длинный сложный метод можно сделать простым для чтения? Выделите небольшую часть длинного метода в отдельный метод и обратитесь к этому методу из длинного метода.
Как
Выделение метода на самом деле является несколько более сложным атомарным рефакторингом. Здесь я опишу самый типичный случай. К счастью, многие среды разработки поддерживают автоматическое выполнение этого рефакторинга. Итак, чтобы выделить метод:
1. Определите фрагмент кода, который можно выделить в отдельный метод. Хорошими кандидатами являются тела циклов, сами циклы, а также ветви условных операторов.
2. Убедитесь, что внутри этого фрагмента не происходит присваивания значений временным переменным, объявленным вне области видимости, соответствующей этому фрагменту.
3. Скопируйте код из старого метода в новый. Скомпилируйте его.
4. Для каждой временной переменной или параметра первоначального метода, используемого в новом методе, добавьте параметр в новый метод.
5. Сделайте так, чтобы в нужном месте старый метод обращался к новому методу.
Зачем
Я использую «Выделение метода» (Extract Method), когда пытаюсь понять сложный код. «Значит так, этот кусок кода делает вот это. А этот кусок делает это. К чему мы там дальше обращаемся?» Через полчаса код будет выглядеть гораздо лучше, ваш партнер начнет понимать, что вы действительно оказываете ему помощь, а вы – существенно лучше понимать, что же все-таки происходит внутри кода.
Я использую выделение метода, чтобы избавиться от дублирования, когда вижу, что два метода обладают сходными участками кода. В этом случае я выделяю схожие участки в отдельный метод. (Браузер рефакторинга для Smalltalk – Refactoring Browser – выполняет еще более полезную задачу: он просматривает код в поисках метода, аналогичного коду, который вы намерены выделить, и в случае, если такой метод уже есть, предлагает использовать уже существующий метод вместо того, чтобы создавать новый.)
Разделение методов на множество мелких кусочков может зайти слишком далеко. Если я не вижу, куда идти дальше, я часто использую шаблон «Встраивание метода» (Inline Method), чтобы собрать код в одном месте и увидеть новый, более удобный способ разделения.
Как можно упростить код, если становится сложно уследить за последовательностью передачи управления от метода к методу? Замените обращение к методу кодом этого метода.
Как
1. Скопируйте код метода в буфер обмена.
2. Вставьте код метода вместо обращения к методу.
3. Замените все формальные параметры фактическими. Если, например, вы передаете reader.getNext(), то есть выражение, обладающее побочным эффектом, будьте осторожны и присвойте полученное значение временной переменной.
Зачем
Один из моих рецензентов пожаловался на сложность кода в первой части книги, который требует от объекта Bank преобразовать объект Expression в объект Money.
public void testSimpleAddition() {
Money five = Money.dollar(5);
Expression sum = five.plus(five);
Bank bank = new Bank();
Money reduced = bank.reduce(sum, "USD");
assertEquals(Money.dollar(10), reduced);
}
«Это слишком сложно. Почему бы не реализовать преобразование в самом объекте Money?» Ну что же, поставим эксперимент. Как это сделать? Давайте встроим метод Bank.reduce() и посмотрим, как это будет выглядеть:
public void testSimpleAddition() {
Money five = Money.dollar(5);
Expression sum = five.plus(five);
Bank bank = new Bank();
Money reduced = sum.reduce(bank, «USD»);
assertEquals(Money.dollar(10), reduced);
}
Возможно, вторая версия понравится вам больше, возможно, нет. Важно понимать, что при помощи шаблона «Встраивание метода» (Inline Method) вы можете экспериментировать с последовательностью выполнения действий. Когда я выполняю рефакторинг, я формирую у себя в голове мысленную картину системы с кусками логики и потоком выполнения программы, перетекающим от одного объекта к другому объекту. Когда мне кажется, что я вижу нечто многообещающее, я использую рефакторинг, чтобы попробовать это и увидеть результат.