Extracting Value Objects
First published at Tuesday 16 May 2017
Warning: This blog post is more then 7 years old – read and use with care.
Extracting Value Objects
Software systems usually get more complex over time. In the beginning a variable starts out to represent something very simple with very few rules and constraints that can are enforced in a single location of the code.
Take this code example where the user selects a start and an end date to query a list of events:
class EventController
{
public function listAction(Request $request)
{
$start = new \DateTime($request->query->get('start', '-60 minute'));
$end = new \DateTime($request->query->get('end', 'now'));
if ($start > $end) {
$tmp = $end;
$end = $start;
$start = $tmp;
}
return [
'events' => $this->eventRepository->findBetween($start, $end),
];
}
public function listTodayAction()
{
$start = new DateTime('today 00:00:00');
$end = new DateTime('today 23:59:59');
return [
'events' => $this->eventRepository->findBetween($start, $end),
];
}
}
This simple switch of start and end date when they are inverted is common and the simplicity of the code often means it is copied rather than abstracted into a method.
But how do we extract a method for this code? We could add a method switchStartEnd()
on the EventController
, but look how ugly that looks like:
private function switchStartEnd($start, $end)
{
if ($start > $end) {
$tmp = $end;
$end = $start;
$start = $tmp;
}
return array($start, $end);
}
public function listAction(Request $request)
{
$start = new \DateTime($request->query->get('start', '-60 minute'));
$end = new \DateTime($request->query->get('end', 'now'));
list ($start, $end) = $this->switchStartEnd($start, $end);
return [
'events' => $this->eventRepository->findBetween($start, $end),
];
}
Plus, the biggest downside of this refactoring is the fact that you cannot use switchStartEnd
in other places that perform date range handling.
The problem here is a code smell that is widespread in every codebase I have ever seen and is called "Primitive Obsession". It means that as developers we often rely on the most basic types of our programming language, instead of increasing the abstraction and introducing new types. In object oriented programming a type is equivalent to a new class.
Object oriented systems often have tons of classes that work on fullfilling a use-case, but they are not really types like string, integer or DateTime are.
In our example we are missing a DateRange class, and introducing it will immediately simplify our code and allow us to heavily unit-test business logic related to date ranges.
class DateRange
{
public function __construct(DateTime $start, DateTime $end)
{
if ($start > $end) {
$tmp = $end;
$end = $start;
$start = $tmp;
}
$this->start = $start;
$this->end = $end;
}
public function getStart()
{
return $this->start;
}
public function getEnd()
{
return $this->end;
}
}
Writing a unit-test for this is simple. Writing a test for the same code embedded into the Controller may be way too much work for the benefit.
We don't have to stop here though, we also have code constructing the DateRange in our controller that we can extract into the new value object:
class DateRange
{
public static function today()
{
$start = new DateTime('today 00:00:00');
$end = new DateTime('today 23:59:59');
return new self($start, $end);
}
public static function fromStrings($start, $end)
{
return new self(new DateTime($start), new DateTime($end));
}
}
Again, these methods on the DateRange
can be easily tested. If we use the DateRange everywhere in our code we could easily add more code into the fromStrings
method that does proper error handling when the strings are not valid dates for example.
Meanwhile the controller code is refactored into something very boring, all the logic is hidden in small testable classes:
public function listAction(Request $request)
{
$range = DateRange::fromStrings(
$request->query->get('start', '-60 minute'),
$request->query->get('end', 'now')
);
return [
'events' => $this->eventRepository->findBetween($range),
];
}
public function listTodayAction()
{
return [
'events' => $this->eventRepository->findBetween(DateRange::today()),
];
}
Introducing value objects is extremely helpful in structuring data and making small business rules reusable and abstracted across a large code base. The best candidates for this kind of refactoring in web applications are classes related to date (Week, DateRange, DateIterator, ...), Money, Email, IPAddress, URLs, slugged Strings, integers used as bitmasks and many others.
As soon as you detect business rules in your code that operate on primitive strings, integers or PHPs Date objects (they are not too powerful) you should think about extracting a value. If you wan't to avoid creating tons of object you can wait for 3-5 different rules on the same kind of primitive type or the same rule spread in 3-5 locations.
Subscribe to updates
There are multiple ways to stay updated with new posts on my blog: