User Brian
Brian
Level 41

How refactoring works in Java

Published in the Java Developer group
As you learn how to programming, you spend a lot of time writing code. Most beginning developers believe that this is what they will do in the future. This is partly true, but a programmer's job also includes maintaining and refactoring code. Today we're going to talk about refactoring. How refactoring works in Java - 1

Refactoring on CodeGym

Refactoring is covered twice in the CodeGym course: The big task provides an opportunity to get acquainted with real refactoring through practice, and the lesson on refactoring in IDEA helps you dive into automated tools that will make your life incredibly easier.

What is refactoring?

It is changing the structure of code without changing its functionality. For example, suppose we have 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;
        }
    }
This is a rather unwieldy code. Even beginners would rarely write something like this, but there is a chance. Why use an if-else block if you can write the 6-line method more concisely?

 public boolean max(int a, int b) {
      return a > b;
 }
Now we have a simple and elegant method that performs the same operation as the example above. This is how refactoring works: you change the structure of code without affecting its essence. There are many refactoring methods and techniques that we'll take a closer look at.

Why do you need refactoring?

There are several reasons. For example, to achieve simplicity and brevity in code. Proponents of this theory believe that code should be as concise as possible, even if several dozen lines of comments are needed to understand it. Other developers are convinced that code should be refactored to make it understandable with the minimum number of comments. Each team adopts its own position, but remember that refactoring does not mean reduction. Its main purpose is to improve the structure of code. Several tasks can be included in this overall purpose:
  1. Refactoring improves understanding of code written by other developers.
  2. It helps find and fix bugs.
  3. It can accelerate the speed of software development.
  4. Overall, it improves software design.
If refactoring is not performed for a long time, development may encounter difficulties, including a complete stop to the work.

"Code smells"

When the code requires refactoring, it is said to have a "smell". Of course, not literally, but such code really doesn't look very appealing. Below we'll explore basic refactoring techniques for the initial stage.

Unreasonably large classes and methods

Classes and methods can be cumbersome, impossible to work with effectively precisely because of their huge size.

Large 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 the class grows. As a rule, too much functionality is crammed into such a class. In this case, it helps to move part of the functionality into a separate class. We’ll talk about this in more detail in the section on refactoring techniques.

Long method

This "smell" arises when a developer adds new functionality to a method: "Why should I put a parameter check into a separate method if I can write the code here?", "Why do I need a separate search method to find the maximum element in an array? Let's keep it here. The code will be clearer this way", and other such misconceptions.

There are two rules for refactoring a long method:

  1. If you feel like adding a comment when writing a method, you should put the functionality in 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 put the subtasks into a separate method.

There are a few ways to eliminate a long method:

  • Move part of the method's functionality into a separate method
  • If local variables prevent you from moving part of the functionality, you can move the entire object to another method.

Using a lot of primitive data types

This problem typically occurs when the number of fields in a class grows over time. For example, if you store everything (currency, date, phone numbers, etc.) in primitive types or constants instead of small objects. In this case, a good practice would be to move a logical grouping of fields into a separate class (extract class). You can also add methods to the class to process the data.

Too many parameters

This is a fairly common mistake, especially in combination with a long method. Usually, it occurs if a method has too much functionality, or if a method implements multiple algorithms. Long lists of parameters are very difficult to understand, and using methods with such lists is inconvenient. As a result, it's better to pass an entire object. If an object doesn't have enough data, you should use a more general object or divide up the method's functionality so that each method processes logically related data.

Groups of data

Groups of logically related data often appear in code. For example, database connection parameters (URL, username, password, schema name, etc.). If not a single field can be removed from a list of fields, then these fields should be moved to a separate class (extract class).

Solutions that violate OOP principles

These "smells" occur when a developer violates proper OOP design. This happens when he or she doesn't fully understand OOP capabilities and fails to fully or properly use them.

Failure to use inheritance

If a subclass uses only a small subset of the parent class's functions, then it smells of the wrong hierarchy. When this happens, usually the superfluous methods are simply not overridden or they throw exceptions. One class inheriting another implies that the child class uses nearly all of the parent class's functionality. Example of a correct hierarchy: How refactoring works in Java - 2Example of an incorrect hierarchy: How refactoring works in Java - 3

Switch statement

What could be wrong with a switch statement? It is bad when it becomes very complex. A related problem is a large number of nested if statements.

Alternative classes with different interfaces

Multiple classes do the same thing, but their methods have different names.

Temporary field

If a class has a temporary field that an object needs only occasionally when its value is set, and it is empty or, God forbid, null the rest of the time, then the code smells. This is a questionable design decision.

Smells that make modification difficult

These smells are more serious. Other smells mainly make it harder to understand code, but these prevent you from modifying it. When you try to introduce any new features, half of the developers quit, and half go crazy.

Parallel inheritance hierarchies

This problem is manifests itself when subclassing a class requires you to create another subclass for a different class.

Uniformly distributed dependencies

Any modifications require you to look for all of a class's uses (dependencies) and make a lot of small changes. One change — edits in many classes.

Complex tree of modifications

This smell is the opposite of the previous one: changes affect a large number of methods in one class. As a rule, such code has cascading dependence: changing one method requires you to fix something in another, and then in the 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 of such odors.

A large number of comments in a method

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

Duplicated code

Different classes or methods use the same blocks of code.

Lazy class

A class takes on very little functionality, though it was planned to be large.

Unused code

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

Excessive connectivity

This category of odors is characterized by a large number of unjustified relationships in the code.

External methods

A method uses data from another object much more often than its own data.

Inappropriate intimacy

A class depends on the implementation details of another class.

Long class calls

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

Task-dealer class

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

Refactoring techniques

Below we'll discuss basic refactoring techniques that can help eliminate the described code smells.

Extract a class

A class performs too many functions. Some of them must be moved to another class. For example, suppose we have a Human class that also stores a home address and has a method that returns 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 put the address information and associated method (data processing behavior) into 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();
    }
 }

Extract a method

If a method has some functionality that can be isolated, you should place it 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");
        }
    }
We calculate each of the three possible options in 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");
    }
Each method's code has become much shorter and easier to understand.

Passing an entire object

When a method is called with parameters, you may sometimes see code like this:

 public void employeeMethod(Employee employee) {
     // Some actions
     double yearlySalary = employee.getYearlySalary();
     double awards = employee.getAwards();
     double monthlySalary = getMonthlySalary(yearlySalary, awards);
     // Continue processing
 }
 
 public double getMonthlySalary(double yearlySalary, double awards) {
      return (yearlySalary + awards)/12;
 }
The employeeMethod has 2 whole lines devoted to receiving values and storing them in primitive variables. Sometimes such constructs can take up to 10 lines. It is much easier to pass the object itself and use it to extract the necessary data:

 public void employeeMethod(Employee employee) {
     // Some actions
     double monthlySalary = getMonthlySalary(employee);
     // Continue processing
 }
 
 public double getMonthlySalary(Employee employee) {
     return (employee.getYearlySalary() + employee.getAwards())/12;
 }

Simple, brief, and concise.

Logically grouping fields and moving them into a separate classDespite the fact that the examples above are very simple, and when you look at them, many of you may ask, "Who does this?", many developers do make such structural errors because of carelessness, unwillingness to refactor the code, or simply an attitude of "that's good enough".

Why refactoring is effective

As a result of good refactoring, a program has easy-to-read code, the prospect of altering its logic is not frightening, and introducing new features doesn't become code analysis hell, but is instead a pleasant experience for a couple of days. You shouldn't refactor if it would be easier to write a program from scratch. For example, suppose your team estimates that the labor required to understand, analyze, and refactor code will be greater than implementing the same functionality from scratch. Or if the code to be refactored has lots of problems that are difficult to debug. Knowing how to improve the structure of code is essential in the work of a programmer. And learning to program in Java is done best on CodeGym, the online course that emphasizes 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 to further immerse yourself in refactoring

The most famous book on refactoring is "Refactoring. Improving the Design of Existing Code" by Martin Fowler. There is also an interesting publication about refactoring, based on a previous book: "Refactoring Using Patterns" by Joshua Kerievsky. Speaking of patterns... When refactoring, it's always very useful to know basic design patterns. These excellent books will help with this: Speaking of patterns... When refactoring, it's always very useful to know basic design patterns. These excellent books will help with this:
  1. "Design Patterns" by Eric Freeman, Elizabeth Robson, Kathy Sierra, and Bert Bates, from the Head First series
  2. "The Art of Readable Code" by Dustin Boswell and Trevor Foucher
  3. "Code Complete" by Steve McConnell, which sets out the principles for beautiful and elegant code.
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION