1. Introduction

Most programmers strive to create good software. But, unfortunately, it’s more complex to achieve that than we’d think at first.

In this tutorial, we’ll dive into software quality, and its relation to code smells.

2. Software Requirements

Creating good software is a challenging task. It has to fulfill many requirements at the same time:

  • Provide useful features
  • It shouldn’t contain bugs
  • Have a user-friendly interface
  • Respond to actions in a reasonable timeframe
  • Don’t consume too many resources
  • It should be easy (and cheap) to operate (install, host, etc.) and maintain (add features, fix bugs)

Many of these characteristics are conflicting. Therefore, we must strive for an acceptable balance between these qualities.

However, some of these things are more important than others. For example, Mich Ravera said the following:

If it doesn’t work, it doesn’t matter how fast it doesn’t work.

Like in this list, maintainability is often standing behind other qualities. However, in most cases, maintainable code inherently contains fewer bugs, has better performance characteristics, and it’s easier to operate. Therefore, if we make our code maintainable, other characteristics will most likely improve, too.

3. Code Quality

Many factors impact maintainability, including code quality. But code quality is a complicated topic in itself, too. Usually, it’s easier to define what makes the code bad than what makes it good.

When we work with a piece of bad code, we usually can feel that it’s bad. In some cases, it’s easy to spot the problem. But more often than not, it’s much less obvious. In those cases, it’s just doesn’t feel right; something’s off. Even if we can’t pinpoint the problem, we feel that the code smells.

These problems can have many forms. Sometimes it’s hard to understand what the code does or how does it do it. In other cases, it’s hard to change behavior when the requirements change or we have to fix a bug. Similarly, it may be hard to add new features.

What’s common is that problems tend to arise together. It has psychological reasons. To understand these, let’s take a look at how code evolves.

3.1. Codebase Evolution

Let’s consider that we see a low-qualitys codebase. How will we change that? Will we add the most beautiful code that we ever created? Most probably not. We’ll add more garbage to the existing pile because it’s already a smelly codebase. A little more doesn’t matter.

One smelly part doesn’t matter, indeed. But if we behave like this, the software will be smellier each time we touch it. Eventually, it’ll rot. And no one wants to work with something that’s rotting. So we start to say phrases like “we don’t want to touch this module, because last time we had to debug it for a week” or “it would be best to rewrite it from scratch”. Usually, we don’t get the chance to dump and restart a project. So we’ll get stuck with this abomination we hate.

Note that it’s not necessarily a good decision to rewrite everything from scratch. The required effort is usually underestimated. What works better is to identify and separate some parts and rewrite them without touching the other parts. Once it’s done, we can move on to the next module. This iterative approach works better in most cases.

In the end, from a quality perspective, it doesn’t matter whether we rewrite the software iteratively or all at once. What matters is if our behavior changed or we work the same way we did before. If we always adapt new code to its surroundings, the process leads to another catastrophic codebase. We won’t understand why. We didn’t make the mistakes we did before. However, we made others.

3.2. How to Write Good Code

The solution is to be mindful of our behavior. Don’t adapt to the bad surroundings. We shouldn’t fix the problem but prevent it. We should always add good-quality, clean code, no matter what. As a result, over time, the overall code quality will improve.

In the end, the old, smelly parts will start to disturb us. They’ll stand out from the rest of the code. So the last thing to do is to identify them.

Fortunately, we tend to repeat mistakes over and over. Smart people recognized the patterns, collected them, and organized them. We call these patterns code smells.

4. Types of Code Smells

Code smells have a catalog. In this catalog, they’re grouped based on common characteristics. In this section, we’ll take a look at those groups without being exhaustive.

4.1. Bloaters

Bloaters are constructs (classes, methods, etc.) in the code which are too large, so we can’t work with them effectively. In most cases, they only appear over time as we add more functionality to the software. We add a line here, a method there, and boom, we have a class with 2000 lines.

A less obvious bloater smell has the name data clumps:

class DateUtil {
    boolean isAfter(int year1, int month1, int day1, int year2, int month2, int day2) {
        // implementation
    }
  
    int differenceInDays(int year1, int month1, int day1, int year2, int month2, int day2) {
        // implementation
    }
  
    // other date methods
}

All the methods above work with dates. Therefore, all of them receive three integer arguments: year, month, and day. Grouping them in a Date class makes the code more readable:

class Date {
    int year;
    int month;
    int day;
}

class DateUtil {
    boolean isAfter(Date date1, Date date2) {
        // implementation
    }
  
    int differenceInDays(Date date1, Date date2) {
        // implementation
    }
  
    // other date methods
}

In addition, we should move those methods to the Date class to encapsulate the data with the operations. But that’s a different story.

4.2. Object-Orientation Abusers

Sometimes it’s hard to write good object-oriented code. If we don’t follow the principles, we may run into one of these smells.

For example, the switch-case statement is considered a code smell in OO. Let’s consider this example:

class Animal {
    String type;
  
    String makeSound() {
        switch (type) {
            case "cat":
                return "meow";
            case "dog":
                return "woof";
            default:
                throw new IllegalStateException();
        }
    }
}

Instead of switch-case, we should use polymorphism:

abstract class Animal {
    abstract String makeSound();
}

class Cat extends Animal {
    @Override
    String makeSound() {
        return "meow";
    }
}

class Dog extends Animal {
    @Override
    String makeSound() {
        return "woof";
    }
}

We not only got rid of the switch-case statement. On top of that, our classes won’t be able to be in an illegal state anymore.

4.3. Change Preventers

Change preventers violate the Single Responsibility Principle.

For example, shotgun surgery means that we need to touch multiple parts of the code to make a behavior change.

From a certain point of view, divergent change is the opposite. It means that multiple behavior changes affect the same part of the code.

There are code smells that don’t always signal poor code. For example, we implement parallel inheritance hierarchies on purpose when we use the abstract factory design pattern. In other cases, it’s a wrong design that causes many headaches.

4.4. Dispensables

Dispensables introduce noise to the code. Without them, the code can be much cleaner.

For example, consider this snippet:

// amount
double a = order.getAmount();
// discount factor
double b = 1;
if (a > 10) {
    b = 0.9;
}
// discounted price
double c = product.getPrice() * b;
// order sum price
double d = a * c;

If we use appropriate variable names, we can get rid of the comments:

double amount = order.getAmount();
double discountFactor = 1;
if (amount > 10) {
    discountFactor = 0.9;
}
double discountedPrice = product.getPrice() * discountFactor;
double orderSumPrice = amount * discountedPrice;

Keep in mind that not all comments are code smells. If they explain what the code does or how it works, they signal that our code isn’t readable enough. But if they state why something is necessary, they provide valuable information. For example, when a weird edge case needs to be handled because of a special business requirement.

4.5. Couplers

Couplers prevent changing classes independently.

For example, inappropriate intimacy violates data hiding by accessing private parts of other classes.

Interestingly, sometimes two smells are the exact opposite of each other.

Consider message chains, where we chain method calls:

class Repository {
    Entity findById(long id) {
        // implementation
    }
}

class Service {
    Repository repository;

    Repository getRepository() {
        return repository;
    }
}

class Context {
    Service service;

    void useCase() {
        // the following is a message chain
        Entity entity = service.getRepository().findById(1);
        // using entity
    }
}

The solution is to introduce a method in the Service, which calls the Repository:

class Service {
    Repository repository;

    Entity findById(long id) {
        return repository.findById(id);
    }
}

class Context {
    Service service;

    void useCase() {
        Entity entity = service.findById(1);
        // using entity
    }
}

But in this example, the method Service.findById() is called a middle man, which is another smell. And to get rid of it, we should rewrite it to the original code.

Does it mean that we can’t win? Of course not. It means that the correct solution depends on what we use it for.

For example, data can be hierarchical by nature. So message chains don’t mean a problem because we want to access the data in different granularity.

For the example above, with Service.getRepository(), we’re violating the Law of Demeter because this code isn’t about data. It’s about behavior, and we’re exposing the Service class’s internal structure.

Alternatively, we could get rid of Service and use Repository directly from Context:

class Context {
    Repository repository;

    void useCase() {
        Entity entity = repository.findById(1);
        // using entity
    }
}

This way, we don’t have a middle man or message chains anymore. However, depending on the situation, it may suffer from other problems. For example, we may violate layer or module boundaries.

Also, note that the Service class suffers from another code smell in these examples. It’s an interesting exercise to find out what’s that smell is. We stated the answer in the last section.

4.6. Other Problems

Note that identifying and fixing code smells isn’t a silver bullet. Sometimes they represent problems. In other cases, they don’t. Like in the abstract factory example.

In other cases, a fix for a potential code smell is another one, for example, in the middle man vs. message chains case. But, again, we need to understand the purpose of the code to be able to make a good decision.

Also, code smells are far from identifying all problems with the code. Instead, they only aim to identify a few common problems that are usually easy but not obvious to fix.

5. How to Get Rid of Code Smells

We talked about that we should prevent code smells by writing clean code despite the codebase quality.

But sometimes, the smells are already present. Then we can identify problematic code by looking for code smells. But how can we get rid of those problems? We should change the structure of the code without altering its externally observable behavior. Refactoring is the technique that does exactly that. We won’t go into details since we already covered the topic here.

If we refactor regularly, we constantly improve the code quality. This process has a similar but opposing effect to introducing smelly code. We slowly make the code better, one piece at a time. And in the end, our codebase becomes great.

6. Conclusion

Maintainable code can prevent other flaws with software. Or at least it makes them easier to address.

Code smells are code quality problems. They’re general, repeating patterns that often appear in codebases. Identifying them helps us to make targeted improvements on the code quality.

In this tutorial, we talked about how to prevent a codebase’s quality from decreasing. Then we saw some examples of code smells. Finally, we stated that refactoring is the go-to technique to get rid of them.

Finally, as promised, Service is a lazy class in the previous examples.