Eradicating the Primitive Obsession
max-arshinov
Posted on September 14, 2023
Before we delve deeper into the intricacies of domain-driven design, I highly recommend taking a moment to familiarize yourself with the essential terms and concepts that will be discussed throughout this article.
Recalling our previous discussion where we initiated the design of a user class and identified issues that were preventing us from implementing the Rich Domain Model, let’s take it a step further by rectifying the anemic implementation that we previously derived. Here is what the anemic implementation might look like:
public class User
{
public int Id { get; init; }
public string? FirstName { get; set; } // Profile
public string? LastName { get; set; } // Profile
public string? MiddleName { get; set; } // Profile
public string? Phone { get; set; } // Contact
public string? Email { get; set; } // Contact
}
This is a perfect example of a common anti-pattern, referred to as "primitive obsession," where developers lean heavily on primitive data types instead of crafting dedicated classes or structures to house domain ideas. This is often considered as a significant code smell.
One can claim that this
User
hasn't decided if he/she is a unicorn or a goat :) There is no invariant here because the class is no more than a bag of properties.
To resolve this, we introduce a technique called “Make illegal states unrepresentable” to help eradicate this obsession and reinforce the robustness of our design through the integration of new UserProfile
and UserContact
classes.
public class UserProfile
{
// now required
public required string FirstName { get; init; }
// now required
public required string LastName { get; init; }
public string? MiddleName { get; init; }
}
public class UserContact
{
public string? Phone { get; init; }
public string? Email { get; init; }
}
Thanks to C#9's init keyword coupled with C#11 required modifier, enforcing the setting of the first and last names is possible, even without a constructor. Here is how I can initialize a profile:
var profile = new UserProfile
{
FirstName = "Max",
LastName = "Arshinov"
};
Adding Constructor
Despite these improvements, a loophole exists - it's still possible to initialize the profile with empty strings. To fix this, we introduce a constructor that validates these fields to prevent such initialization:
public class UserProfile
{
public UserProfile(string firstName, string lastName,
string? middleName = null)
{
FirstName = firstName;
LastName = lastName;
MiddleName = middleName;
this.EnsureInvariant();
}
[Required]
public string FirstName { get; protected set; }
[Required]
public string LastName { get; protected set; }
public string? MiddleName { get; protected set; }
}
Now, our constructor ensures that first and last names are provided, making our class more robust against erroneous inputs.
Ensure Invariant
To further beef up our class, we introduce a method, EnsureInvariant
, which helps in validating the object state after its initialization. Here is the implementation of this method.
public static class FunctionalExtensions
{
private static ConcurrentDictionary<Type, IValidator>
_validators = new();
public static void EnsureInvariant(this object obj,
bool validateAllProperties = true)
{
Validator.ValidateObject(
obj,
new ValidationContext(obj),
validateAllProperties);
}
}
Or slightly different if you prefer FluentValidaton.
public static void EnsureInvariant<TEntity, TValidator>(
this TEntity obj)
where TValidator: AbstractValidator<TEntity>, new()
{
var ctx = new ValidationContext<TEntity>(obj,
new PropertyChain(),
ValidatorOptions
.Global
.ValidatorSelectors
.DefaultValidatorSelectorFactory());
var result = _validators.GetOrAdd(typeof(TEntity),
_ => new TValidator()).Validate(ctx);
if(!result.IsValid)
{
throw new ValidationException(
"Validation failed: " + string.Join(",
result.Errors.Select(e => e.ErrorMessage)));
}
}
//…
this.EnsureInvariant<UserProfile, UserProfileInvariantValidator>();
The above skeleton can be fleshed out with actual validation logic to ensure the object's state's correctness.
User Contact
Turning our attention to the contact information, we encounter a similar predicament. We aim to define parameters that are not merely optional but follow certain validation rules. Here is an evolved UserContact
class adhering to this principle:
public class UserContact
{
const string PhonePattern = @"\+?\d";
[EmailAddress]
public string? Email { get; protected set; }
[RegularExpression(PhonePattern)]
public string? Phone { get; protected set; }
public UserContact(string? email, string? phone)
{
Email = email;
Phone = phone;
this.EnsureInvariant();
}
}
I could use such a constructor, but in C# there is no way to indicate that one of the parameters is mandatory. The method signature will indicate that both parameters are optional. Therefore, let's make the constructor private and instead provide two public methods with more descriptive names. In C#, the prefix Try
is usually used for operations that can fail but do not throw exceptions. We can implement the constructor in the following way.
public class UserContact
{
const string PhonePattern = @"\+?\d";
[EmailAddress]
public string? Email { get; protected set; }
[RegularExpression(PhonePattern)]
public string? Phone { get; protected set; }
private Contact(string? email, string? phone)
{
Email = email;
Phone = phone;
}
public static bool TryParsePhone(string phone,
out Contact? c)
{
if (PhoneRegex.IsMatch(phone))
{
c = new Contact(null, phone);
return true;
}
c = null;
return false;
}
}
Wrapping up
We now have a User
type that embodies two subtypes: UserContact
and UserProfile
. With these changes, our User
class has transitioned from being anemic to a more rich and robust representation, promoting easier maintenance and fewer errors during development.
public class User
{
public required int Id { get; init; }
public required UserContact Contact { get; init; }
public UserProfile? Profile { get; init; }
}
That’s all for today. The next time, we’ll evolve this design even more towards the Rich Domain Model.
Posted on September 14, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.