Victor Brandalise Victor is very curious and likes understanding how and why things work; this is what drives him to learn new things. Most of his time is spent learning Android and Kotlin, but he also leaves some time for other subjects.

Identifying and addressing Kotlin code smells

8 min read 2275 107

Identifying Addressing Kotlin Code Smells

Code smells, bad code patterns, and poorly thought-out code all refer to the same thing — a piece of code that works now but may generate problems for you in the future. These future issues can arise because the code is difficult to understand, modify, or test.

As a Kotlin developer, it’s important to identify and address these issues to ensure your code is maintainable. In this article, we’ll dive into the five most common code smells in Kotlin and provide practical solutions for fixing them.

Jump ahead:

What are code smells?

Code smells exist in all programming languages, and while there’s no definitive definition, code smells do share some general characteristics:

  • They’re subjective; what’s considered a code smell in one language might not be in another
  • They reduce the quality of code
  • They’re not bugs in the sense that users may notice them, but they can slow down or otherwise impede a developer’s efforts to add new features or modify a program

Code smells are not good for code bases and should be avoided whenever possible. I know this sounds obvious – like, who would intentionally write code that’s difficult to maintain?

Dealing with code smells is something even the most experienced developers do — the difference is that they’re good at identifying these code smells and fixing them!

Identifying code smells

The first step to identifying code smells is to understand them. For example, it’s very difficult to remove a “primitive obsession” code smell if you don’t know anything about that.

Be sure to identify all code smells, even if you don’t have time to fix them immediately. All projects have code smells, but most developers are unaware of many of them. Identifying a code smell as soon as you spot it, either by leaving a to-do comment or by adding it to a list, is a great start for fixing it later.

To save precious development time, there are several automated tools that are handy for identifying code smells. For example, Detekt is a popular, open source and highly customizable library that identifies Kotlin code smells.

N.B., it’s important to remember that automated tools can’t always understand context, so in the end it’s up to you to decide if something really is a code smell or if it can be ignored

Addressing code smells

The best way to address or eliminate a code smell varies according to the type of code smell you’ve uncovered. However, there are a few things you should not do — regardless of the type of code smell you’re dealing with. Let’s take a look.

Don’t add functionally and refactor at the same time

A common mistake people make is trying to add functionality and refactor at the same time, or trying to refactor and change something else simultaneously.

If you’re implementing a new feature and see the possibility of refactoring a piece of code, finish implementing the functionality first and then refactor. This ensures that you’re focused on just one task at a time and reduces complexity. If you’re working on a complex problem and try to refactor some code at the same time, that’s only going to make things harder.



Don’t forget to test your code

We refactor code to make it easier to read, maintain, and so on. However, that’s not very useful if every time we refactor some piece of code, something breaks. That’s why it’s highly recommended that you first make sure there are tests covering the piece of code you wish to change.

If there are no tests covering something you want to refactor, first write tests for that piece of code, and then refactor. Not only have you improved the code by refactoring it, but you also added some tests; that’s very useful.

Don’t obsess

And finally, all projects have code smells. Prioritize the code smells that are most important, and deal with those.

Common code smells

Now, let’s take a look at five common code smells (duplicate code, long method/class, tight coupling, primitive obsession, and magic numbers) and review solutions to address them.

Duplicate code

I’d like to start by stating that not all code duplication is a code smell. You don’t need to go around refactoring your application every time you copy an existing code block.

The main problem with duplicate code is that it increases maintenance costs. If you need to change some piece of code that has a bug and it is duplicated all over the project then that’s going to be more difficult.

Fortunately, automated tools like Detekt are very good at spotting duplicate code, so most of the time you won’t need to handle this manually.


More great articles from LogRocket:


The way to fix this code smell is by refactoring the common code into a single function or module that can be reused in multiple places.

Let’s imagine you need to calculate the sale price for some items and by default you have a 10% margin:

val bookSalePrice = book.price * (1 + 0.1)
val penSalePrice = pen.price * (1 + 0.1)

The problem with the code above is that you need to repeat (1 + 0.1) multiple times — that’s code duplication. To solve that, extract the common piece of code into a new function:

fun calculateSalePrice(itemPrice: Double, margin: Double): Double {
    return itemPrice * (1 + margin)
}

Now, call your new function instead:

val margin = 0.1 // 10% margin
calculateSalePrice(book.price, margin)
calculateSalePrice(pen.price, margin)

As a rule of thumb, one duplication is okay, two is suspicious, and three means there’s a common abstraction that you need to extract.

Long method/class

Unlike duplicate code, which is sometimes valid, a long method or class is always a code smell.

Long methods and classes do too much, thereby increasing the complexity of a project and reducing its code quality. They either indicate that your code is poorly organized or that it has too many responsibilities.

If you’re using automated tools, you can usually define a max line count for functions and classes. That way, you’ll get an alert if your functions or classes get too long. If that’s the case, you can fix it by extracting the existing code into smaller functions or by creating smaller classes that have fewer responsibilities.

Using the same example as above, imagine the margin changes based on how many items you have in inventory:

fun calculateSalePrice(itemPrice: Double, quantityInStock: Int, margin: Double): Double {
   return if (quantityInStock < 10) {
       if (itemPrice <= 10.0) {
           val newMargin = margin * 2 // 2x the margin for cheap items
           calculateSalePrice(itemPrice, newMargin)
       } else if (itemPrice > 500.0) {
           val newMargin = margin * 1.5 // 1.5x the margin for expensive
           calculateSalePrice(itemPrice, newMargin)
       } else {
           val newMargin = margin * 1.7 // 1.7x the margin in between items
           calculateSalePrice(itemPrice, newMargin)
       }
   } else if (quantityInStock < 500) {
       val newMargin = margin * 1.3 // 1.3x the margin
       calculateSalePrice(itemPrice, newMargin)
   } else {
       calculateSalePrice(itemPrice, margin)
   }
}

This function has 18 lines; we can make it easier to read by breaking it down into smaller functions, like so:

fun calculateSalePrice(itemPrice: Double, quantityInStock: Int, margin: Double): Double {
   return if (quantityInStock < 10) {
       calculateSalePriceForLowStockItems(itemPrice, margin)
   } else if (quantityInStock < 500) {
       calculateSalePriceForMediumStockItems(margin, itemPrice)
   } else {
       calculateSalePriceForHighStockItems(itemPrice, margin)
   }
}

By refactoring the code into smaller functions we were able to reduce the code from 18 lines to just eight.

Tight coupling

There’s no way to write software without coupling — that would be like writing code that doesn’t depend on other things! Some level of coupling is needed, but if we have too much it becomes “tight coupling” and consequently a code smell.

Tight coupling is when two or more classes have a direct, hard-coded dependency on one another. This can make it difficult to change or modify one class without affecting the other(s).

Automated tools are unlikely to help with this code smell, because as I mentioned earlier, code always has some coupling. And, unfortunately, it’s not always easy to define how much coupling is “too much”.

If you’re hard coding your classes, one simple thing you can do to reduce tight coupling is to use dependency injection. Instead of initiating another class manually, get it injected through the constructor.

Let’s look at an example of tight coupling.

Imagine you have a Car class that has an Engine. You’re manually instantiating the Engine class:

class Engine {
   ...
}

class Car {
   private val engine = Engine()
}

Now, suppose your Engine class requires a new parameter in the constructor:

class Engine constructor(private val coolingSystem: CoolingSystem) {
   ...
}

class Car {
   private val engine = Engine(CoolingSystem.AIR)
}

You changed the Engine class, but because the Car class is tightly coupled to it, you have to change that as well. One way to fix that is by using dependency injection:

class Car constructor(private val engine: Engine) {
   ...
}

Now if you change the Engine constructor, you don’t need to change your Car class because they’re not tightly coupled anymore.

Primitive obsession

Primitive obsession is the excessive use of primitive data types, such as integers, strings, or Booleans, to represent domain-specific concepts.

It’s important to point out that this code smell is not called “using primitives”, instead it’s called “primitive obsession.” Most of the time, using primitives is fine; this becomes a code smell when everything in your code is primitive.

Primitive obsession results in decreased code readability. When primitives are used to represent domain-specific concepts, it can be difficult to understand the intent of the code.

Automated tools are not likely to be of much use in identifying this code smell. There’s simply no way to define rules for identifying this code smell because it varies from project to project.

To resolve primitive obsession, it is best to create custom data types, also known as value objects, that represent domain-specific concepts. These value objects can have specific behavior and validation rules, making it easier to understand and manipulate the data.

Imagine there’s an app that alerts the user every day at a specific time. The code might look like this:

val hourMinute = "11:30" // This would probably come from user input

val hour = hourMinute.split(":")[0].toInt()
val minute = hourMinute.split(":")[1].toInt()

val nextAlertTime = Date().time + (hour * 1000 * 60 * 60) + (minute * 1000 * 60)

This code works, but the following might be a better way to do this:

data class HourMinute(val hours: Int, val minutes: Int) {
   init {
       require(hours >= 0) { "hour can't be negative" }
       require(minutes >= 0) { "minute can't be negative" }
   }

   val millis = (hours * 60 * 60 * 1000) + (minutes * 60 * 1000)
}

fun Date.plus(hourMinute: HourMinute) = time + hourMinute.millis

val hourMinute = HourMinute(hours = 11, minutes = 30)
val nextAlertTime = Date().plus(hourMinute)

First of all, this code removes the possibility of having invalid dates; if you used a string it’s possible that you’d get values like “”, “:”, “aa:aa”, and so on.

It’s also easier to understand. In the first example, is “11:30”, 11 minutes and 30 seconds? Is it some kind of coordinate, like x=11, y=30? It’s not clear because it’s just a string and that can represent many things.

Magic numbers

Magic numbers refers to the use of hard-coded, literal values in code that have no descriptive meaning. These values are called “magic” because they have no clear explanation or context, making it difficult to understand the purpose of the code that uses them.

Magic numbers can also make it more difficult to change the code. Imagine you have two places where the number “15” is used. How do you know if you need to replace both of them?

Automated tools are really good at detecting magic numbers. You can easily rely on them for this purpose.

To resolve the issue of magic numbers, it’s best to replace them with named constants or enumerated values that have descriptive names that explain their purpose. This makes the code easier to understand and maintain, as the values have a clear meaning and context.

Let’s use the same example from the last code smell. If you take close look, you can see that we have some magic numbers like 1000 and 60:

private const val ONE_SECOND_MILLIS = 1000
private const val ONE_MINUTE_MILLIS = 60 * ONE_SECOND_MILLIS
private const val ONE_HOUR_MILLIS = 60 * ONE_MINUTE_MILLIS

class HourMinute(val hours: Int, val minutes: Int) {
   ...
   val millis = hours * ONE_HOUR_MILLIS + minutes * ONE_MINUTE_MILLIS
}

We can address this code smell by replacing the numbers with constants that define what they are. This makes it much easier for developers to understand what’s happening.

Further reading

To further improve your knowledge of code smells, I recommend the following books:

  • Refactoring: Improving the Design of Existing Code: Martin Fowler provides a comprehensive guide to refactoring, focusing on identifying and fixing code smells; this book is a must-read for developers interested in improving their code quality
  • Clean Code: A Handbook of Agile Software Craftsmanship: Robert C. Martin provides a comprehensive guide for writing clean, maintainable, and scalable code and also discusses how to identify and fix code smells

There are many other books on this topic, but these two are a good start.

Conclusion

Maintainable code isn’t the norm, it requires effort to write. But being able to produce maintainable code is an essential skill that all developers should master.

In this article, we discussed how to spot code smells using both manual and automated tools. We also investigated how to remove five of the most common code smells. I hope these tips will help you become a better Kotlin developer.

LogRocket: Instantly recreate issues in your Android apps.

LogRocket is an Android monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your Android apps.

LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.

Start proactively monitoring your Android apps — try LogRocket for free.

Victor Brandalise Victor is very curious and likes understanding how and why things work; this is what drives him to learn new things. Most of his time is spent learning Android and Kotlin, but he also leaves some time for other subjects.

Leave a Reply