5 years ago by James Hickey
Ever struggle with where to place your app's business rules and validation?
Should you put them in your MVC models?
Should you create special classes for validation?
I'll show you a battle-tested technique used in Domain Driven Design!
Domain Driven Design is a holistic approach and framework for designing software. It places a heavy emphasis on exploring business problems by starting with the business problem, how people involved currently deal with the problem, figuring out who the experts in this field are, consulting with the experts, exploring potential ways of modelling the problem, etc.
DDD is a huge subject. For today, we'll be looking at a technique that comes from DDD.
Let's look at two DDD concepts to lay the foundation.
An entity is simply a model that is identified by some unique identifier. Maybe a Customer, Order, Insurance Policy, etc.
When testing whether two entities are the same or equal, we test the unique ids of each to see if they match.
Value objects correspond to things that aren't uniquely identifiable. Things like an address, a person's name, or a date range could be value objects.
They can hold multiple pieces of data like, as mentioned, a date range (which has a start and end date).
When testing whether two value objects are the same or equal, we test to see if all values are the same.
Entities and value objects usually go hand-in-hand.
An entity is usually comprised of (a) other entities and (b) value objects.
For example, an Order entity might hold a reference to a few OrderItem entities. But it might also hold a reference to the price of the purchase (value object).
In DDD, it's a good practice for business rules and validation to go into the value objects.
There are two basic properties that make this a great technique - immutability and immediate validation at creation time.
By testing whether a value object is valid at creation time we ensure that it's not possible to have a value object that is in an invalid state.
Making value objects immutable means that once they are created they cannot be modified.
Both of these properties ensure that you cannot have an object that is in an invalid state - either by creating it with invalid data or by causing side-effects that change the data.
It's quite simple yet powerful to know that you will always have instances of valid objects.
Let's look at a real-world example of how you might use value objects to perform business validation.
Imagine we had requirements to validate an insurance policy. This policy has:
Let's imagine the following rules exist:
There's an entire area of study around modelling entities and value objects. At the end of the day, the general rule is to keep data that changes together in the same place.
This is actually a fundamental Object Oriented principle. One we often choose to ignore...
So, we need to figure out, based on business requirements, what pieces of data change together:
We can see from tracking what data changes together what our models might look like:
public class Object1
{
public int Age { get; private set; }
public Gender Gender { get; private set; }
public decimal Price { get; private set; }
public Object1(int age, Gender gender)
{
this.Age = age;
this.Gender = gender;
this.Price = 15.00m; // 1st business requirement: Base price is $15/month
}
}
public class Object2
{
public Address Address { get; private set; }
public DateTime DateStarted { get; private set; }
public Object2(Address address, DateTime dateStarted)
{
this.Address = address;
this.DateStarted = dateStarted;
}
}
When creating objects using this technique, is usually recommended to not name your value objects and entities right away.
This ensures you focus on the data and where things belong based on the business. It's all too tempting to label our objects in our own minds - which brings in a bias about what "should" belong in each model.
But we shouldn't decide that - the business requirements should. We can give them meaningful names once we are sure our models are as good as we can make them.
Let's add our first 3 rules.
We'll add this validation in our object's constructor. This satisfies the property of value objects needing to be valid at creation time. You should never be able to create an object that holds invalid data.
public class Object1
{
public int Age { get; private set; }
public Gender Gender { get; private set; }
public decimal Price { get; private set; }
public Object1(int age, Gender gender)
{
// Rule #1: Base price is $15/month.
decimal basePrice = 15.00m;
// Rule #2: When age is above 50 then price is doubled.
if(age >= 50)
{
basePrice = basePrice * 2;
}
// Rule #3: When gender is Male then price is doubled.
if(gender == Gender.Male)
{
basePrice = basePrice * 2;
}
this.Age = age;
this.Gender = gender;
this.Price = basePrice;
}
}
There's a technique we'll use to ensure that our value object can never be in an invalid state:
For this, we'll create a new Exception type:
public class BusinessRuleException : Exception
{
public BusinessRuleException(string message) : base(message) { }
}
We'll use it to add our next business rule:
public class Object2
{
public Address Address { get; private set; }
public DateTime DateStarted { get; private set; }
public Object2(Address address, DateTime dateStarted)
{
// Rule #4: If address is in Canada then Date Started must be the beginning of the current month.
if(address.IsCanadian && dateStarted.Day != 1)
{
throw new BusinessRuleException("Canadian policies must begin on the first day of the current month.");
}
this.Address = address;
this.DateStarted = dateStarted;
}
}
One might point out that we could just change the Date Started to the beginning of the month automatically.
But, this is part of the requirement. Do we transform the date for the user or not? In this case, our business simply wants to inform the user of this business rule.
Now, further up the caller would catch the exception and display the error message to the user.
Glad you asked!
As a total aside - one way would be to replace the constructor with a static factory method. You might return a tuple (in C#) as the result:
public class Object2
{
public Address Address { get; private set; }
public DateTime DateStarted { get; private set; }
// Make sure no one can create this object with a constructor.
private Object2() { }
// Static Factory Method that returns (a) the value object and (b) any errors.
public static (Object2, IEnumerable<string>) Make(Address address, DateTime dateStarted)
{
var errors = new List<string>();
// Rule #4: If address is in Canada then Date Started must be the beginning of the current month.
if(address.IsCanadian && dateStarted.Day != 1)
{
errors.Add("Canadian policies must begin on the first day of the current month.");
}
// Imagine that there might be more business rules here.
if(errors.Any()){
return (null, errors); // Never return an invalid instance of the value object ;)
}
return (
new Object2
{
Address = address,
DateStarted = dateStarted
},
errors
);
}
}
There are other patterns for doing this, such as the Result Object pattern.
Let's now name our value objects and create an entity for our overall insurance policy.
Note: My names are not the best. It actually takes a long time and thought to give these meaningful names. If your business is taking DDD to it's fullest, then looking at an ubiquitous language upfront will always inform what names you choose to use, and potentially how you choose the model your objects.
public class InsurancePolicy
{
public PolicyPricingDetails Pricing { get; private set; }
public PolicyAddressDetails AddressDetails { get; private set; }
public InsurancePolicy(PolicyPricingDetails pricingDetails, PolicyAddressDetails addressDetails)
{
this.Pricing = pricingDetails;
this.AddressDetails = addressDetails;
}
}
Here's how we would supply user input and create our entity:
try {
var pricing = new PolicyPricingDetails(age, gender);
var address = new PolicyAddressDetails(userAddress, dateStarted);
// You can never get here unless all business rules are valid.
var entity = new InsurancePolicy(pricing, address);
// Now you might save the entity in a DB?
}
catch(BusinessRuleException ex)
{
errorMessage = ex.Message; // This would be returned to the UI, etc.
}
You might notice that testing your value objects can make testing specific business rules very easy!
I hope this was a helpful introduction to how you might want to incorporate this Domain Driven Design concept in your code.
Using value objects this way can give you a framework to use for:
If you have any thoughts or comments you can tweet me @jamesmh_dev or connect on LinkedIn!
Hello, I'm Corstiaan. I'm a software developer from the Netherlands and I manage BuiltWithDot.Net. I created this site as a place where developers working with .net technology can showcase their projects and inspire other developers.
There's so much you can build with .net these days that I thought it would be nice to have a corner of the web dedicated to the breadth of .net. Enjoy!
We write about effective c# coding methods all the time. Sign up below for updates.
Great! Click the link in the e-mail to confirm. Check the spam folder if you can't find it.
No spam. Unsubscribe any time.
© 2024 - created by Corstiaan Hesselink - submit project - RSS feed - contact