JavaRush /Java Blog /Random EN /How refactoring works in Java

How refactoring works in Java

Published in the Random EN group
When learning to program, a lot of time is spent writing code. Most beginning developers believe that this is their future activity. This is partly true, but a programmer’s tasks also include maintaining and refactoring code. Today we'll talk about refactoring. How refactoring works in Java - 1

Refactoring in the JavaRush course

The JavaRush course covers the topic of refactoring twice: Thanks to the big task, there is an opportunity to get acquainted with real refactoring in practice, and a lecture on refactoring in IDEA will help you understand the automatic tools that make life incredibly easier.

What is refactoring?

This is a change in the structure of the code without changing its functionality. For example, there is a method that compares 2 numbers and returns true if the first one is greater, and false otherwise:
public boolean max(int a, int b) {
    if(a > b) {
        return true;
    } else if(a == b) {
        return false;
    } else {
        return false;
    }
}
The result was very cumbersome code. Even beginners rarely write something like this, but there is such a risk. It would seem, why is there a block here if-elseif you can write a method 6 lines shorter:
public boolean max(int a, int b) {
     return a>b;
}
Now this method looks simple and elegant, although it does the same thing as the example above. This is how refactoring works: it changes the structure of the code without affecting its essence. There are many refactoring methods and techniques, which we will consider in more detail.

Why is refactoring needed?

There are several reasons. For example, the pursuit of simplicity and conciseness of the code. Proponents of this theory believe that the code should be as concise as possible, even if it requires several dozen lines of commentary to understand it. Other developers believe that code should be refactored so that it is understandable with a minimum number of comments. Each team chooses its position, but we must remember that refactoring is not a reduction . Its main goal is to improve the structure of the code. Several objectives can be included in this global goal:
  1. Refactoring improves understanding of code written by another developer;
  2. Helps find and fix errors;
  3. Allows you to increase the speed of software development;
  4. Overall improves software composition.
If refactoring is not carried out for a long time, development difficulties may arise, up to a complete stop of work.

“Code smells”

When code requires refactoring they say it “smells.” Of course, not literally, but such code really does not look very nice. Below we will consider the main refactoring techniques for the initial stage.

Unnecessarily large elements

There are cumbersome classes and methods that are impossible to work with effectively precisely because of their huge size.

Big class

Such a class has a huge number of lines of code and many different methods. It is usually easier for a developer to add a feature to an existing class rather than create a new one, which is why it grows. As a rule, the functionality of this class is overloaded. In this case, separating part of the functionality into a separate class helps. We'll talk about this in more detail in the refactoring techniques section.

Big Method

This “smell” occurs when a developer adds new functionality to a method. “Why should I put parameter checking in a separate method if I can write it here?”, “Why is it necessary to separate the method for finding the maximum element in the array, let’s leave it here. This way the code is clearer,” and other misconceptions. There are two rules for refactoring a large method:
  1. If, when writing a method, you want to add a comment to the code, you need to separate this functionality into a separate method;
  2. If a method takes more than 10-15 lines of code, you should identify the tasks and subtasks that it performs and try to separate the subtasks into a separate method.
Several ways to eliminate a large method:
  • Separate part of the functionality of a method into a separate method;
  • If local variables do not allow you to extract part of the functionality, you can pass the entire object to another method.

Using many primitive data types

Typically, this problem occurs when the number of fields to store data in a class grows over time. For example, if you use primitive types instead of small objects to store data (currency, date, phone numbers, etc.) or constants to encode any information. A good practice in this case would be to logically group the fields and place them in a separate class (selecting a class). You can also include methods for processing this data in the class.

Long list of options

A fairly common mistake, especially in combination with a large method. It usually occurs if the functionality of the method is overloaded, or the method combines several algorithms. Long lists of parameters are very difficult to understand, and such methods are inconvenient to use. Therefore, it is better to transfer the entire object. If the object does not have enough data, it is worth using a more general object or splitting the functionality of the method so that it processes logically related data.

Data groups

Logically related groups of data often appear in code. For example, connection parameters to the database (URL, username, password, schema name, etc.). If not a single field can be removed from the list of elements, then the list is a group of data that must be placed in a separate class (class selection).

Solutions that spoil the concept of OOP

This type of “smell” occurs when the developer violates the OOP design. This happens if he does not fully understand the capabilities of this paradigm, uses them incompletely or incorrectly.

Refusal of inheritance

If a subclass uses a minimal part of the functions of the parent class, it smells like an incorrect hierarchy. Typically, in this case, unnecessary methods are simply not overridden or exceptions are thrown. If a class is inherited from another, this implies almost complete use of its functionality. Example of a correct hierarchy: How refactoring works in Java - 2 Example of an incorrect hierarchy: How refactoring works in Java - 3

switch statement

What could be wrong with an operator switch? It is bad when its design is very complex. This also includes many nested blocks if.

Alternative classes with different interfaces

Several classes actually do the same thing, but their methods are named differently.

Temporary field

If the class contains a temporary field that the object needs only occasionally, when it is filled with values, and the rest of the time it is empty or, God forbid, null, then the code “smells”, and such a design is a dubious decision.

Odors that make modification difficult

These “smells” are more serious. The rest mainly impair the understanding of the code, while these do not make it possible to modify it. When introducing any features, half of the developers will quit, and half will go crazy.

Parallel inheritance hierarchies

When you create a subclass of a class, you must create another subclass of another class.

Uniform dependency distribution

When performing any modifications, you have to look for all the dependencies (uses) of this class and make many small changes. One change - edits in many classes.

Complex modification tree

This smell is the opposite of the previous one: changes affect a large number of methods of the same class. As a rule, the dependency in such code is cascading: having changed one method, you need to fix something in another, and then in a third, and so on. One class - many changes.

“Garbage smells”

A rather unpleasant category of odors that causes headaches. Useless, unnecessary, old code. Fortunately, modern IDEs and linters have learned to warn about such odors.

A large number of comments in the method

The method has a lot of explanatory comments on almost every line. This is usually associated with a complex algorithm, so it is better to divide the code into several smaller methods and give them meaningful names.

Code duplication

Different classes or methods use the same blocks of code.

Lazy class

The class takes on very little functionality, although a lot of it was planned.

Unused code

A class, method or variable is not used in the code and is “dead weight”.

Excessive coupling

This category of smells is characterized by a large number of unnecessary connections in the code.

Third party methods

A method uses another object's data much more often than it uses its own data.

Inappropriate intimacy

A class uses service fields and methods of another class.

Long class calls

One class calls another, which requests data from the third, that from the fourth, and so on. Such a long chain of calls means a high level of dependence on the current class structure.

Class-task-dealer

A class is only needed to pass a task to another class. Maybe it should be removed?

Refactoring Techniques

Below we will talk about initial refactoring techniques that will help eliminate the described code smells.

Class selection

The class performs too many functions; some of them need to be moved to another class. For example, there is a class Humanthat also contains a residential address and a method that provides the full address:
class Human {
   private String name;
   private String age;
   private String country;
   private String city;
   private String street;
   private String house;
   private String quarter;

   public String getFullAddress() {
       StringBuilder result = new StringBuilder();
       return result
                       .append(country)
                       .append(", ")
                       .append(city)
                       .append(", ")
                       .append(street)
                       .append(", ")
                       .append(house)
                       .append(" ")
                       .append(quarter).toString();
   }
}
It would be a good idea to place the address information and method (data processing behavior) in a separate class:
class Human {
   private String name;
   private String age;
   private Address address;

   private String getFullAddress() {
       return address.getFullAddress();
   }
}
class Address {
   private String country;
   private String city;
   private String street;
   private String house;
   private String quarter;

   public String getFullAddress() {
       StringBuilder result = new StringBuilder();
       return result
                       .append(country)
                       .append(", ")
                       .append(city)
                       .append(", ")
                       .append(street)
                       .append(", ")
                       .append(house)
                       .append(" ")
                       .append(quarter).toString();
   }
}

Method selection

If any functionality in a method can be grouped, it should be placed in a separate method. For example, a method that calculates the roots of a quadratic equation:
public void calcQuadraticEq(double a, double b, double c) {
    double D = b * b - 4 * a * c;
    if (D > 0) {
        double x1, x2;
        x1 = (-b - Math.sqrt(D)) / (2 * a);
        x2 = (-b + Math.sqrt(D)) / (2 * a);
        System.out.println("x1 = " + x1 + ", x2 = " + x2);
    }
    else if (D == 0) {
        double x;
        x = -b / (2 * a);
        System.out.println("x = " + x);
    }
    else {
        System.out.println("Equation has no roots");
    }
}
Let’s move the calculation of all three possible options into separate methods:
public void calcQuadraticEq(double a, double b, double c) {
    double D = b * b - 4 * a * c;
    if (D > 0) {
        dGreaterThanZero(a, b, D);
    }
    else if (D == 0) {
        dEqualsZero(a, b);
    }
    else {
        dLessThanZero();
    }
}

public void dGreaterThanZero(double a, double b, double D) {
    double x1, x2;
    x1 = (-b - Math.sqrt(D)) / (2 * a);
    x2 = (-b + Math.sqrt(D)) / (2 * a);
    System.out.println("x1 = " + x1 + ", x2 = " + x2);
}

public void dEqualsZero(double a, double b) {
    double x;
    x = -b / (2 * a);
    System.out.println("x = " + x);
}

public void dLessThanZero() {
    System.out.println("Equation has no roots");
}
The code for each method has become much shorter and clearer.

Transferring the entire object

When calling a method with parameters, you can sometimes see code like this:
public void employeeMethod(Employee employee) {
    // Некоторые действия
    double yearlySalary = employee.getYearlySalary();
    double awards = employee.getAwards();
    double monthlySalary = getMonthlySalary(yearlySalary, awards);
    // Продолжение обработки
}

public double getMonthlySalary(double yearlySalary, double awards) {
     return (yearlySalary + awards)/12;
}
In the method, employeeMethodas many as 2 lines are allocated for obtaining values ​​and storing them in primitive variables. Sometimes such designs take up to 10 lines. It is much easier to pass the object itself to the method, from where you can extract the necessary data:
public void employeeMethod(Employee employee) {
    // Некоторые действия
    double monthlySalary = getMonthlySalary(employee);
    // Продолжение обработки
}

public double getMonthlySalary(Employee employee) {
    return (employee.getYearlySalary() + employee.getAwards())/12;
}
Simple, short and concise.

Logical grouping of fields and placing them in a separate class

Despite the fact that the above examples are very simple and when looking at them many may ask the question “Who actually does this?”, many developers, due to inattention, unwillingness to refactor the code, or simply “It will do,” make similar structural errors.

Why refactoring is effective

The result of a good refactoring is a program whose code is easy to read, modifications to the program logic do not become a threat, and the introduction of new features does not turn into code parsing hell, but a pleasant activity for a couple of days. Refactoring should not be used if it would be easier to rewrite the program from scratch. For example, the team estimates the labor costs for parsing, analyzing and refactoring code to be higher than for implementing the same functionality from scratch. Or the code that needs to be refactored has a lot of errors that are difficult to debug. Knowing how to improve the structure of code is mandatory in the work of a programmer. Well, it’s better to learn Java programming at JavaRush - an online course with an emphasis on practice. 1200+ tasks with instant verification, about 20 mini-projects, game tasks - all this will help you feel confident in coding. The best time to start is now :) How refactoring works in Java - 4

Resources for further diving into refactoring

The most famous book about refactoring is “Refactoring. Improving the Design of Existing Code” by Martin Fowler. There is also an interesting publication on refactoring, written based on a previous book - “Refactoring with Patterns” by Joshua Kiriewski. Speaking of templates. When refactoring, it is always very useful to know the basic application design patterns. These great books will help with this:
  1. “Design Patterns” - by Eric Freeman, Elizabeth Freeman, Kathy Sierra, Bert Bates from the Head First series;
  2. “Readable Code, or Programming as an Art” - Dustin Boswell, Trevor Faucher.
  3. “Perfect Code” by Steve McConnell, which outlines the principles of beautiful and elegant code.
Well, a few articles about refactoring:
  1. Hell of a task: let's start refactoring legacy code ;
  2. Refactoring ;
  3. Refactoring for everyone .
    Comments
    TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
    GO TO FULL VERSION