I use domain-driven design, and one of the core patterns in DDD is the entity. I won’t go into a description of aggregates or aggregate roots, but the entity is a central pattern when implementing domain-driven design.
I often encounter the desire by some developers to create an entity that guards itself against ever becoming invalid
Let’s consider the following example of a UserProfile class:
public class UserProfile
{
public string Name { get; set; }
public Gender? Gender { get; set; }
public DateTime? JoinedDate { get; set; }
public DateTime? LastLogin { get; set; }
}
private string _name;
private Gender? _gender;
public DateTime? JoinedDate { get; set; }
public DateTime? LastLogin { get; set; }
public string Name
{
get { return _name; }
set
{
if (string.IsNullOrEmpty(value))
{
throw new Exception("Name is required");
}
_name = value;
}
}
public Gender? Gender
{
get { return _gender; }
set
{
if (_gender == null)
{
throw new Exception("Gender is required");
}
_gender = value;
}
}
This will ensure that the properties cannot be set to null at any time. In fact, some UI frameworks will catch these exceptions and take the message and turn it into an user’s error message. Required property validation and the accompanying error messages are misplaced here inside the entity.
- The fact that name is required needs to be context-bound. When is it invalid?
- The message should be the responsibility of the presentation layer.
These are simple things and don’t illustrate my point very strongly. In real systems, there is complex logic that decides when an entity is valid for particular operations. There is seldom only one context for being valid or invalid. For instance, when loading historical data, some genders may be missing. Should the application blow up when loading data? What page would get the error message? When loading historical data, perhaps the user needs to enter a gender when he edits his profile the next time. The answer is certainly not to fail the query operation.
The dates in the UserProfile class present the opportunity for some more complex business rules. For instance, should the JoinedDate ever be greater than the LastLogin date? Probably not. If this rule is applied, which setter should contain the validation? Neither. Even with this simple validation rule, the always-valid entity notion already falls down.
This type of scenario runs rampant in any non-trivial business application. This type of validation makes up much of the business logic used to consume and validate user input into the system. This business logic needs to be separated out into other classes. Let’s consider what it might look like to factor this out.
public interface IValidator<T>
{
ValidationResult Validation(T obj);
}
public class ValidationResult
{
List<string> _errorCodes = new List<string>();
public void AddError(string errorCode)
{
_errorCodes.Add(errorCode);
}
public string[] GetErrors()
{
return _errorCodes.ToArray();
}
public bool IsValid()
{
return _errorCodes.Count > 0;
}
}
First we’ve defined an interface to represent the concept of a validator. It returns a result that contains zero or more error codes. It’s the job of the UI to print the message that goes with the particular error. Below are two rules that implement this interface.
public class NameRequiredRule : IValidator<UserProfile>
{
public ValidationResult Validation(UserProfile obj)
{
var validation = new ValidationResult();
if(string.IsNullOrEmpty(obj.Name))
{
validation.AddError("NAME_REQUIRED");
}
return validation;
}
}
public class LastLoginAfterJoinedDateRule : IValidator<UserProfile>
{
public ValidationResult Validation(UserProfile obj)
{
var validation = new ValidationResult();
if (obj.JoinedDate.GetValueOrDefault() > obj.LastLogin.GetValueOrDefault())
{
validation.AddError("JOINED_DATE_AFTER_LAST_LOGIN");
}
return validation;
}
}
These two rules encapsulate the logic, and further refactoring could have them apply to multiple types with similar rules. This topic can become quite large, but the lesson to take home is that these business rules (validation rules) should be external to the entity. Some of the logic might become methods on the entity, with a boolean return type, but in a medium to large application, there will be so many business rules that factoring them into independent classes becomes a maintainability necessity.
It is futile to attempt to keep entities always-valid. Let bad things happen to them, and then validate.