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 devoted to writing code. Most novice developers believe that this is their future activity. This is partly true, but the tasks of the programmer also include maintaining and refactoring the code. Let's talk about refactoring today. How refactoring works in Java - 1

Refactoring in CodeGym course

The CodeGym course touches on 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 automatic tools that make life incredibly easy.

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 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;
    }
}
It turned out to be a very cumbersome code. Even beginners rarely write like this, but there is such a risk. It would seem, why is there a block here if-else, if 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 methods and techniques of refactoring, which we will consider in more detail.

What is refactoring for?

There are several reasons. For example, the pursuit of simplicity and conciseness of the code. Supporters of this theory believe that the code should be as concise as possible, even if it takes several dozen lines of comments to understand it. Other developers believe that code should be refactored to the point that it can be understood with as few comments as possible. Each team chooses its position, but you need to remember that refactoring is not a reduction . Its main goal is to improve the structure of the code. Several tasks can be included in this global goal:
  1. Refactoring improves understanding of code that is written by another developer;
  2. Helps to 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.

"Smells of Code"

When code needs 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.

Unreasonably large elements

There are bulky classes and methods that cannot be effectively worked with precisely because of their huge size.

big class

Such a class has a huge number of lines of code and many different methods. It's 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 such a class is overloaded. In this case, it helps to separate part of the functionality into a separate class. We'll talk more about this in the Refactoring Techniques section.

big method

This "smell" occurs when a developer adds new functionality to a method. “Why should I move the parameter check into 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. So 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 the method takes more than 10-15 lines of code, you should determine the tasks and subtasks that it performs, and try to move the subtasks into a separate method.
Several ways to eliminate the big method:
  • Separate part of the method functionality into a separate method;
  • If local variables do not allow you to take out 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 for storing data grows in the class over time. For example, if you use primitive types instead of small objects for storing data (currency, date, telephone numbers, etc.) or constants for encoding some information. A good practice in this case would be a logical grouping of fields and removal into a separate class (class selection). You can also include methods for processing this data in the class.

Long list of parameters

A fairly common mistake, especially in conjunction with a large method. Usually it occurs if the functionality of the method is overloaded, or the method combines several algorithms in itself. Long lists of parameters are very difficult to understand, and it is inconvenient to use such methods. Therefore, it is better to pass the entire object. If an object doesn't have enough data, it's worth using a more generic object or splitting the functionality of a method so that it handles logically related data.

Data groups

Often logically related groups of data appear in the code. For example, connection parameters in the database (URL, username, password, scheme name, etc.). If none of the fields can be deleted from the list of elements, then the list is a group of data that needs to be moved to a separate class (class selection).

Solutions that spoil the concept of OOP

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

Renunciation of Inheritance

If a subclass uses a minimal part of the functions of the parent class, then it smells like the wrong hierarchy. Usually, in this case, unnecessary methods are simply not overridden or throw exceptions. If a class is inherited from another, this implies the almost complete use of its functionality. Correct hierarchy example: How refactoring works in Java - 2 Incorrect hierarchy example: How refactoring works in Java - 3

switch statement

What's 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 a class has a temporary field that an object needs only occasionally, when it is filled with values, and the rest of the time it is empty or, God forbid, , then the nullcode “smells”, and such a design is a dubious decision.

Odors that make modification difficult

These "smells" are more serious. The rest basically impair understanding of the code, while these make it impossible 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 for 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 edits. One change - edits in many classes.

Complex modification tree

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

“Garbage smells”

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

Too many 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 large one was planned.

Unused code

The class, method or variable is not used in the code and is a "dead weight".

Overconnectedness

This category of smells is characterized by a large number of unjustified links in the code.

Third Party Methods

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

Misplaced proximity

The 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 dependency on the current class structure.

Class-task-dealer

The class is needed only to pass the task to another class. Maybe it should be removed?

Refactoring Techniques

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

Class selection

The class performs too many functions, some must be moved to another class. For example, there is a class Humanthat also contains the address of residence 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's good practice to move the address information and method (data handling behavior) to 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();
   }
}

Extraction method

If some functionality can be grouped in a method, it should be moved to 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 of each method has become much shorter and clearer.

Passing 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 removal into a separate class

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

Why refactoring is effective

The result of 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 a hell of code analysis, but a pleasant experience for a couple of days. Refactoring should not be used if it is easier to rewrite the program from scratch. For example, a team estimates the labor costs of parsing, analyzing, and refactoring code to be higher than implementing the same functionality from scratch. Or the code that needs to be refactored has a lot of bugs that are hard to debug. Knowing how to improve the structure of the code is indispensable in the work of a programmer. Well, it’s better to learn Java programming on CodeGym, 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 an Extra Dive into Refactoring

The most famous book about refactoring is Refactoring. Improving the design of existing code” by Martin Fowler. There is also an interesting book on refactoring 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. Here are some great books to help you:
  1. Design Patterns by Eric Freeman, Elizabeth Freeman, Catty 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. A Hell of a Problem: Getting Started with Legacy Code Refactoring ;
  2. refactoring ;
  3. Refactoring for everyone .
    Comments
    TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
    GO TO FULL VERSION