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.
Refactoring in the JavaRush course
The JavaRush course covers the topic of refactoring twice:- Big challenge on level 5 of the Multithreading quest ;
- Lecture on refactoring in Intellij IDEA at level 9 of the Java Collections quest .
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-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 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:- Refactoring improves understanding of code written by another developer;
- Helps find and fix errors;
- Allows you to increase the speed of software development;
- Overall improves software composition.
“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:- If, when writing a method, you want to add a comment to the code, you need to separate this functionality into a separate method;
- 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.
- 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: Example of an incorrect hierarchy:switch statement
What could be wrong with an operatorswitch
? 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 classHuman
that 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, employeeMethod
as 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 :)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:- “Design Patterns” - by Eric Freeman, Elizabeth Freeman, Kathy Sierra, Bert Bates from the Head First series;
- “Readable Code, or Programming as an Art” - Dustin Boswell, Trevor Faucher.
- “Perfect Code” by Steve McConnell, which outlines the principles of beautiful and elegant code.
GO TO FULL VERSION