Vogen Help

Primitive Obsession

Primitive Obsession (AKA StringlyTyped) means being obsessed with primitives. It is a Code Smell that degrades the quality of software.

There's a blog post here that describes it more fully; what follows is a summary.

Primitive Obsession is this:

int customerId = 42

What's wrong with that?

An int likely cannot fully represent a customer ID. An int can be negative or zero, but it's unlikely a customer ID can be. So, we have constraints on a customer ID. We can't represent or enforce those constraints on an int.

So, we need some validation to ensure the constraints of a customer ID are met. Because it's in int, we can't be sure if it's been checked beforehand, so we need to check it every time we use it. Because it's a primitive, someone might've changed the value, so even if we're 100% sure we've checked it before, it still might need checking again.

So far, we've used as an example, a customer ID of value 42. In C#, it may come as no surprise that this is true: " 42 == 42 " (I haven't checked that in JavaScript!). But in our domain, should 42 always equal 42? Probably not if you're comparing a Supplier ID of 42 to a Customer ID of 42! But primitives won't help you here (remember, 42 == 42!):

(42 == 42) // true (SuppliedId.From(42) == SupplierId.From(42)) // true (SuppliedId.From(42) == VendorId.From(42)) // compilation error

But sometimes, we need to denote that a Value Object isn't valid or has not been set. We don't want anyone outside of the object doing this as it could be used accidentally. It's common to have Unspecified instances, e.g.

public class Person { public Age Age { get; } = Age.Unspecified; }

We can do that with Instances, which is covered more in this tutorial. Instances allows new to be used in the value object itself, which bypasses validation and normalization. More information on validation can be found in this tutorial.

[ValueObject] public readonly partial struct Age { public static readonly Age Unspecified = new(-1); public static Validation Validate(int value) => value > 0 ? Validation.Ok : Validation.Invalid("Must be greater than zero."); }

The constructor is private, so only this type can (deliberately) create invalid instances.

Now, when we use Age, our validation becomes clearer:

public void Process(Person person) { if(person.Age == Age.Unspecified) { // age not specified. } }

Primitive Obsession can also help introduce bugs into your software. Take, for instance, the following method:

public void IncreaseQuantity( int customerId, int supplierId, int quantity)

… and a caller calls it like this:

_service.IncreaseQuantity(_supplierId, _customerId, _quantity)

We've messed up the order of the parameters, but our compiler won't tell us. The best we can hope for is a failing unit test. However, given the contrived data often used in unit tests, it could be that the data will hide the problem by using the same ID for customer and supplier.

With Value Objects representing SupplierId, CustomerId, and Quantity, the compiler can tell us if we mess up the order. These types make the domain code more understandable (more domain language and less C# types), and validation is as close to the data as possible; in this example, it likely means that all these types cannot be zero or negative.

Last modified: 22 November 2024