Feb 12, 2021 15 min read
I believe that the best way to explain a problem is to introduce the problem itself first, I know that sounds unquestionable, but people always forget that. They jump to the solution without figuring out what the actual problem is, we all do that.
Here we have an example; imagine you are building an eCommerce platform which allows users to sell anything and you are offering them a shipping option, and to determine if this item is deliverable or not you have this ShippingRequestclass.
class ShippingRequest {
/**
* @param mixed $isDeliverable
* - false: no need for shipping service
* - true: ship my product
* - null: if to be decided
*/
public function checkAvailabilityForDelivery ($isDeliverable)
{
//...
}
}
And since the shipping service is optional, we are allowing our sellers if they want to ship their product just select the delivery flag. If not, select not deliverable flag, or just leave it empty if you didn't decide yet, and on your side (backend) based on this value you have this checkAvailabilityForDelivery which accepts $isDeliverable argument, and based on this argument value you need to take a decision and this value might be true, false, null.
And this will be your lucky day if you even got the TRUE, FALSE as bool types but you forgot that you are sending this value over HTTP request, where basically everything is a STRING so the TRUE will be "TRUE" and FALSE will be "FALSE", and if you are working with a language like PHP this will not be your last problem. Yes "false" is considered as true in PHP, as php translates that to a non-empty string value so it’s simply TRUE
var_dump((bool) “false”) // true
var_dump(“false” == false) // false
Now I hear the voice in your head saying what the hell is this?
You may say now if the problem is, if I am accepting a mixed type then simply I’ll not use it. I’ll just accept only one type.
Let's check this other example:
You are sending a welcome email to the new registered users to your eCommerce App using this WelcomeEmail class through sendEmailmethod.
class WelcomeEmail {
/**
* @param string $emailAddress
* @param string $subject
* @param string $body
*/
public function sendEmail (string $emailAddress, string $subject, string $body)
{
//...
}
}
Now you are expecting a string $emailAddress, string $subject, string $body Everything looks good, so let's use it.
(new WelcomeEmail())->sendEmail(
"registration success",
"glad to have u with us :)",
"johndoe"
);
The expected behavior: when a new user registers to your platform, that user should receive a welcome
email.
But the actual behavior: the user registered, but he didn't receive any emails.
"If you think that you can rely on a developer's attention…"
So instead of having these meaningless generic arguments with string values now, we have valid meaningful arguments by introducing a new type that has a meaning which goes beyond being only a primitive type string.
final class WelcomeEmail {
/**
* @param EmailAddress $emailAddress
* @param Subject $subject
* @param Body $body
*/
public function sendEmail (EmailAddress $emailAddress, Subject $subject, Body $body)
{
//...
}
}
This also helps you avoid mixing parameters, since the first parameter expects a type of EmailAddress not just a string. We call those new types ValueObjects.
An object that represents a descriptive aspect of the domain with no conceptual identity is called a Value Object. Value Objects are instantiated to represent elements of the design that we care about only for what they are, not who or which they are. -Eric Evans-
Let’s say that you have a method to calculate the distance between two points and each point has a pair of (x,y) values, so it might look like this:
function calculateDistance (Coordinates $startPoint, Coordinates $endPoint)
{
//...
}
Let's take another example:
function register ($age)
{
dump("your age is {$age}");
}
This is a simple register function responsible for registering new users to your platform, so for simplicity, let’s have it accept only one argument which is $age, and then you can just call this function and give it the user’s age which is in our case 10.
register(10);
Then you got a new issue reported; that your registerfunction accepts
age that is zero or less than
zero, or greater than 200 which does not make any sense.
So instead of that, let’s introduce a new type in our codebase called Agewhich is always
responsible
for giving you a valid age whenever needed.
final class Age {
public $age;
public function __construct ($age)
{
if ($age < 0 || $age > 120) {
throw new InvalidArgumentException("provided age is invalid");
}
$this->age = $age;
}
public function __toString ()
{
return $this->age;
}
}
Then let’s modify the register function signature and instead of defining that this function accepts meaningless $age argument, it accepts a valid age value of type Age and if the user provided an invalid age value, it will throw an InvalidArgumentException that the provided age value is invalid.
E.g: Imagine you have 5 banknotes of 10$ and you asked someone to take a 10$ note, does it really matter which banknote he took? Isn’t the important thing is that he took a 10$ banknote? But if we said that the police are looking for a missing 10$ banknote which has a serial number of 0xxxxxxx0, now this banknote has an identity.
$age = new Age(10);
$age->age = 1000;
register($age); // 1000
Host Port Query Params
┌──────┴──────┐ ┌┴┐ ┌────────────┴───────────┐
https://www.example.com:123/forum/questions/?tag=networking&order=newest
└─┬─┘ └───────┬───────┘
Protocol Path
Let's go one step back and ask ourselves what do we really need here? We need to ensure that when we always ask for a valid duration we get a valid duration. When we ask for seconds, we get only seconds. Ask for minutes and we should get only minutes, etc… We also need to ensure our conversion code is consistent in all places in our codebase. We actually need a ValueObject to represent the Duration type in our domain, which will contain all the validation logic and the conversion logic.
final class Duration {
private $seconds;
public function __construct (int $seconds)
{
$this->seconds = $seconds;
}
public function asMinutes () : int
{
return (int) floor($this->seconds / 60);
}
}
Imagine if you're using an IDE that does not support showing parameter name hints or you are just editing something in VIM, you can easily end up with mixing valid parameters of the same type.
function sendShippingData (string $courierName, string $trackingID)
{
//...
}
shippingData("#123456", "DHL");
final class ShippingInstrument {
private $trackingID;
private $courierName;;
public function __construct (string $courierName, TrackingID $trackingID)
{
if ($trackingID === "") {
throw new InvalidArgumentException("Invalid shipping data");
}
$this->trackingID = $trackingID;
$this->courierName = $courierName;
}
public function getCourier ()
{
return CourierStorage::where('name', $this->courierName)->first();
}
}
You get the idea now.
With value objects you will stop revalidating data everywhere. You just validate it once and then after that you should be able to use it anywhere with confidence and guarantee that it is valid. This is because the only way for a value object to exist is to be valid, once it exists you can not change it. Immutable, do you remember?
Using value objects you don’t have to guess what variables truly are, you no longer have to worry about internal representation but think about domain concepts instead and the code now looks a lot more expressive.
Check the duration example above.
At the beginning it might be hard to recognize ValueObject candidates but with the passage of time, it becomes easier. Moreover, I found some useful tricks that you might follow: