Primitive Obsession: The Hidden Enemy of Clean Code

Primitive obsession is a code smell where primitives are overused for domain concepts, leading to poor encapsulation and maintainability issues.

Primitive Obsession: The Hidden Enemy of Clean Code

It shows a medieval village scene with villagers using primitive tools and a craftsman demonstrating proper tools and methods.

Image credits: Image generated by DALL-E.

Before we go any further, let’s know what a primitive data type is. In computer science, a primitive data type is a built-in basic data type provided by a programming language as a basic building block. Examples of the basic primitive types are characterinteger, floatstringboolean, etc. Furthermore, many programming languages provide a set of composite data types.

What is primitive obsession?

Primitive obsession is one of the code smells that occurs when primitive data types (e.g., integer, string, boolean) are used excessively to represent domain concepts instead of creating dedicated classes or types.

Code smell!

Figure 1: An imaginary oil painting depicts the concept of code smell in a vintage office setting. The characters react to the computer’s bad smell, adding a fun touch to this traditional scene by DALL-E.

It's a common but often overlooked issue in the codebase. This coding pitfall emerges when these primitive types are used over and over to represent complex ideas, bypassing the rich expressiveness offered by dedicated classes or custom types.

Primitive types are considered simple to use because they are straightforward and familiar to most programmers. While simple to use, this approach can blur the code's clarity and purpose, making it tougher to manage and improve over time. 

Excessive usage of primitive types can lead to several issues, such as:

  • Lack of encapsulation
  • Increased complexity
  • Difficulty in maintaining and extending the code

This type of code smell is easy to spot. The term was first coined by Kent Beck. Primitive types often break encapsulation by allowing invalid values to be assigned. For instance, strings and integers do not sufficiently encapsulate concepts or business rules.

Symptoms of primitive obsession

  1. Using primitive types for domain-specific concepts:
    • Example: Using a String to represent an email address, phone number, or user ID.
  2. Lack of validation and business logic encapsulation:
    • Example: Email validation logic scattered across multiple places instead of being encapsulated in an Email class.
  3. Overuse of string constants:
    • Example: Using string literals for configuration keys, status codes, etc., instead of enums or constants.
  4. Complex method signatures:
    • Example: Methods with long lists of primitive parameters instead of passing objects that encapsulate related data.
  5. Duplication of logic:
    • Example: Repeating validation logic for a specific data type in multiple locations.

Consequences of primitive obsession

  • Lack of readability: It becomes harder to understand what a primitive value represents without additional context.
  • Reduced maintainability: Changes to validation or business rules need to be applied in multiple places.
  • Increased risk of errors: Using primitives makes it easier to introduce bugs, as there is no type safety enforcing domain rules.
  • Difficulty in extending functionality: Adding new features or modifying existing ones can become cumbersome due to scattered logic.

Let us explore some common issues that we often run into when working with primitive types.

For example, let’s take zip codes and phone numbers:

  • If we represent a zip code as a string, one can possibly assign values like null, empty, some string, etc.
  • Similarly, when representing a phone number as an integer, one can possibly assign values like 98, 0, and so on. 

To prevent assigning invalid data to zip codes and phone numbers, we implement business validation. However, these validations often end up compounding the problem.

The business rules (zip code format, etc.) we want to apply to these primitive types are typically captured somewhere else, as shown below, and generally duplicated throughout the application.

if (string.IsNullOrWhiteSpace(userName)) 
    return this.BadRequest("Invalid user name.")

Let's take other example with solution:

Example of primitive obsession:

public class Order {
    private String productId;
    private int quantity;
    private String customerId;
    
    // constructor, getters, setters, etc.
}

Refactored solution:

public class ProductId {
    private final String id;

    public ProductId(String id) {
        if (id == null || id.isEmpty()) {
            throw new IllegalArgumentException("Product ID cannot be null or empty");
        }
        this.id = id;
    }

    // equals, hashCode, toString, etc.
}

public class Quantity {
    private final int value;

    public Quantity(int value) {
        if (value <= 0) {
            throw new IllegalArgumentException("Quantity must be greater than zero");
        }
        this.value = value;
    }

    // equals, hashCode, toString, etc.
}

public class CustomerId {
    private final String id;

    public CustomerId(String id) {
        if (id == null || id.isEmpty()) {
            throw new IllegalArgumentException("Customer ID cannot be null or empty");
        }
        this.id = id;
    }

    // equals, hashCode, toString, etc.
}

public class Order {
    private ProductId productId;
    private Quantity quantity;
    private CustomerId customerId;
    
    // constructor, getters, setters, etc.
}

Best practices to avoid primitive obsession

  1. Use value objects: Create classes that encapsulate the primitives and the logic associated with them.
  2. Use enums: Replace string or integer constants with enums for fixed sets of values.
  3. Encapsulate collections: Create classes to encapsulate collections and their operations rather than passing raw lists or maps.
  4. Refactor regularly: Identify and refactor primitives that represent domain concepts to dedicated classes.

By addressing primitive obsession, we can make our code more robust, readable, and maintainable, leading to better overall software design.