Here is the best analogy that I've been able to "teach" to my co-workers when wanting to work on DDD with Value Objects.
Every time you think of a Value Object, think of DateTime object in .Net.
For example, when you have a DateTime in your object, and you persist that object, you are only saving a string representation of the date and time in question. It can also be formatted to satisfy certain locales (month-day or day-month for example).
Yet, the DateTime object is so sophisticated internally. They have multiple members to store different values (day, month, year, hour, minute, seconds, milliseconds) so that they can do operations like .AddDay(2)
which basically takes the day field, adds a 2 to it, and returns a brand new DateTime object with the new addition. So, you really never modify the DateTime object. You create a new one every time you use one of their methods.
Same for your custom value objects... Say, CreditCard... let's look at an example.
We want to store credit card information for a user. In our domain, we don't need to keep track of the Creditcard. So, the first thing that we may be tempted to do is to create 4 fields.
string CreditCardNumber {get;set;}
string CreditCardExpiration {get;set;}
string CreditCardType {get;set;}
string CreditCardSecurityCode {get;set;}
Now say that we have these fields in our AggregareRoot/Entity. If we want to 'parse' our ExpirationDate to a meaningful DateTime object (07/18 to 07/01/2018) for validation purposes, now we need to add that logic to our AR/Entity. You may think... "that's ok" but it is not, because our AR/Entities should be focused on the problem that they solve, not validating credit card expiration dates. That's more ancillary work.
Imagine if DateTime didn't have all those functionalities of Date and Time manipulation and you need to implement all of that in your class, does it sound right? Of course not.
So, in our example, we create a CreditCard class
public class CreditCard: ValueObject
then we transfer all of those fields into this class (you can remove the CreditCard prefix). Now you have a lot of power in your hands. You can create as many validation methods as you want: checksum validation, expiration date validation, and parsing, actually validate that the credit card type matches its number sequence (Mastercard starts with 54 if I am not mistaken)... I mean, you can add SO many features to your CreditCard class without polluting your AR/Entity. You can create your own "format" to persist this info if you want. Say that you override your .ToString()
method and format it as "MC 5474 3434 8383 4848 07/18". Or you can configure your ORM to map to the individual read-only properties for those values.
How do you use it effectively? Simple. Say that you have a PaymentMethod
class that is an Aggregate Root. You want to keep track of that payment method because it will be used to make payments, so maybe you want to have a payment history linked to this class.
Inside your class, all you need to do is add a CreditCard value object...
public CreditCard BusinessCard {get;}
Now in one of the methods of your AR... say,
private void Enable()
{
this.IsEnabled = BusinessCard.IsValid();
}
Do you see? I moved the logic of validating the card into the value object. And guess what? Can be reused!!! You can actually share value objects.
I hope this helps!