• Shift Elevate
  • Posts
  • Primitive Obsession: Replace Primitive with Object Refactoring | Clean Code

Primitive Obsession: Replace Primitive with Object Refactoring | Clean Code

Primitive Obsession is a code smell where primitive types are used instead of small objects to represent domain concepts.

We will see how we can refactor using the Replace Primitive with Object technique for better domain modeling and type safety.

⚠️ Code Smell: Primitive Obsession
Refactoring: Replace Primitive with Object
🎯 Goal: Rich domain models with meaningful types

The Primitive Obsession code smell occurs when primitive types (strings, numbers, booleans) are used to represent domain concepts instead of creating dedicated objects. The Replace Primitive with Object refactoring technique replaces these primitives with meaningful objects that better represent the domain and provide type safety.

The Code Smell: Primitive Obsession

Primitive Obsession is a subtle but important code smell that reduces the expressiveness of your domain model. When you use primitive types to represent domain concepts, you lose the opportunity to encapsulate related behaviour and make your code more self-documenting. This leads to scattered validation logic and makes the code harder to understand and maintain.

Symptoms

Impact

Primitive types used for domain concepts

Reduced expressiveness

Scattered validation logic

Poor domain modeling

Type confusion and errors

Higher bug risk

Here's a typical example of Primitive Obsession:

public class User {
    private String email;
    private String phoneNumber;
    private String zipCode;
    private double salary;
    private String status;

    public User(String email, String phoneNumber, String zipCode,
                double salary, String status) {
        this.email = email;
        this.phoneNumber = phoneNumber;
        this.zipCode = zipCode;
        this.salary = salary;
        this.status = status;
    }

    public boolean isValidEmail() {
        return email != null && email.contains("@") && email.contains(".");
    }

    public boolean isValidPhoneNumber() {
        return phoneNumber != null && phoneNumber.matches("\\+?[1-9]\\d{1,14}");
    }

    public boolean isValidZipCode() {
        return zipCode != null && zipCode.matches("\\d{5}(-\\d{4})?");
    }

    public boolean isActive() {
        return "ACTIVE".equals(status);
    }
}

In this example, the User class uses primitive types like String for email, phone number, zip code, and status, and double for salary. This creates several problems: validation logic is scattered, there's no type safety, and the domain concepts are not clearly expressed.

The Refactoring: Replace Primitive with Object

The Replace Primitive with Object refactoring technique creates dedicated objects to represent domain concepts, encapsulating related behaviour and providing type safety. This makes the code more expressive and easier to maintain.

Step by Step Refactoring Process:

  1. Identify primitive types that represent domain concepts.

  2. Create dedicated classes for each domain concept.

  3. Move validation logic into the new classes.

  4. Replace primitive fields with the new objects.

  5. Update methods to work with the new objects.

Here's the refactored version:

// User class now uses meaningful objects
public class User {
    private Email email;
    private PhoneNumber phoneNumber;
    private ZipCode zipCode;
    private Money salary;
    private UserStatus status;

    public User(Email email, PhoneNumber phoneNumber, ZipCode zipCode,
                Money salary, UserStatus status) {
        this.email = email;
        this.phoneNumber = phoneNumber;
        this.zipCode = zipCode;
        this.salary = salary;
        this.status = status;
    }

    public boolean isActive() {
        return status.isActive();
    }
}

// Dedicated class for email with validation
public class Email {
    private final String value;

    public Email(String email) {
        if (!isValid(email)) {
            throw new IllegalArgumentException("Invalid email format: " + email);
        }
        this.value = email;
    }

    private boolean isValid(String email) {
        return email != null && email.contains("@") && email.contains(".");
    }

    public String getValue() {
        return value;
    }

    public String getDomain() {
        return value.substring(value.indexOf("@") + 1);
    }
}

// Dedicated class for phone numbers
public class PhoneNumber {
    private final String value;

    public PhoneNumber(String phoneNumber) {
        if (!isValid(phoneNumber)) {
            throw new IllegalArgumentException("Invalid phone number format: " + phoneNumber);
        }
        this.value = phoneNumber;
    }

    private boolean isValid(String phoneNumber) {
        return phoneNumber != null && phoneNumber.matches("\\+?[1-9]\\d{1,14}");
    }

    public String getValue() {
        return value;
    }

    public String getCountryCode() {
        if (value.startsWith("+")) {
            return value.substring(1, 3);
        }
        return "US"; // Default assumption
    }
}

// Dedicated class for zip codes
public class ZipCode {
    private final String value;

    public ZipCode(String zipCode) {
        if (!isValid(zipCode)) {
            throw new IllegalArgumentException("Invalid zip code format: " + zipCode);
        }
        this.value = zipCode;
    }

    private boolean isValid(String zipCode) {
        return zipCode != null && zipCode.matches("\\d{5}(-\\d{4})?");
    }

    public String getValue() {
        return value;
    }
}

// Dedicated class for money
public class Money {
    private final double amount;

    public Money(double amount) {
        if (amount < 0) {
            throw new IllegalArgumentException("Amount cannot be negative");
        }
        this.amount = amount;
    }

    public double getAmount() {
        return amount;
    }

    public String getFormattedAmount() {
        return String.format("$%.2f", amount);
    }
}

// Enum for user status
public enum UserStatus {
    ACTIVE, INACTIVE, SUSPENDED, PENDING;
    
    public boolean isActive() {
        return this == ACTIVE;
    }
}

Benefits of Replace Primitive with Object

Benefit

Description

Rich Domain Model

Objects represent domain concepts clearly, making the code more expressive and self-documenting.

Type Safety

Compile-time type checking prevents errors and makes the code more robust.

Encapsulated behaviour

Related validation and business logic is encapsulated within the appropriate objects.

When to Apply Replace Primitive with Object Refactoring

  • Primitive types used to represent domain concepts (email, phone, money, etc.).

  • Scattered validation logic for the same primitive type.

  • Multiple parameters of the same primitive type that could be confused.

  • Business logic that operates on primitive values.

  • When you want to make your domain model more expressive and type-safe.

Repository & Resources

Complete Code Examples: Clean Code Repository

Find the complete implementation of Primitive Obsession refactoring and other clean code techniques in our dedicated repository. Each example includes:

  • Before and after code comparisons

  • Unit tests demonstrating the improvements

Found this helpful? Share it with a colleague who's struggling with Primitive Obsession. Got questions? We'd love to hear from you at [email protected]