DEV Community

Cover image for Eradicating the Primitive Obsession
max-arshinov
max-arshinov

Posted on

Eradicating the Primitive Obsession

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  
}
Enter fullscreen mode Exit fullscreen mode

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; }
}
Enter fullscreen mode Exit fullscreen mode

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"
};
Enter fullscreen mode Exit fullscreen mode

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; }
}

Enter fullscreen mode Exit fullscreen mode

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);
   }
}
Enter fullscreen mode Exit fullscreen mode

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>();
Enter fullscreen mode Exit fullscreen mode

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();
   }
}

Enter fullscreen mode Exit fullscreen mode

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;
   }
}
Enter fullscreen mode Exit fullscreen mode

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; }
}
Enter fullscreen mode Exit fullscreen mode

That’s all for today. The next time, we’ll evolve this design even more towards the Rich Domain Model.

Top comments (0)