Value Objects - Your Savior from Primitive obsession

Feb 12, 2021 15 min read

image From Movie The Last Samurai on IMDB

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.

What is the problem?

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?


image

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"
    );

                
            

You have an Issue!


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…"


image

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.


What is ValueObject?

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-

Which means?


  • A value object is an object that is defined according to its value or its data rather than its identity.
  • A value object represents a typed value in your domain.
  • Also, this means we are representing things that are related to each other as a compound object or type.

e.g:
  • A 2D coordinate consists of an X value and Y value.
  • An mount of money consists of {number + currency}.
  • A date range consists of {start_date + end_date}.

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.


ValueObject Characteristics:


  • No Identity
  • ValueObjects are defined by their attributes or data. They are equal if their attributes are equal, and they are completely interchangeable.
  • Unlike entities, they don't have an ID property.

So what is an Identity?

  • an object that represents a specific thing would be an entity. E.g: Models ----> represents Entities.
  • ValueObjects are simple Objects that represent simple things but not specific things .

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.

  • A ValueObjects can't exist without a parent Entity owning them.
    E.g saying an address like this (Egypt, Cairo, street number, building number) doesn't provide a full comprehensive statement.
    But if we mention that John Doe’s address is (Egypt, Cairo, street_number, building_number) now this makes complete sense.
  • ValueObjects should not have separate tables in DB.
  • There always must be a composition relationship between a ValueObject class and an Entity class, without it ValueObjects doesn't make any sense.

2. Immutability

  • Mutable object: is an object whose internal state can be changed.
  • Immutable object: is an object whose internal state can not be changed .
  • The only way to change its value is by a full replacement (a new instance with a new value).
  • Immutable: ---> NO SETTERS, NO PUBLIC PROPERTIES, the constructor is the only injection point.
    E.g in our previous age example, we defined $age property in the Age Class as public. And this means that any user of Age type can easily change the value, so instead of that, we need to change it to be private so no one can access or modify the age value.


                
$age = new Age(10);
$age->age = 1000;
register($age); 	// 1000

                
            

3. Self Validation

  • A ValueObject must verify the validity of its attributes when being created.
  • An error or exception should be raised if any of the attributes are invalid.

Reasons for ValueObjects to exists in your codebase:


1. Reduce Primitive Obsession

  • Primitive Obsession is using primitive data types {int, string, float, etc...} to represent domain ideas.
    E.g we usually store a URL as a String but a URL has more information and specific properties Compared to a String and by storing it as a string you can no longer access these URL specific properties (The domain concept) without additional code.
    
                    
                  Host	    Port			             Query Params
            ┌──────┴──────┐ ┌┴┐		             ┌────────────┴───────────┐
    https://www.example.com:123/forum/questions/?tag=networking&order=newest
    └─┬─┘				       └───────┬───────┘
    Protocol		                      Path
    
                    
                

2. Type Safety:

  • When using primitive types, it is easy to make mistakes and bugs even when we are using valid types but we are passing invalid values.
    E.g: private float $duration;
    Float here is a valid type and when you initialize this variable with a float value, your compiler/interpreter won’t complain about this since you're giving it a valid float value.
    But what about the type of the Duration itself since the Duration could be (seconds, minutes, hours, etc..)?
    You might now scratch your head and say why not just rename the variable, and you can just mention it explicitly that you want the duration in seconds, so it might be something like $durationInSeconds.
    What if someone from your business team requested a new Change Request as they always do? They want to display the duration in minutes and hours, so now you need to convert the duration to adopt the new changes which means extra conversion code, so you might ask yourself about your options: where should this conversion code reside?
  • put it inline just right before the usage! What if we decided that we need to use Duration in minutes somewhere else. No copy/paste. Don’t Repeat Yourself.
  • You might put it in as a helper function in a helpers file where all the evils reside. I think you need to reconsider this decision again.

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);
       }
    }

                
            

3. Mixing parameters:

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.


4. Avoid Revalidating:

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?


5. Easy to read:

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.


6. Ensure Consistency:

Check the duration example above.


Drawbacks:


  • Wrapping every single primitive type you have with a ValueObject leads to creating too many classes, and this will bloat the codebase leading to a ridiculous codebase that you don’t want to work with. So choose wisely what might be a good ValueObject candidate.
  • Performance issues: in some cases creating too many instances might lead to a performance drop.

Load your brain with ValueObjects:


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:


  • You have a special validation logic that has to be applied in multiple places.
    E.g: phone numbers, email addresses, etc…
  • You have a special formatting logic that is used to render values for humans.
    E.g: Price = Amount + Currency
  • In general, if you find yourself in a situation where you are constantly passing variables together then this might be a good candidate for a ValueObject.

References: