There are some great answers here. I had been struggling to grasp the concept for a while. This is because we tend to run in circles between Equality and Immutability. Initially I didn't even know this was happening, but from the various answers presented here, now I have a better understanding of why I was failing to understand earlier.
Most answers have addressed the issue in pieces but the complete picture is missing. So this is my attempt to complete the puzzle by fitting the pieces I've found above. But pardon me, this needs to be verbose and will be long.
I want to give credit to all answer's and comments above, they've been most helpful.
I'll start simple by elaborating @jason 6
example which was too simplistic for me and few others. Suppose you distribute 10 cash to all children in a class. Do they have the same amount, yes. Now you give one child 20 cash, now he has 30. Does it mean that everyone has 30 amount, no. Changing one child's cash doesn't mean that the amount others have is also changed. This is immutability. But the 10 cash of the other children have different serial numbers (note's identity). Does this mean they have different amount? No, because the value of the notes is same, this is equality.
If this is enough for you, good enough. But if you're still having trouble relating it with actual programming concepts, read on to find out about how we run in circles when trying to understand the concept and eventually, why we need immutability.
There are 3 aspects to ValueObjects:
- They have no identity, i.e. they are represented by their state (explained in the cash example above)
- Equality: Two ValueObjects with same property values should be equal.
- Immutability: If any property value needs to be changed, a new instance must be created.
We're running in circles between the Immutability problem and the Equality problem. So let's focus on each one by one.
Consider the following Employee
s and their SalaryStructure
:
public class SalaryStructure
{
public decimal BasicSalary {get;set;}
public decimal Allowance {get;set;}
}
public class Employee
{
// name, age ...
public SalaryStructure SalaryStructure {get;set;}
}
Immutability problem
Let's say I want to create a few employees who have the SAME SalaryStructure
. It feels natural to create a common SalaryStructure
instance and assign it to all employees.
void Main
{
SalaryStructure ss = new() { BasicSalary = 5000, Allowance = 500 };
Employee john = new() { SalaryStructure = ss };
Employee max = new() { SalaryStructure = ss };
// ... more employees
bool bothHaveSameSalaryStructure = john.SalaryStructure == max.SalaryStructure; // IT IS
}
As said above that both employees should have SAME salary structure and it is. Equality of VO is intact, so where's the problem?
The problem lies here. After creating various employees, you decide to enhance the Allowance
of the best performer.
...
bool bothHaveSameSalaryStructure = john.SalaryStructure == max.SalaryStructure; // IT IS
john.SalaryStructure.Allowance = 1000;
bothHaveSameAllowance = john.SalaryStructure.Allowance == max.SalaryStructure.Allowance; // it is BUT IT SHOULDN'T BE
Not only Max but all employees' Allowance
was enhanced (MUTATED). Although, Equality is still maintained because they refer to the same SalaryStructure
instance but this is an IMMUTABILITY problem.
THEY ARE EQUAL WHEN THEY SHOULDN'T BE! (remember to look at this statement later, you'll know when)
Equality problem
We can fix the above problem by creating a new instance of SalaryStructure
for each employee, although it is repetitive and seems unnatural to be creating new instances with same values.
void Main
{
Employee john = new() {
new SalaryStructure() { BasicSalary = 5000, Allowance = 500 }
};
Employee max = new() {
new SalaryStructure() { BasicSalary = 5000, Allowance = 500 }
};
bool bothHaveSameSalaryStructure = john.SalaryStructure == max.SalaryStructure; // IT IS NOT, NEW PROBLEM
john.SalaryStructure.Allowance = 1000;
bothHaveSameAllowance = john.SalaryStructure.Allowance == max.SalaryStructure.Allowance; // it is not, PREVIOUS PROBLEM SOLVED
}
This has created a new problem. Before John's allowance enhancement, although both employees have same SalaryStructure
, equality check fails. Although we have solved the previous problem of values being MUTATED to other employees, now have encountered EQUALITY problem.
THEY ARE NOT EQUAL WHEN THEY SHOULD BE! (I told you you'll know when)
And here's the vicious circle. If you begin this whole example from this Equality problem, you will end up with the Immutability problem, back where you started. TRY IT.
Solution
Now let's reconsider the above 2 problems with C# record SalaryStructure
, Employee
remains class
.
public record SalaryStructure(decimal BasicSalary, decimal Allowance);
Immutability problem
Using same code from above
void Main
{
SalaryStructure ss = new(5000, 500);
Employee john = new() { SalaryStructure = ss };
Employee max = new() { SalaryStructure = ss };
// ... more employees
bool bothHaveSameSalaryStructure = john.SalaryStructure == max.SalaryStructure; // IT IS
}
Now John gets his enhancement, the mutation:
...
bool bothHaveSameSalaryStructure = john.SalaryStructure == max.SalaryStructure; // IT IS
john.SalaryStructure.Allowance = 1000; // compiler error, Allowance is an init property and cannot be mutated. Therefore we must create a new object
john.SalaryStructure = new(5000, 1000);
bothHaveSameAllowance = john.SalaryStructure.Allowance == max.SalaryStructure.Allowance; // IT SHOULDN'T BE and it is not
Immutability solved! And it seems more natural assigning the same SalaryStructure
to all the employees because it is same for all.
Equality problem
Even if you do the unnatural thing by creating new SalaryStructure
instances:
void Main
{
Employee john = new() {
new SalaryStructure(5000,500)
};
Employee max = new() {
new SalaryStructure(5000, 500)
};
bool bothHaveSameSalaryStructure = john.SalaryStructure == max.SalaryStructure; // IT IS like it should have been
// And as for John's enhancement
//john.SalaryStructure.Allowance = 1000;
john.SalaryStructure = new(5000, 1000);
bothHaveSameAllowance = john.SalaryStructure.Allowance == max.SalaryStructure.Allowance; // nope, John's still the best
}
This way, if we use the same instance, the objects are equal, if we create different instances, the objects are still same owing to equality by value and not by reference (like in class), and if we need to change a value, instead of allowing to change single value (the compiler error above), force to change the whole object reference by creating a new instance altogether so that other's using the object are not affected.