We are often able to solve complex problems with simple solutions. Avoiding the imperfection known as primitive obsession allows us to create high-quality, less error-prone code without much effort. Check out in this blog post how to identify primitive obsession and how to avoid it through practical examples.
Primitive obsession is one of the most common imperfections found in most systems. Many developers, even experienced ones, cannot detect this problem—which ends up resulting in poorly written code, subject to several failures.
In this article, we will see an explanation of primitive obsession, how to identify it and how to avoid it and thus create better code using good practices.
According to C2 Wiki documentation, primitive obsession is a code smell (any characteristic in the source code of a program that possibly indicates a more profound problem) where primitive data types are used to represent domain ideas. This would be a case where a string is used to represent a message or an integer is used to represent an amount of money.
As with other code smells, primitive obsession is generated when there is no concern about possible future problems.
As a developer, you probably have needed to create a model class to store some data. However, if that class only contained fields with primitive types, like strings for example, without methods or subclasses, surely primitive obsession was present in the code because that forced the service class to contain methods and business rules that could have been created in the model class and thus keep the service class cleaner and more organized.
To demonstrate an example of primitive obsession, let’s create a simple application just to display some data. To keep the focus on the purpose of the post, let’s keep it as simple as possible, without a database, etc.
You can access the complete source code of the project at this link: Source Code.
So let’s start by creating a console app. This example uses .NET 6 (but .NET 7 is now available!).
In Visual Studio:
Via terminal:
dotnet new console --framework net6.0 -n BadExample
Then create a folder called “Models” and inside it create the class below:
Customer
namespace BadExample.Models;
public class Customer
{
public Customer(string? name, string? email, string? phoneNumber, bool clubMember)
{
Name = name;
Email = email;
PhoneNumber = phoneNumber;
ClubMember = clubMember;
}
public string? Name { get; set; }
public string? Email { get; set; }
public string? PhoneNumber { get; set; }
public bool ClubMember { get; set; }
}
💡 Note that in this model class, we are only creating fields to store values—there is no subclass or method. This demonstrates an evident code smell of primitive obsession—after all, we are only using primitive types here, like text fields and booleans.
Now let’s create the service class which will use the client entity. So, create a new folder called “Services” and inside it create the class below:
CustomerService
using BadExample.Models;
namespace BadExample.Services;
public class CustomerService
{
public static void ServiceProcess(Customer customer, decimal productValue)
{
decimal amount = 0;
if (customer.ClubMember == true)
{
amount = productValue - 10;
}
else
{
amount = productValue;
}
if (customer.PhoneNumber.Any(p => char.IsLetter(p)))
{
throw new Exception("Phone numbers must contain numbers only");
}
string message = @$"Name: {customer.Name} - Email: {customer.Email} - Amount: {amount}";
Console.WriteLine(message);
}
}
💡 Notice the code we just implemented. In the “ServiceProcess” method, we are implementing a logic commonly found in the vast majority of systems that have a primitive obsession. This happens because the model class has only fields to store data, so the service class is bound to contain “if” and “else” logic.
Of course, conditional operations like “if” and “else” were created to be used, but we should avoid their use as much as possible, as they pollute the code and make it difficult to read and interpret when making any changes or even when debugging it.
Avoiding primitive obsession allows many problems found in systems today to be avoided. Among the most common are:
As a result, we get professional-grade code, where we manage to keep many business rules “tied” to the model class, where they are explicit and easily accessible for any developer who needs to perform some maintenance on the code, while the service class is reserved only for non-model-dependent methods, making it easier to read and keeping the code clean and cohesive.
To avoid primitive obsession, we can follow the principles described in Martin Fowler’s book “Refactoring: Improving the Design of Existing Code,” where he advises replacing the data value with an object. In this context, “object” refers to a new class containing all the details that make up its value.
So, now let’s create a new project, but this time using a good example, without the presence of primitive obsession.
In Visual Studio:
Via terminal:
dotnet new console --framework net6.0 -n GoodExample
Then create a folder called “Models” and inside it create the classes below:
namespace GoodExample.Models;
public class ClubMember
{
public ClubMember(bool isClubMember)
{
IsClubMember = isClubMember;
}
public bool IsClubMember { get; set; }
public decimal CalculateAmount(decimal productValue) =>
IsClubMember ? productValue - 10 : productValue;
}
namespace GoodExample.Models;
public class Customer
{
public string? Name { get; set; }
public string? Email { get; set; }
public PhoneNumber PhoneNumber { get; set; }
public ClubMember ClubMember { get; set; }
public Customer(string? name, string? email, PhoneNumber phoneNumber, ClubMember clubMember)
{
Name = name;
Email = email;
PhoneNumber = phoneNumber;
ClubMember = clubMember;
}
}
namespace GoodExample.Models;
public class PhoneNumber
{
public PhoneNumber(string phoneNumber)
{
Value = phoneNumber.Any(p => char.IsLetter(p)) ? throw new Exception("Phone numbers must contain numbers only") : phoneNumber;
}
public string Value { get; set; }
}
💡Note that in this new version of the “Client” class, the “ClubMember” field, which was previously of boolean type, has now become a subclass. In it, we added the “CalculateAmount” method, which receives a value, and, if the “IsClubMember” property “is true,” the product value is subtracted from the discount value (10). In the same way, the “PhoneNumber” field is now a subclass, and in it there is already a method to validate if the value contains any letters, thus avoiding the primitive obsession when linking the discount calculation and character validation to the client model classes.
The next step is to create the service class that will use the model classes and their methods.
So, create a new folder called “Services” and inside it create the class below:
CustomerService
using GoodExample.Models;
namespace GoodExample.Services;
public class CustomerService
{
public static void ServiceProcess(Customer customer, decimal productValue)
{
decimal amount = customer.ClubMember.CalculateAmount(productValue);
string message = @$"Name: {customer.Name} - Email: {customer.Email} - Amount: {amount}";
Console.WriteLine(message);
}
}
💡Note that now the service class doesn’t have any unnecessary logic. After all, the “CalculateAmount” method of the model class is used to check the value, and character checking is coupled to the model class, so the code is clean and easy to understand.
It is also worth noting that we are fulfilling one of the principles of SOLID (Single-responsibility) as the responsibility for calculating the total was assigned to the “CalculateAmount” method, which was previously not assigned to any specific method but was spread across the service class.
By creating model classes composed of methods and subclasses linked to the entity, we are leaving the service classes free to contain only what makes sense, such as specific business rules that have no direct relationship with the domain.
This is the perfect way to avoid primitive obsession and create clean, maintainable code. Therefore, always consider adding methods and subclasses to your model classes and keeping the service class as simple as possible.