Перейти к основному содержимому

Введение в рефакторинг с IntelliJ IDEA

· 15 мин. чтения

1. Обзор

Поддерживать порядок в коде не всегда легко. К счастью для нас, наши IDE в настоящее время довольно умны и могут помочь нам в этом. В этом руководстве мы сосредоточимся на IntelliJ IDEA, редакторе Java-кода JetBrains.

Мы увидим несколько функций, предлагаемых редактором для рефакторинга кода , от переименования переменных до изменения сигнатуры метода.

2. Переименование

2.1. Базовое переименование

Во-первых, давайте начнем с основ: переименование . IntelliJ предлагает нам возможность переименовывать различные элементы нашего кода: типы, переменные, методы и даже пакеты.

Чтобы переименовать элемент, нам нужно выполнить следующие шаги:

  • Щелкните элемент правой кнопкой мыши
  • Активируйте параметр « Рефакторинг » > «Переименовать ».
  • Введите имя нового элемента
  • Нажмите Enter

Кстати, мы можем заменить первые два шага, выделив элемент и нажав Shift+F6 .

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

Давайте представим класс SimpleClass с плохо названным методом добавления, someAdditionMethod , вызываемым в основном методе:

public class SimpleClass {
public static void main(String[] args) {
new SimpleClass().someAdditionMethod(1, 2);
}

public int someAdditionMethod(int a, int b) {
return a + b;
}
}

Таким образом, если мы решим переименовать этот метод в add , IntelliJ создаст следующий код:

public class SimpleClass() {
public static void main(String[] args) {
new SimpleClass().add(1, 2);
}

public int add(int a, int b) {
return a + b;
}
}

2.2. Расширенное переименование

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

Эти параметры доступны, нажав Shift + F6 еще раз перед переименованием нашего элемента, и появится всплывающее окно:

./85b697c62d45ff168977c2f912ccfe68.png

Опция Поиск в комментариях и строках доступна для любого переименования. Что касается параметра Поиск вхождений текста , он недоступен для параметров метода и локальных переменных. Наконец, параметр « Переименовать параметры в иерархии » доступен только для параметров метода.

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

Давайте добавим в наш метод немного Javadoc, а затем переименуем его первый параметр a :

/**
* Adds a and b
* @param a the first number
* @param b the second number
*/
public int add(int a, int b) {...}

Проверяя первый вариант во всплывающем окне подтверждения, IntelliJ сопоставляет любое упоминание параметров в комментарии Javadoc к методу и также предлагает переименовать их:

/**
* Adds firstNumber and b
* @param firstNumber the first number
* @param b the second number
*/
public int add(int firstNumber, int b) {...}

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

3. Извлечение

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

Итак, в этом разделе мы узнаем, как использовать функцию извлечения, предлагаемую IntelliJ.

3.1. Переменные

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

  • Выберите выражение, которое соответствует переменной
  • Щелкните правой кнопкой мыши выбранную область
  • Запустите параметр Refactor > Extract > Variable/Parameter/Field/Constant .
  • Выберите между вариантами « Заменить только это вхождение » или « Заменить все x вхождений », если они предложены .
  • Введите имя для извлеченного выражения (если выбранное нам не подходит)
  • Нажмите Enter

Что касается переименования, то вместо меню можно использовать сочетания клавиш. Комбинации клавиш по умолчанию — это, соответственно, Ctrl + Alt + V, Ctrl + Alt + P, Ctrl + Alt + F и Ctrl + Alt + C.

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

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

public static boolean isNowBetween(LocalDate startingDate, LocalDate endingDate) {
return LocalDate.now().isAfter(startingDate) && LocalDate.now().isBefore(endingDate);
}

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

./9cf1dc7eef7cd1a94bff61df39a770ca.png

Затем наш вызов LocalDate.now() фиксируется в локальной переменной:

public static boolean isNowBetween(LocalDate startingDate, LocalDate endingDate) {
LocalDate now = LocalDate.now();
return now.isAfter(startingDate) && now.isBefore(endingDate);
}

Установив флажок « Заменить все », мы убедились, что оба выражения были заменены одновременно.

3.2. Методы

Давайте теперь проверим, как извлекать методы с помощью IntelliJ:

  • Выберите выражение или строки кода, соответствующие методу, который мы хотим создать.
  • Щелкните правой кнопкой мыши выбранную область
  • Запустите параметр Refactor > Extract > Method
  • Введите информацию о методе: его имя, видимость и параметры.
  • Нажмите Enter

Нажатие Ctrl + Alt + M после выбора тела метода также работает.

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

В открывшемся диалоговом окне мы видим, что IntelliJ уже определил три необходимых параметра: startDate , endingDate и now . Поскольку мы хотим, чтобы этот метод был как можно более универсальным, мы переименовываем параметр now в date . И в целях сплоченности мы помещаем его в качестве первого параметра.

Наконец, мы даем нашему методу имя isDateBetween и завершаем процесс извлечения:

./2038bda4aa439c0eeefd3b076f4025f5.png

Тогда мы получим следующий код:

public static boolean isNowBetween(LocalDate startingDate, LocalDate endingDate) {
LocalDate now = LocalDate.now();
return isDateBetween(now, startingDate, endingDate);
}

private static boolean isDateBetween(LocalDate date, LocalDate startingDate, LocalDate endingDate) {
return date.isBefore(endingDate) && date.isAfter(startingDate);
}

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

3.3. Классы

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

  • Щелкните правой кнопкой мыши класс, в котором есть наши элементы, которые мы хотим переместить.
  • Активируйте параметр Refactor > Extract > Delegate .
  • Введите информацию о классе: его имя, его пакет, элементы для делегирования, видимость этих элементов.
  • Нажмите Enter

По умолчанию для этой функции недоступно сочетание клавиш.

Скажем, перед запуском функции мы вызываем наши методы, связанные с датой, в основном методе:

isNowBetween(LocalDate.MIN, LocalDate.MAX);
isDateBetween(LocalDate.of(2019, 1, 1), LocalDate.MIN, LocalDate.MAX);

Затем мы делегируем эти два метода классу DateUtils , используя опцию делегата:

./d3c29c09f5d98ec59eaf8944d0d388a9.png

Активация функции приведет к следующему коду:

public class DateUtils {
public static boolean isNowBetween(LocalDate startingDate, LocalDate endingDate) {
LocalDate now = LocalDate.now();
return isDateBetween(now, startingDate, endingDate);
}

public static boolean isDateBetween(LocalDate date, LocalDate startingDate, LocalDate endingDate) {
return date.isBefore(endingDate) && date.isAfter(startingDate);
}
}

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

В нашем случае isDateBetween используется в основном методе SimpleClass:

DateUtils.isNowBetween(LocalDate.MIN, LocalDate.MAX);
DateUtils.isDateBetween(LocalDate.of(2019, 1, 1), LocalDate.MIN, LocalDate.MAX);

Таким образом, при перемещении метода необходимо сделать его не приватным.

Тем не менее, можно придать нашим элементам особую видимость, выбрав другие параметры.

4. Встраивание

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

Чтобы встроить элемент, мы должны щелкнуть правой кнопкой мыши этот элемент — либо его определение, либо ссылку на него — и вызвать параметр Refactor > Inline . Мы также можем добиться этого, выбрав элемент и нажав клавиши Ctrl + Alt + N.

На этом этапе IntelliJ предложит нам несколько вариантов, хотим ли мы встроить переменную или метод, независимо от того, выбрали ли мы определение или ссылку. Вот эти варианты:

  • Вставьте все ссылки и удалите элемент
  • Вставьте все ссылки, но сохраните элемент
  • Встраивайте только выбранную ссылку и сохраняйте элемент

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

./c2797a5d645eb3e9af5681703dcbb138.png

Вставив эту переменную, мы получим следующий результат:

public static boolean isNowBetween(LocalDate startingDate, LocalDate endingDate) {
return isDateBetween(LocalDate.now(), startingDate, endingDate);
}

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

./644d3a3f189625b5bae8838397da990f.png

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

public class DateUtils {
public static boolean isNowBetween(LocalDate startingDate, LocalDate endingDate) {
LocalDate date = LocalDate.now();
return date.isBefore(endingDate) && date.isAfter(startingDate);
}

public static boolean isDateBetween(LocalDate date, LocalDate startingDate, LocalDate endingDate) {
return date.isBefore(endingDate) && date.isAfter(startingDate);
}
}

Наш основной метод в SimpleClass также остается нетронутым.

5. Переезд

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

Чтобы переместить элемент, мы должны выполнить следующие шаги:

  • Выберите элемент для перемещения
  • Щелкните элемент правой кнопкой мыши
  • Активируйте параметр « Рефакторинг » > «Переместить ».
  • Выберите класс получателя и видимость метода
  • Нажмите Enter

Мы также можем добиться этого, нажав F6 после выбора элемента.

Допустим, мы добавляем в наш SimpleClass новый метод isDateOutstide() , который сообщит нам, находится ли дата за пределами интервала дат:

public static boolean isDateOutside(LocalDate date, LocalDate startingDate, LocalDate endingDate) {
return !DateUtils.isDateBetween(date, startingDate, endingDate);
}

Затем мы понимаем, что его место должно быть в нашем классе DateUtils . Итак, мы решили перенести его:

./5414786ea969f503ce6e2e77ed565cfc.png

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

public static boolean isDateOutside(LocalDate date, LocalDate startingDate, LocalDate endingDate) {
return !isDateBetween(date, startingDate, endingDate);
}

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

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

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

6. Изменение подписи метода

Наконец, мы поговорим о возможности, позволяющей изменить сигнатуру метода . Цель этой функции — манипулировать каждым аспектом сигнатуры метода.

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

  • Выберите метод для изменения
  • Щелкните правой кнопкой мыши метод
  • Запустите параметр Рефакторинг > Изменить подпись
  • Внесите изменения в сигнатуру метода
  • Нажмите Enter

Если мы предпочитаем использовать сочетание клавиш, можно также использовать Ctrl + F6 .

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

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

./7863983381cebbe58cf17702bc57f00b.png

Изменив сигнатуру метода, мы можем добавить этот параметр, назвать его и присвоить ему значение по умолчанию:

public static boolean isDateBetween(LocalDate date, LocalDate startingDate,
LocalDate endingDate, boolean inclusive) {
return date.isBefore(endingDate) && date.isAfter(startingDate);
}

После этого нам просто нужно адаптировать тело метода в соответствии с нашими потребностями.

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

7. Потяните вверх и нажмите вниз

Наш Java-код обычно имеет иерархию классов — производные классы расширяют базовый класс .

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

7.1. Остановить

Во-первых, давайте подтянем метод к базовому классу:

  • Выберите член производного класса для извлечения
  • Щелкните член правой кнопкой мыши
  • Активируйте параметр Refactor > Pull Members Up…
  • Нажмите кнопку рефакторинга

По умолчанию для этой функции недоступно сочетание клавиш.

Допустим, у нас есть производный класс под названием Derived. Он использует закрытый метод doubleValue() :

public class Derived extends Base {

public static void main(String[] args) {
Derived subject = new Derived();
System.out.println("Doubling 21. Result: " + subject.doubleValue(21));
}

private int doubleValue(int number) {
return number + number;
}
}

Базовый класс Base пуст.

Итак, что происходит, когда мы подтягиваем doubleValue() в Base ?

./9614e54bba1cd6b39457898ad0d17c85.png

Две вещи происходят с doubleValue() , когда мы нажимаем «Refactor» в диалоговом окне выше:

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

После этого базовый класс имеет метод:

public class Base {
protected int doubleValue(int number) {
return number + number;
}
}

Диалоговое окно для подтягивания элементов (на фото выше) дает нам еще несколько вариантов:

  • мы можем выбрать других членов и подтянуть их всех сразу
  • мы можем просмотреть наши изменения с помощью кнопки «Предварительный просмотр»
  • только методы имеют флажок в столбце «Сделать абстрактным». Если флажок установлен, эта опция даст базовому классу определение абстрактного метода во время подтягивания. Фактический метод останется в производном классе, но получит аннотацию @Override . Следовательно, другие производные классы больше не будут компилироваться, потому что им не хватает реализации этого нового абстрактного базового метода.

7.2. Нажмите вниз

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

  • Выберите член базового класса для нажатия
  • Щелкните член правой кнопкой мыши
  • Активируйте параметр Refactor > Push Members Down…
  • Нажмите кнопку рефакторинга

Как и в случае с подтягиванием участников, по умолчанию для этой функции недоступно сочетание клавиш.

Давайте снова нажмем метод, который мы только что подняли. Базовый класс выглядел так в конце предыдущего раздела :

public class Base {
protected int doubleValue(int number) {
return number + number;
}
}

Теперь давайте добавим doubleValue() в класс Derived :

./6640f0590a8d88a7bfbf1c0369fe0bd0.png

Это производный класс после нажатия кнопки «Рефакторинг» в приведенном выше диалоговом окне. Метод doubleValue() вернулся:

public class Derived extends Base {
private int theField = 5;

public static void main(String[] args) {
Derived subject = new Derived();
System.out.println( "Doubling 21. Result: " + subject.doubleValue(21));
}

protected int doubleValue(int number) {
return number + number;
}
}

Теперь и базовый класс, и производный класс вернулись к тому, с чего они начали в предыдущем разделе «Подтягивание». Почти то есть — doubleValue() сохранил защищенную видимость, которую он имел в Base ( изначально он был закрытым ).

IntelliJ 2019.3.4 фактически выводит предупреждение при нажатии doubleValue() : «Отправленные элементы не будут видны с определенных сайтов вызовов». Но, как мы можем видеть в классе Derived выше, метод doubleValue() действительно виден методу main() .

Диалоговое окно для выталкивания элементов (на фото выше) также дает нам несколько дополнительных опций:

  • если у нас есть несколько производных классов, то IntelliJ будет помещать членов в каждый производный класс
  • мы можем нажать несколько членов вниз
  • мы можем просмотреть наши изменения с помощью кнопки «Предварительный просмотр»
  • только у методов есть флажок в столбце «Сохранить абстрактный» — это похоже на подтягивание членов: если этот флажок установлен, этот параметр оставит абстрактный метод в базовом классе. В отличие от подтягивания элементов, этот параметр помещает реализации методов во все производные классы. Эти методы также получат аннотацию @Override.

8. Заключение

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

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