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

1. Общ преглед

Поддържането на кода подреден не винаги е лесно. За наше щастие, нашите IDE днес са доста умни и могат да ни помогнат за постигането на това. В този урок ще се съсредоточим върху IntelliJ IDEA, редактора на JetBrains Java код.

Ще видим няколко функции, предлагани от редактора за рефакториране на код, от преименуване на променливи до промяна на подпис на метод.

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; } }

По този начин, ако решим да преименуваме този метод в добавяне , 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 още веднъж, преди да преименувате нашия елемент и ще се появи изскачащ прозорец:

Опцията Търсене в коментари и низове е достъпна за всяко преименуване. Що се отнася до опцията Търсене на текстови появи , тя не е налична за параметри на метода и локални променливи. И накрая, опцията Преименуване на параметри в йерархия е достъпна само за параметри на метода.

Така че, ако се намери някакво съвпадение с една от тези две опции, 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. Променливи

Първо, нека започнем с извличането на променливи. Това означава локални променливи, параметри, полета и константи. За да извлечем променлива, трябва да изпълним следните стъпки:

  • Изберете израз, който се побира в променлива
  • Щракнете с десния бутон върху избраната област
  • Активирайте Преструктуриране на> Извличане> Променлива / параметър / поле / Постоянна вариант
  • Изберете между опциите Замяна само на това или Замяна на всички 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 () два пъти и бихме искали да се уверим, че има абсолютно еднаква стойност и в двете оценки. Нека просто изберем израза и го извлечем в локална променлива, сега:

След това нашето обаждане LocalDate.now () се улавя в локална променлива:

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

Проверявайки опцията Замяна на всички , се уверихме, че двата израза са заменени наведнъж.

3.2. Методи

Нека сега проверим как да извличаме методи с помощта на IntelliJ:

  • Изберете израза или редовете на кода, съответстващи на метода, който искаме да създадем
  • Щракнете с десния бутон върху избраната област
  • Активирайте Преструктуриране на> Извличане> Метод вариант
  • Въведете информацията за метода: името му, видимостта и параметрите му
  • Press Enter

Hitting Ctrl + Alt + M after selecting the method body works as well.

Let's reuse our previous example and say we want to have a method checking if any date is between to other dates. Then, we would just have to select our last line in the isNowBetween method and trigger the method extraction feature.

In the opened dialog, we can see that IntelliJ has already spotted the three needed parameters: startingDate, endingDate and now. As we want this method to be as generic as possible, we rename the now parameter into date. And for cohesion purposes, we place it as the first parameter.

Finally, we give our method a name, isDateBetween, and finalize the extraction process:

We would then obtain the following code:

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); }

As we can see, the action triggered the creation of the new isDateBetween method, which is also called in the isNowBetween method. The method is private, by default. Of course, this could have been changed using the visibility option.

3.3. Classes

After all that, we might want to get our date-related methods in a specific class, focused on dates management, let's say: DateUtils. Again, this is pretty easy:

  • Right-click in the class that has our elements we want to move
  • Trigger the Refactor > Extract > Delegate option
  • Type in the class information: its name, its package, the elements to delegate, the visibility of those elements
  • Press Enter

By default, no keyboard shortcut is available for this feature.

Let's say, before triggering the feature, that we call our date-related methods in the main method:

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

Then we delegate those two methods to a DateUtils class using the delegate option:

Trigger the feature would produce the following code:

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); } }

We can see that the isDateBetween method has been made public. That is the result of the visibility option, that is set to escalate by default. Escalate means that visibility will be changed in order toensure current calls to the delegated elements are still compiling.

In our case, isDateBetween is used in the main method of SimpleClass:

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

Thus, when moving the method it's necessary to make it not private.

However, it's possible to give specific visibility to our elements by selecting the other options.

4. Inlining

Now that we covered extraction, let's talk about its counterpart: inlining. Inlining is all about taking a code element and replace it with what it's made of. For a variable, this would be the expression it has been assigned. For a method, it would be its body.Earlier, we saw how to create a new class and delegate some of our code elements to it. But there are times we might want to delegate a method to an existing class. That's what this section is about.

In order to inline an element, we must right-click this element – either its definition or a reference to it – and trigger the Refactor >Inline option. We can also achieve this by selecting the element and hitting the Ctrl + Alt + N keys.

At this point, IntelliJ will offer us multiple options, whether we wish to inline a variable or a method, whether we selected a definition or a reference. These options are:

  • Inline all the references and remove the element
  • Inline all the references, but keep the element
  • Only inline the selected reference and keep the element

Let's take our isNowBetween method and get rid of the now variable, which now seems a bit overkill:

By inlining this variable, we would obtain the following result:

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

In our case, the only option was to remove all the references and remove the element. But let's imagine we also want to get rid of the isDateBetween call and choose to inline it. Then, IntelliJ would offer us the three possibilities we spoke about before:

Choosing the first one would replace all calls with the method body and delete the method. As for the second one, it would replace all calls with the method body but keep the method. And finally, the last one would only replace the current call with the method body:

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); } }

Our main method in the SimpleClass remains untouched as well.

5. Moving

Earlier, we saw how to create a new class and delegate some of our code elements to it. But there are times we might want to delegate a method to an existing class. That's what this section is about.

In order to move an element, we must follow these steps:

  • Select the element to move
  • Right-click the element
  • Trigger the Refactor > Move option
  • Choose the recipient class and the method visibility
  • Press Enter

We can also achieve this by pressing F6 after selecting the element.

Let's say we add a new method to our SimpleClass, isDateOutstide(), that will tell us if a date is situated outside an interval of dates:

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

We then realize that its place should be in our DateUtils class. So we decide to move it:

Our method is now in the DateUtils class. We can see that the reference to DateUtils inside the method has disappeared as it's no longer needed:

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

The example we just did works well as it concerns a static method. However, in the case of an instance method, things are not so straight-forward.

If we want to move an instance method, IntelliJ will search for classes referenced in the fields of the current class and offer to move the method to one of those classes (provided they are ours to modify).

If no modifiable class is referenced in the fields, then IntelliJ proposes making the method static before moving it.

6. Changing a Method Signature

Finally, we'll talk about a feature allowing us to change a method signature. The purpose of this feature is to manipulate every aspect of a method signature.

As usual, we must go through a few steps to trigger the feature:

  • Select the method to change
  • Right-click the method
  • Trigger the Refactor > Change signature option
  • Bring changes to the method signature
  • Press Enter

If we prefer to use keyboards shortcut, it's possible to use Ctrl + F6 as well.

This feature will open a dialog very similar to the method extraction feature. And so we have the same possibilities that when we extract a method: changing its name, its visibility and also adding/removing parameters and fine-tuning them.

Let's imagine we want to change our implementation of isDateBetween to either consider the date bounds as inclusive or exclusive. In order to do that we want to add a boolean parameter to our method:

By changing the method signature we can add this parameter, name it and give it a default value:

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

After that, we just have to adapt the method body according to our needs.

If we wanted, we could have checked the Delegate via overloading method option in order to create another method with the parameter instead of modifying the current one.

7. Pull Up & Push Down

Our Java code typically has class hierarchies – derived classes extend a base class.

Sometimes we want to move members (methods, fields, and constants) between these classes. That is where the last refactoring comes in handy: it allows us to pull up members from a derived class into the base class or push them down from a base class into each derived class.

7.1. Pull Up

First, let's pull up a method to the base class:

  • Select a member of a derived class to pull up
  • Right-click the member
  • Trigger the Refactor > Pull Members Up… option
  • Push the Refactor button

By default, no keyboard shortcut is available for this feature.

Let's say we have a derived class called Derived. It uses a private doubleValue() method:

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; } }

The base class Base is empty.

So, what happens when we pull up doubleValue() into Base?

Two things happen to doubleValue() when we push “Refactor” in the above dialog:

  • it moves to the Base class
  • its visibility changes from private to protected so that the Derived class can still use it

The Base class afterward now has the method:

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

The dialog for pulling up members (pictured above) gives us some more options:

  • we can select other members and pull them up all at once
  • we can preview our changes with the “Preview” button
  • only methods have a checkbox in the “Make abstract” column. If checked, this option will give the base class an abstract method definition during the pull-up. The actual method will remain in the derived class but gain an @Override annotation. Consequently, other derived classes won't compile anymore then because they're missing implementation of that new, abstract base method

7.2. Push Down

Lastly, let's push down a member to the derived class. This is the opposite of the pulling up we just performed:

  • Select a member of a base class to push down
  • Right-click the member
  • Trigger the Refactor > Push Members Down… option
  • Push the Refactor button

As with pulling members up, no keyboard shortcut is available for this feature by default.

Let's push down the method again that we just pulled up. The Base class looked like this at the end of the previous section:

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

Now, let's push doubleValue() down to the Derived class:

This is the Derived class after pushing “Refactor” in the above dialog. The doubleValue() method is back:

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; } }

Now both the Base class and the Derived class are back to where they started in the previous “Pull Up” section. Nearly, that is – doubleValue() kept the protected visibility it had in Base (it was private originally).

IntelliJ 2019.3.4 actually brings up a warning when pushing down doubleValue(): “Pushed members will not be visible from certain call sites”. But as we can see in the Derived class above, doubleValue() is visible indeed to the main() method.

The dialog for pushing down members (pictured above) also gives us some more options:

  • if we have multiple derived classes, then IntelliJ will push members into each derived class
  • we can push multiple members down
  • we can preview our changes with the “Preview” button
  • only methods have a checkbox in the “Keep abstract” column – That is similar to pulling up members: If checked, this option will leave an abstract method in the base class. Unlike pulling up members, this option will put method implementations into all derived classes. These methods will also gain an @Override annotation

8. Conclusion

In this article, we had the chance to deep dive into some of the refactoring features offered by IntelliJ. Of course, we didn't cover all the possibilities as IntelliJ is a very powerful tool. To learn more about this editor, we can always refer to its documentation.

Видяхме няколко неща като как да преименуваме нашите кодови елементи и как да извлечем някои поведения в променливи, методи или класове. Също така научихме как да вградим някои елементи, ако не ни трябваше да стоят сами, да преместим някакъв код на друго място или дори напълно да променим съществуващ подпис на метода.