Value Objects
Since PHP Value Objects is now a thing, I thought I’d write about them as well. You may also want to read http://kacper.gunia.me/blog/ddd-building-blocks-in-php-value-object and http://kacper.gunia.me/blog/validating-value-objects.
So Value Objects are objects that are considered equal based on their values and not on identity. For the sake of having an example, let’s say that our domain is ordering takeaway food for delivery. So within this we have the concept of a Takeaway as a place we can order food from. Since that is what we call them here even if they only deliver food, for, er, reasons.
Typically we would model this with an entity and not a value object. This does indeed fit well since equality between two takeaways is not determined by value. For now, let's assume the only property they have are their names. If we have two takeaways with the same name they are not the same takeaway:
new Takeaway('Thai Tantic') != new Takeaway('Thai Tantic');
A takeaway that changes its name can still be the same takeaway:
$takeaway = new Takeaway('Wok around the clock');
$takeaway->changeName('Wok this way');
So if we are deciding whether they are equal we have some identity beyond just values such as its name.
If we consider just the name itself though then two names are the same if they are the same value. This is clearly true with strings:
'Lord of the Fries' == 'Lord of the Fries';
'Man Fryday' != 'The Codfather';
If we created an object to encapsulate the name then this needs not have identity but can just be consider equal if the value is the same:
new TakeawayName('Just Falafs')
== new TakeawayName('Just Falafs');
new TakeawayName('Abra Kebabra')
!= new TakeawayName('Jason Donnervan');
So if we want to model the Takeaway's name as an object then a value object is a good fit. Some value objects, such as the TakeawayName are about single values but trying to focus on the benefits of making that an object are perhaps a little misleading. Instead let's look at where we might want a value object to have more than one property.
So what other properties might our Takeaway entity have if it just stores them as primitive values? One important domain concept is the area that they will deliver to. For now let’s say assume a simplistic concept of this where it is determined by the location and a distance from that location. This will then form a circle which they will deliver within. The distance can be provided in kilometers or miles as well so we will need to know which it is. So we have the following:
class Takeaway
{
//...
private $long;
private $lat;
private $distanceQuantity;
private $distanceUnits;
}
So let’s consider the distance. This is made up of two values, the unit of measurement and the amount of that measurement. The amount alone e.g. 6 is not a distance. The unit of distance e.g. km is not a distance. 6 km is a distance.
Both need to be equal for a distance to be considered equal:
- 5 km = 5 km
- 5km != 5cm
- 5Km != 8km
So using primitives as two separate fields in the Takeaway entity is not fulling capturing the connection between these values. It is possible to change just the unit or just the quantity which does not seem like the correct behaviour. Instead we can extract an object that represents a distance, then we can change the whole distance object instead.
class Distance
{
private $quantity;
private $unit;
public function __construct($quantity, $unit)
{
$this->quantity = $quantity;
$this->unit = $unit;
}
public function equals(Distance $toCompare)
{
return $this->quantity == $toCompare->quantity
&& $this->unit == $toCompare->unit;
}
}
class Takeaway
{
//...
private $long;
private $lat;
/**
* @var Distance
*/
private $distance;
}
Likewise lat and long as separate properties don’t seem right, what we are really interested in is the location they determine. So let’s make that an object as well and make the reason we are interested in these values more explicit in our model.
class Location
{
private $long;
private $lat;
public function __construct($long, $lat)
{
$this->long = $long;
$this->lat = $lat;
}
public function equals(Location $toCompare)
{
return $this->long == $toCompare->long
&& $this->lat == $toCompare->lat;
}
}
class Takeaway
{
//...
/**
* @var Location
*/
private $location;
/**
* @var Distance
*/
private $distance;
}
So value objects need not wrap a single primitive value. As well as this, value objects do not need to just contain primitives. What we are really interested in here is the area the company are willing to deliver to. The distance and location do not capture and make explicit this concept in out code. So let’s extract an object that represents the area covered. This can be made up of the location and distance value objects and not have any primitive values itself.
class DeliveryArea
{
private $location;
private $radius;
public function __construct(Location $location, Distance $radius)
{
$this->location = $location;
$this->radius = $radius;
}
public function equals(DeliveryArea $toCompare)
{
return $this->location->equals($toCompare->location) && $this->radius->equals($toCompare->radius);
}
}
class Takeaway
{
//...
/**
* @var DeliveryArea
*/
private $areaCovered;
}
Going back to the start again, these are value objects because there equality is determined by values and not by identity. The area covered still has no identity, we can swap it with another object with the same values without any issues. None of this says anything about behviour though; being a value object does not mean having no behaviour and just values.
So if we have a location and we want to find out if the company will deliver to it then we can ask the company object:
class Takeaway
{
//...
/**
* @var DeliveryArea
*/
private $areaCovered;
public function deliversTo(Location $location)
{
//determine if location falls within the area covered.
}
}
This method could calculate whether the location falls in the area itself but it would be simpler to just ask the Area object if the location falls in it:
class Takeaway
{
//...
/**
* @var DeliveryArea
*/
private $areaCovered;
public function deliversTo(Location $location)
{
return $this->areaCovered->includes($location);
}
}
Not only can the value object have behaviour but it attracts it. It is better for the DeliveryArea to decide whether the Location is included or not than have the Takeaway reach into the DeliveryArea to get its values and make the decision. This breaks encapsulation and by making it a method on the DeliveryArea it can be called from elsewhere. Putting the logic involved in checking in the Takeaway ties it to the wrong object.
So our company object now just has an AreaCovered object and a method that delegates to it for deciding if a location is within it. One thing that stands out here is that the company no longer knows anything about what that Area is or what the location is. When we started they were tied to lat, long, and a radius. Should alternative ways of identifying locations and areas - e.g. a list of postcodes that is covered then nothing needs changing in the company object to support this. Different implementations of the Location and AreaCovered objects could be used for this. We could extract interfaces for Location and AreaCovered and have different implementations without changing the Takeaway entity at all.
This encapsulation of data and polymorphism are the benefits of OO. If we just had the company object with primitives and that had the logic of deciding is a location was covered then supporting different area and location types would be much more difficult. We have introduced more objects, where each one is in itself very simple.
As well as this, the company object’s level of abstraction is higher now. We can see that it has an area that is covered and that we can find out whether a location falls in it. For many purposes this may be all we need to know when reading the code. We need not concern ourselves with the detail of how these things are implemented. Without this level of abstraction we would see that a company has a latitude, a longitude, a distance amount and a distance unit and some logic around these value. This tells us too much about the detail and not enough about the purpose and this will only get worse if we want to support more ways of representing these things.
So value objects are useful for encapsulating data and exposing related behavior. Well, yes, but that’s not specific to value objects that is Object Orientation.
So what about immutability and validation? Well they are not unimportant but they are not what value objects are primarily about. Objects are about encapsulation and polymorphism. Value objects are the subset of objects where equality is determined by value and not identity.
More on those things soon anyway.