Historically I have performed validation of my objects within their constructors and thrown an exception when validation fails. For example:
class Name
{
const MIN_LENGTH = 1;
const MAX_LENGTH = 120;
private $value;
public function __construct(string $name)
{
if (!$this->isValidNameLength($name)) {
throw new InvalidArgumentException(
sprintf('The name must be between %d and %d characters long', self::MIN_LENGTH, self::MAX_LENGTH)
);
}
$this->value = $name;
}
public function changeName(string $name)
{
return new self($name);
}
private function isValidNameLength(string $name)
{
return strlen($name) >= self::MIN_LENGTH && strlen($name) <= self::MAX_LENGTH;
}
}
Whilst I like this approach as my object is responsible for enforcing its consistency and ensuring it is always-valid, I've never been overly enthusiastic about the use of exceptions. Whilst there are people who would argue for and against the use of exceptions as above, it does limit the number of validation messages I can return when performing validation over multiple objects. For example:
class Room
{
private $name;
private $description;
public function __construct(Name $name, Description $description)
{
$this->name = $name;
$this->description = $description;
}
}
class Name
{
public function __construct(string $name)
{
// do some validation
}
}
class Description
{
public function __construct(string $description)
{
// do some validation
}
}
Should both Name
and Description
fail validation, I want to be able to return the failure messages for both objects, not just a single exception from whichever object failed first.
Having done some reading on the notification pattern I feel this would be a good fit for my scenario. Where I become stuck is how I perform validation and prevent my object from entering an invalid state should validation fail.
class Name
{
const MIN_LENGTH = 1;
const MAX_LENGTH = 120;
private $notification;
private $value;
public function __construct(string $name, Notification $notification)
{
$this->notification = $notification;
$this->setName($name);
}
private function setName(string $name)
{
if ($this->isValidNameLength($name)) {
$this->value = $name;
}
}
private function isValidNameLength(string $name)
{
if (strlen($name) < self::MIN_LENGTH || strlen($name) > self::MAX_LENGTH) {
$this->notification->addError('NAME_LENGTH_INVALID');
return false;
}
return true;
}
public function hasError()
{
return $this->notification->hasError();
}
public function getError()
{
return $this->notification->getError();
}
}
I have a few concerns about the above:
- If validation fails, then the object is still constructed but its
$value
isnull
which is not a valid state. - After creating a
Name
, I must remember to callhasError
to determine if a validation error has occured. - I am now pebbling my domain objects with
hasError
/getError
functions which I'm not sure is good practice.
Is there a piece of this puzzle I am missing? How would I got about utilising the notification pattern but ensuring that my object can not enter an invalid state?
Factory
for which object, theRoom
? In thisRoomFactory(string name, string description)
I create myName
&Description
objects and checkhasError
on each to determine their validity? Does that not still allow for invalid invariants? – Guadalajara