Featured Image
Software Development

Understanding Code Smells: Types, Refactoring & Best Practices

If you have also witnessed your code not feeling as fresh as it should have been, or if you have doubts about any potential glitches or pop-ups, then you are definitely not alone! On the contrary, you are well aware and have already entered the world of ‘Code Smells.’ These are the little tell-tale signs of what might be wrong with your code and what could be done better or paid more attention to.

These signs can be spotted giving a heads-up to show where the code is broken or inefficient so as to enable timely improvisations.

In this article today, we will do an in-depth study of Code Smells. We will discuss types of code smells, their identification, and, most importantly, why they matter and how to spot them easily. 

What is a code smell?

Code smells are indicators that, when detected, source out critical loopholes, missing commands, or issues in the respective application or codebase. Sometimes, these can also predict errors or issues that might arise in the near future, even if there aren’t any currently. These errors can be modified during refactoring as the code smells are easy to detect and fix.

Code smells may also manifest as symptoms of deeper issues within the code. So, even if the code is performing and operating as intended, it may interfere with the development process and pose security or implementation threats while the program or application is running. 

Therefore, by definition, a code smell represents an indication of a problem simmering beneath the surface currently or that might originate in the near future. Why the word ‘smell’? Simply because it is easy to spot!

Types of code smells

Code smells originate when code created by developers does not meet the required design standards or principles. The reasons behind this can be varied, including lack of understanding or knowledge, time constraints, rushed efforts to meet deadlines, and at times, bypassing code reviews, which can lead to mistakes causing these glitches during development and resulting in code smells.

Code smells come in different kinds and vary based on the project as well as the developer. Here are some of the most common types:

1. Duplicate code

  • One of the most commonly detected code smells is duplication. It occurs when an entire algorithm or a part of it is repeated in two places, or data has repeated occurrences in the same code. 
  • If the execution functionality of certain code is shared, there’s a probability that the purpose of it is also shared among its parts. In this case, the functionality is initially implemented in one place and then reused when needed again. This increases the risk of maintaining the code and substantially raises security threats. It can also lead to bugs in the future.
  • Spotting this smell is easy, and rectification can be done quickly. The portion of code that is causing duplication should be extracted into a separate method and then called or referenced from every location where it’s duplicated.

2. Contrived complexity

  • At times, unnecessary complexities also lead to code smells. One common occurrence is Contrived Complexity, which refers to code that is unnecessarily difficult and complicated. This also makes understanding the code more challenging.
  • Such code is also hard to maintain and doesn’t offer any significant advantages.
  • Code that is easy to understand and comprehend contributes to convenient management and maintenance. Improving or recreating it is also simpler.

In the example below, the function aims to calculate the sum of even numbers from a given array. However, it is unnecessarily complex and could be simplified using higher-order functions in Swift.       

func findSumOfEvenNumbers(numbers: [Int]) -> Int {
    var sum = 0
    for number in numbers {
        if number % 2 == 0 {
            sum += number
        }
    }
    return sum
}

The refactored version is much more concise and easier to understand. It utilizes the filter method to filter out even numbers and the reduce method to calculate their sum. It effectively removes the contrived complexity present in the original implementation.

func findSumOfEvenNumbers(numbers: [Int]) -> Int {
    let evenNumbers = numbers.filter { $0 % 2 == 0 }
    return evenNumbers.reduce(0, +)
}

3. Improper names

  • Inefficient nomenclature of classes, variables, and functions leads to glitches, as it might suggest that your code isn’t clean.

For example, in the code below; it is unclear what is happening and what these variables are for:
 

// user data
int a;
String n;
String e;

Now, it is easier to understand the meaning with better variable names:

// user data
int age;
String name;
String email;

4. Dead code

  • A particular portion of the source code that is no longer functional or reachable is known as dead code.
  • If a dead code is detected, it should be deleted. If it needs to be referenced in the future, it can be accessed from version history. Deleting it will remove clutter. It would also reduce the effort of maintenance and avoid any prospective confusion arising at a later stage.

5. Middle man

  • The class of code that only delegates the tasks to another class is known as the Middle Man. These classes can complicate the code and add unnecessary noise to the codebase. 
  • Hence, if any class has no specific task and only acts as a “middle man,” then it needs to be removed to improve the program quality. 

In the below example, MiddleMan class has two methods, both of which simply delegate functionality to DataManager class. The MiddleMan class does not provide any additional value or functionality. It merely acts as an unnecessary middleman between the client code and the DataManager.

  class MiddleMan {
    private var dataManager: DataManager
   
    init() {
        self.dataManager = DataManager()
    }
   
    func fetchData() -> [String] {
        return dataManager.fetchData()
    }
   
    func saveData(_ data: [String]) {
        dataManager.saveData(data)
    }
}

As a resolution, the MiddleMan Class can be deleted and the DataManager class can be straight away used in the client code. This will be beneficial as it will streamline the codebase and also increase organized efforts.

6. Change preventers

  • Such code smells indicate that if changes are made in one section, then the changes should be made in other sections too. In this scenario, overall program development becomes expensive, and it adds to the complexity of the code.

In the snippet below, the function prints a message but hardcodes the version number into the message. If you need to update the app to a newer version, you would have to modify the function’s code directly, which is not ideal.

func printWelcomeMessage() {
    print("Welcome to our app version 1.0!")
}

To avoid this, we should pass the version number as a parameter in the function. Now the function can accept different version numbers without modifying its code.

func printWelcomeMessage(version: String) {
    print("Welcome to our app version \(version)!")
}

7. Long functions

  • If a function is excessively long, then in that case also code smell occurs. Such functions are responsible for too many tasks at the same time. There is no finite length which can be put under the Long Function category. When individual functions are compared, then it becomes obvious as the long functions makes the readability of the code difficult. Even the testing and reuse becomes quite complex with this.

8. Lazy class

  • In any code, it is expected that each class carries its own weight. If however any class is unable to do that then it needs to be merged with another class and become a part of a single unit. This is done to decrease the complexities and difficulty level of the code. 

Like in below example the class has no properties of its own and hence can be recognized as LazyClass. And since it is not adding any value at all to the codebase, it should be removed.

class LazyClass {
    // This class has no methods or properties, just an empty shell.
}

9. Inheritance method

  • Inheritance method also called Regmfused Bequest is the code smell with a lot of unused properties in classes. The main reason behind this is that the classes inherit many fields and methods from their source code, but all these properties usually remain obsolete and find no current use. It also creates empty method parts and makes the code heavy.

In the example stated below, the subclass Fish does not utilize the behavior it has inherited from its superclass – Animal. This leads to code smell. As a resolution, it is to be ensured that all subclasses use the inherited methods correctly or either override it completely.

class Animal {
    func makeSound() {
        // Code to make a generic animal sound
        print("Animal makes a sound.")
    }
}

class Cat: Animal {
    override func makeSound() {
        // Code to make a specific sound for a cat
        print("Meow!")
    }
}

class Fish: Animal {
    // Fish does not override the makeSound() method
    // Refusing to use the inherited behavior
}

// Usage
let cat = Cat()
cat.makeSound() // Output: "Meow!"

let fish = Fish()
fish.makeSound()

10. Uncommunicative name / Inconsistent names
 

11. Bloaters

  • Codes, classes, and methods that have grown to such proportions that they have become difficult to work with are called bloaters. 

Also read: Simplifying software development by building faster with reusable components

  12. God object

  • As the name suggests, God Object is majorly responsible for multitasking as well as managing different aspects of the application. This huge work load can however be difficult to maintain and also the testing of code becomes a bit complex. 

In the below example, the GodObject class has properties to store data related to customers, orders, and inventory. It contains various methods to process and manipulate this data. 

class GodObject {
    var customerData: [Customer]
    var orderData: [Order]
    var inventoryData: [Product]

    init() {
        customerData = []
        orderData = []
        inventoryData = []
    }

    func processCustomerData() {
        // Complex logic to process customer data
        // ...
    }

    func processOrderData() {
        // Complex logic to process order data
        // ...
    }

    func updateInventory() {
        // Complex logic to update product inventory data
        // ...
    }

    // ... many more methods and properties handling various functionalities ...
}

To address the code smell in the above case, split the class responsibilities.

13. Comments

  • Comments are important for any code as they better the understanding of developers about that specific code. Hence, the comments can be added wherever the requirement arises but not too many as that can disrupt the flow of the code and turn it complex.
  • The major objective of comments is to define the logic and reasoning of the various parts of the code and explain the perspective behind it. However, the developer should avoid overdoing it as too many comments will lead to noise. 

14. Conditional statement

  • The use of conditional statements is dependent on developers. Some use it heavily while some do not prefer it at all. 
  • It is suggested that developers should avoid overdoing it and put a limit to the use of conditional statements. This is because long conditional statements can transform the readability of the code negatively and also increase noise. They also make it overcrowded.

Like in the example below, a single function is using multiple if-else statements to manage different commands.

func greetUser(timeOfDay: String) {
    if timeOfDay == "Morning" {
        print("Good morning!")
    } else if timeOfDay == "Afternoon" {
        print("Good afternoon!")
    } else if timeOfDay == "Evening" {
        print("Good evening!")
    } else {
        print("Hello!")
    }
}

Hence, one can use a dictionary to solve this since that is a cleaner approach than if-else statements.

func greetUser(timeOfDay: String) {
    let greetings = [
        "Morning": "Good morning!",
        "Afternoon": "Good afternoon!",
        "Evening": "Good evening!"
    ]

    if let greeting = greetings[timeOfDay] {
        print(greeting)
    } else {
        print("Hello!")
    }
}

Also read: Guide to Incorporating Augmented Reality into Flutter Apps

15. Long parameter lists

  • If a method call contains a long parameter list, then that also comes under the category of a code smell. It majorly indicates that something is wrong with operational capacity of the code. 
  • Anything exceeding three or four parameters decreases the quality of readability and also creates issues with developing an understanding of the code. Many bugs, errors, and security threats also arise in such cases. 

In the below example “createproduct” method has a long list of parameters. This makes the function call cumbersome and hard to read.

class ProductService {
    func createProduct(productId: Int, name: String, category: String, price: Double, manufacturer: String, stockQuantity: Int, isOnSale: Bool, salePrice: Double?) {
        // Code to create a new product with the provided parameters.
        // ...
    }

}

To address this smell, we can use “struct” or “class” to encapsulate multiple parameter lists into a single entity.

struct Product {
    let productId: Int
    let name: String
    let category: String
    let price: Double
    let manufacturer: String
    let stockQuantity: Int
    let isOnSale: Bool
    let salePrice: Double?
    }

class ProductService {
    func createProduct(product: Product) {
        // Code to create a new product using the provided Product instance.
        // ...
    }

    // Other methods related to product management...
}

 16. Temporary field

  • Temporary fields are those class instance variables that have been utilized only once in the entire code and hence lead to code smell.
  • The value of temporary fields gets generated only under specific circumstances. If these circumstances don’t exist, then these fields are left empty. This makes the reading of code difficult. One expects some data in all the fields, so the empty object fields make it lengthy and heavy.

  17. Message chain

  • When a chain of messages gets formed in the way that a client requests one object, then that object requests yet another one, and this goes on, then it is known as a message chain.
  • In a code section, if there is a series of calls indicating a chain like $a->b()->c()->d().
  • Such codes are hard to maintain since they are tightly stuffed together and act as a unified unit. This could be either a series of method or property calls. Such chains decrease flexibility and increase vulnerabilities.

Here in the below example, person.address.city creates message chains of method calls.

class Person {
    var name: String
    var address: Address

    init(name: String, address: Address) {
        self.name = name
        self.address = address
    }
}

class Address {
    var city: String
    var postalCode: String

    init(city: String, postalCode: String) {
        self.city = city
        self.postalCode = postalCode
    }
}

let person = Person(name: "Colin Hoppe", address: Address(city: "New York", postalCode: "411057"))

let city = person.address.city

Now, the Person class has a getCity() method that retrieves the city from the Address object. This decouples the code from the internal structure of the Address class.

class Person {
    var name: String
    private var address: Address

    init(name: String, address: Address) {
        self.name = name
        self.address = address
    }

    func getCity() -> String {
        return address.city
    }
}

class Address {
    var city: String
    var postalCode: String

    init(city: String, postalCode: String) {
        self.city = city
        self.postalCode = postalCode
    }
}

let person = Person(name: "Colin Hoppe", address: Address(city: "New York", postalCode: "411057"))

let city = person.getCity()

 18. Oddball solution

  • The “Oddball Solution” smell is a specialized category of code smell and it arises when one piece of code is too unique and also stands out from the rest easily. The differences could be of different types like its formation or design pattern. This makes the codebase difficult to manage or maintain.

The below example showcases a calculation where the sum of an integer array is deduced using a loop and indexing. Even though the code works, it also shows an oddball solution smell and thus it cannot be used as a solution for Swift.

func calculateSum(_ numbers: [Int]) -> Int {
    var sum = 0
    for i in 0 ..< numbers.count {
        sum += numbers[i]
    }
    return sum
}

There’s a method to sum an array of integers in Swift. By using the reduce method, the calculation can be completed. This technique is a higher-order function as it will keep on repeating over the elements of the array.

func calculateSum(_ numbers: [Int]) -> Int {
    return numbers.reduce(0, +)
}

 19. Speculative generality

  • Speculative generality refers to the prospective future use of a code and has no current functionality. Since it exists without use, it leads to noise in the code and can crowd a project. There’s also a probability that it may never actually come into any use whatsoever, so it is best if it is removed. The methods and classes that have present use and are serving current requirements should be included in the code.

Below methods are generic and do not hold any specific relation to any actual data type currently being handled:   

class DataManager {
    func fetchDataFromAPI(url: String, parameters: [String: Any]) {
        // Code to fetch data from the API using the given URL and parameters
    }
   
    func saveDataToDatabase(data: [String: Any]) {
        // Code to save data to the database
    }
   
    // More generic methods...
}
struct User {
    var id: Int
    var name: String
    var email: String
    // Additional properties specific to the User entity
}

class UserDataManager {
    func fetchUsersFromAPI() -> [User] {
        // Code to fetch user data from the API
        return [] // For simplicity, returning an empty array here
    }
   
    func saveUserToDatabase(_ user: User) {
        // Code to save user data to the database
    }
   
    // Other user-specific methods...
}

// Usage
let userDataManager = UserDataManager()
let users = userDataManager.fetchUsersFromAPI()

In the above example, the Speculative Generality has been removed during refactoring. A specific UserDataManager class was used for user-related operations. Now, suppose if we have to handle a variety of data, we can create separate managers or classes for respective data types instead of having overly generic methods.

Also read: [Comparison] Android UI Development with Jetpack Compose vs XML

  20. Primitive obsession

  • Adding primitive variables to a code is like cheating on your diet. Even before we know it, the code would be stuffed with variables and become loaded as well as over-crowded. This is known as primitive obsession.
  • The developer needs to prevent this from happening and for the same, a parameter object will be helpful to clean up the primitive obsession or one can also create a whole object to initiate clean up.  

In the below example, Using primitives (Double) for such domain concepts can be a sign of Primitive Obsession, especially if we have multiple functions repeating this pattern or if the concept of a rectangle is used in various places throughout the codebase.

func calculateAreaOfRectangle(width: Double, height: Double) -> Double {
    let area = width * height
    return area
}

let width = 5.0
let height = 10.0
let area = calculateAreaOfRectangle(width: width, height: height)

Here in the below code, by using a custom struct, we address the Primitive Obsession code smell and provide a clearer representation of the rectangle domain concept throughout the codebase. If we need to add more behaviors or properties related to rectangles, we can easily do so within the struct.

struct Rectangle {
    var width: Double
    var height: Double
   
    func calculateArea() -> Double {
        return width * height
    }
}

let rectangle = Rectangle(width: 5.0, height: 10.0)
let area = rectangle.calculateArea()

  21. Data clump

  • A data clump is simple by definition- it is basically a set of arbitrary data, variables, etc. that is found in different portions of the project. These data pieces have an interesting property that either they are interrelated to one specific function or have the same feature. 
  • Whenever data clumps are detected, it is important to clean them up immediately. This can be done by relocating them to their respective object with a meaningful class name.
func processOrder(orderId: Int, customerName: String, customerEmail: String, shippingAddress: String, items: [Product]) {
    // Code to process the order
}

// Usage
let orderId = 12345
let customerName = "Stephen"
let customerEmail = "Stephen@gmail.com"
let shippingAddress = "123, Main Street"
let items = [Product(name: "Widget", price: 10.0), Product(name: "Gadget", price: 20.0)]

processOrder(orderId: orderId, customerName: customerName, customerEmail: customerEmail, shippingAddress: shippingAddress, items: items)

In the above example, the processOrder takes various parameters under consideration. These factors frequently appear together even when a command happens from multiple places. By grouping related data together using the data structure, we can address the situation of data clump.

struct Order {
    var orderId: Int
    var customerName: String
    var customerEmail: String
    var shippingAddress: String
    var items: [Product]
}

func processOrder(order: Order) {
    // Code to process the order
}

// Usage
let order = Order(orderId: 12345, customerName: "Stephen", customerEmail: "Stephen@gmail.com", shippingAddress: "123, Main Street", items: [Product(name: "Widget", price: 10.0), Product(name: "Gadget", price: 20.0)])

processOrder(order: order)

  22. Switch statement

  • Switch Statements arise as code smells whenever a statement of the program is abused, duplicated, or overused; it leads to glitches known as Switch Statements. However, this category of code smell is easier to repair than their corresponding statements. It involves a simple method of remedy with a design pattern.

When the switch happens because of an object type, a design pattern is used to frame and outline it for smoother functioning.

func getDayName(_ day: Int) -> String {
    switch day {
    case 1:
        return "Sunday"
    case 2:
        return "Monday"
    case 3:
        return "Tuesday"
    case 4:
        return "Wednesday"
    case 5:
        return "Thursday"
    case 6:
        return "Friday"
    case 7:
        return "Saturday"
    default:
        return "Invalid day"
    }
}

The above can be considered as a code smell because here we are using magic numbers. Also, this code has limited flexibility because if we need to extend a function, it would add additional complexity.

We should use enum instead to avoid this smell.

enum DayOfWeek: Int {
    case sunday = 1, monday, tuesday, wednesday, thursday, friday, saturday

    var name: String {
        switch self {
        case .sunday:
            return "Sunday"
        case .monday:
            return "Monday"
        case .tuesday:
            return "Tuesday"
        case .wednesday:
            return "Wednesday"
        case .thursday:
            return "Thursday"
        case .friday:
            return "Friday"
        case .saturday:
            return "Saturday"
        }
    }
}

func getDayName(_ day: DayOfWeek) -> String {
    return day.name
}

23. Magic numbers

  • The hard-coded numbers in the code are known as magic numbers. 
  • These can usually be seen in conditionals and are mostly understood by the developer himself and not any other person. Any other reader is bound to get confused while going through them. The number as such does not hold much context or meaning and hence decoding them becomes hard.

In the snippet below, it is not apparent why we are checking 10. What is the significance behind this number?

func calculateAreaOfCircle(radius: Double) -> Double {
    let pi = 3.14159
    return pi * radius * radius
}

let radius = 5.0
let area = calculateAreaOfCircle(radius: radius)

In the above example, the value of pi is hard coded as 3.14159. While it’s a valid value for π (pi), it is considered a magic number because its meaning is not immediately apparent to someone reading the code. Instead, it is better to use the built-in constant Double.pi or declare a named constant for it.

func calculateAreaOfCircle(radius: Double) -> Double {
    let pi = Double.pi
    return pi * radius * radius
}

let radius = 5.0
let area = calculateAreaOfCircle(radius: radius)

Also read: Creating Private CocoaPods Libraries

Refactoring code to eliminate code smells

Once the concept of code smells is well understood, it becomes easy to handle them. The removal and tackling of code smells happen through refactoring. Refactoring can be seen as a complete makeover of the code, which is complete with sweeping, decluttering, and streamlining. 

Recognizing the need for refactoring

Spotting a code smell is a major task. Once that is done, look at the indicator and understand the location of the code smell. So the initial step becomes recognition. The acknowledgment of code smells’ existence marks a major work done.

Understand the code smell

Once the code smell has been identified, another milestone to cover is recognizing its category or belonging. Maybe it is a duplication or a piece of code, which is kept for future use. Maybe there is a function that is too large. Once we know exactly what we are dealing with, it becomes easy to refactor it efficiently.

Plan your approach

Refactoring is not a process that can be completed hurriedly. It requires a methodological approach that demands thought and deliberation. Before taking any step, plan ahead – break down the problem into doable tasks, outline your goals, and find out the best way to solve the issue behind the code smell. 

Make small, incremental changes

Refactoring does not necessarily mean that your entire code is to be written again. It only means that once the issues have been identified, changes need to be made to the code. These changes are localized to the origin of the code smell and are manageable. This approach reduces the risk of bugs and also maintains full functionality throughout the process.

Utilize design patterns

Design patterns serve as tested templates for perfect code. They offer varied solutions to common problems, helping in the process of refactoring. Whether it is the Singleton pattern, Factory pattern, or Observer pattern, implementing these patterns can prove extremely advantageous in removing code smells. 

Leverage automated tests

Refactoring the first stage is always followed by testing. Anything that goes without testing cannot guarantee accuracy. Automated tests guarantee safety and lower the amount of risks. They also ensure that your changes don’t break current functionality. Hence, before beginning the refactoring activity, it is to be ensured that testing mechanisms and plans are active.

Iterate and review

Refactoring is not a one-step process. It happens in iteration. After introducing any change, review the code. Test its functionality and check for optimization. One can always take a second opinion to review the code as well and ensure its functionality. 

Document your changes

It is important that documentation of every step is done with clarity and precision. During the refactoring process, put in comments wherever necessary to showcase the changes. This leads to clear understanding as well as make it easy to collaborate. 

Measure the impact

Once refactoring is completed, evaluation must begin. It will help in gaining insights into the impact of changes made. One can understand if the code smells have been removed or not. It will also show if the code is now clean and easy to manage. Performance improvements and bug reduction can also be observed.

Guidelines for new developers

Here, we are sharing some tips that new developers can follow to avoid code smells. If one follows these tips, the code can remain clean throughout, and the chances of code smell arising will be low. 

GuidelineDescription
Clear PlanningDraft a clear plan before you begin coding. It is important to understand the problem first and then design your solutions as per the required customization. Any hasty coding should be avoided. 
Follow Naming RulesMeaningful names should be used for variables, functions, and classes. Clear names are like guidelines for readers and users.
Avoid Copy-PastingOne must avoid copying, and instead create functions that can be reused or classes to lower the risk of duplicate code.
Keep Functions BriefThe length of the functions should be kept brief and concise. Avoid unnecessary long codes. It not only adds to the weight of the code but also makes it susceptible to bugs. Long codes can be broken down into smaller fragments.
Limit ParametersTry to avoid lengthy parameter lists. Similar or related data can be merged or grouped together into structs or classes.
Regular Code ReviewsRegular reviews can detect code smells early. One can get inspired by experienced developers.
Future-Ready CodeChanges are bound to come. Hence, write flexible codes for easy and quick iterations.
Meaningful CommentsComments are helpful as they detail complex logic, assumptions, and decisions.
Test ThoroughlyAutomated tests catch bugs, and security threats and also support refactoring.
Learn from ExpertsRead and study well-written code in open-source projects. Learn to apply their techniques.

Creating clean code takes time. To master this subject, always keep learning and practicing.  

Also read: [Guide] Creating Charts with Swift Charts

Conclusion

In this blog, we have learned about the importance of code smells in detail and discovered their inevitability during the course of software development. We have also gained an understanding of the various types of code smells and their impact.

It is important to note that code smells serve as early indicators of potential problems, and all these issues can be easily solved through refactoring. If we address them quickly and also utilize best practices, then we can make our code strong, bug-free, and adaptable over time.
At Aubergine Solutions, we understand and reaffirm the importance of clean code and efficient solutions. Our skilled developers create top-notch software that is designed to meet your current requirements and remain effective over time. Connect with us today to find out more about our software development services and how we can collaborate with you on your next project.

author
Nikita Gondaliya
I am a iOS Developer having expertise in developing applications for mobile devices powered by Apple’s iOS operating system. I have a strong understanding of the Swift and SwiftUI framework and I am able to use its various features and build scalable and maintainable apps.