DEV Community

Cover image for A mockumentary about Value Objects
Carlos Gándara
Carlos Gándara

Posted on

A mockumentary about Value Objects

Some weeks ago, this happened:

Precision for the win

It is a screenshot from the actual changelog of the Jira app in the App Store. Two thoughts outstand from the several that arose when I saw it for the first time:

  • In which scenario this may be considered worthy to be highlighted.
  • What a good case it is for explaining the power of Value Objects (VOs).

Let's take a number of assumptions about how Jira is implemented in order to illustrate what the Value Objects pattern is and why it is one of the most powerful patterns in object-oriented programming. Needless to say, the example is 100% made up and not based on the actual Jira code.

Pattern definition

The definition of Value Object according to wikipedia is

A small object that represents a simple entity whose equality is not based on identity

I would say they are not necessarily small, although it is legit to claim they are usually small. Furthermore, there are other non-mandatory but highly desirable characteristics of Value Objects:

  • They are immutable.
  • They attract behavior related to the concept they represent.

Leveraging encapsulation and semantics

Coming back to our new outstanding Jira feature, let's assume the ability to have decimal story points is that important because a) it was not possible before and b) there is a fair amount of work behind making it possible.

Let's assume as well, and it looks like a fair assumption, that Jira has a massive codebase. It is a huge application and it has been around for a while. Probably Story Points are all around the place since they are a relevant piece of the average adoption of Scrum, a field in which Jira kind of excels.

Now consider the following code snippets from a possible implementation of some of the concepts Jira handles (in PHP, just in case it is not clear enough the not-real nature of the code exposed):

class Task {
    private string $id;
    private string $title;
    private int $storyPoints;

    public function estimation(): int {}
}

class Sprint {
    private TaskList $tasks;
    private int $totalStoryPoints;

    public function estimation(): int {}
    public function addTask(Task $task): void {}
}

class SprintReport {
    private int $completedStoryPoints;

    public function addCompletedStoryPoints($date, int $amount): void {}
}
Enter fullscreen mode Exit fullscreen mode

All the references to int values are actually references to Story Points. As the code is now, allowing decimals for them is far from trivial: we have seven different appearances to change only in this small bit of code. Imagine the whole codebase or even the implementation of the methods outlined here.

Now, if we consider the following Value Object to model the concept of Story Point:

class StoryPoint {
    public function __construct(private int $value) {}

    public function value(): int {
        return $this->value;
    }
}
Enter fullscreen mode Exit fullscreen mode

and how the code would look like using it:

class Task {
    private string $id;
    private string $title;
    private StoryPoint $estimation;

    public function estimation(): StoryPoint {}
}

class Sprint {
    private TaskList $tasks;
    private StoryPoint $totalStoryPoints;

    public function estimation(): StoryPoint {}
    public function addTask(Task $task): void {}
}

class SprintReport {
    private StoryPoint $completedStoryPoints;

    public function addCompletedStoryPoints($date, StoryPoint $amount): void {}
}
Enter fullscreen mode Exit fullscreen mode

The change from integer to float, allowing decimals, implies only two changes: the two int references in the StoryPoint Value Object. And probably whatever mapping you are doing in your persistence system to translate a Story Point to a database column supporting decimals, something required whatsoever.

Confronting our VO with the pattern definition it hardly could be smaller than that, although size is not a key aspect.

It is identified by its value, meaning we don't care about having one instance of it or another as long as they hold the same value:

$aStoryPointVO = new StoryPoint(2);
$anotherStoryPointVO = new StoryPoint(2);

//using one VO or the other is indifferent
//and would result in an equivalent Task
$aTask = new Task('task-id', 'Task title', $aStoryPointVO); 
Enter fullscreen mode Exit fullscreen mode

Our VO sticks to the definition and that's good. In the end... well, it is a VO. Out of the box, Value Objects provide encapsulation, greatly reducing the number of places to modify when introducing a change in the concept they represent, and semantics. The other two aspects we mentioned before, immutability and attracting behavior, are not "built-in" and they depend on our diligence.

Attracting behavior

Attracting behavior

Attracting behavior is something that comes naturally and bolsters the encapsulation and meaningfulness of the Value Object. Immutability is quite related to it as well. We will use the method addCompletedStoryPoints in our SprintReport example to illustrate both. Consider this not desirable way of implementing it:

class SprintReport {
    private StoryPoint $completedStoryPoints;

    //Don't do this
    public function addCompletedStoryPoints($date, StoryPoint $amount) {
        $previousValue = $this->completedStoryPoints->value();
        $addedValue = $amount->value();
        $this->completedStoryPoints = new StoryPoint($previousValue + $addedValue);
    }
}
Enter fullscreen mode Exit fullscreen mode

With this operation we are breaking the encapsulation provided by the Value Object: the client code (the method) knows that the internal representation of the value of StoryPoint is an integer and we fall into the same trap as before, because a change in this internal value will lead to updates on several parts of our codebase -the Value Object itself plus all parts that know too much about its internals and use them, like in the code above.

The logic that belongs to a VO should be put inside the VO. Adding two StoryPoint is an operation that makes sense only inside StoryPoint because only StoryPoint should know about its internals. That's what attracting behavior means and it does very good work on empowering our code encapsulation and semantics.

Consider this VO-free piece of code:

$age = 21;
$storyPoints = 3;

$somethingVeryWrong = $age + $storyPoints;
Enter fullscreen mode Exit fullscreen mode

When your code works with primitives to handle concepts of your domain (which by the way is a code smell named primitive obsession), the programming language cannot prevent you from doing such things. Types and Value Objects do.

So, how to approach adding Story Points? Putting the add operation in StoryPoint and passing another StoryPoint to be added:

class StoryPoint {
    public function __construct(private int $value) {}

    public function value(): int {
        return $this->value;
    }

    //We are skipping the return type on purpose. Stay tuned!
    public function add(StoryPoint $storyPoint) {}
}
Enter fullscreen mode Exit fullscreen mode

This way we guarantee that nobody knows about the internals of our VO and can nonsensically add random unrelated values to them.

Immutability

Avoid mutant VOs!

When implementing the add operation in our StoryPoint VO we may be tempted to go like the following:

class StoryPoint {
    public function __construct(private int $value) {}

    public function value(): int {
        return $this->value;
    }

    //Don't do this
    public function add(StoryPoint $storyPoint) {
        $this->value = $this->value + $storyPoint->value();
    }
}
Enter fullscreen mode Exit fullscreen mode

For sure we are sticking to the encapsulation, but we are introducing mutability as well. And we mentioned we want our VOs to be immutable when possible. Mutability is something to avoid because it is a source of hard to track bugs. Consider the following:

$fiveStoryPoints = new StoryPoint(5);
$report = new SprintReport();
$report->addCompletedStoryPoints($today, $fiveStoryPoints);
//Can we trust our $fiveStoryPoints are still five?
Enter fullscreen mode Exit fullscreen mode

As long as our StoryPoint public interface (the add method) is changing the state of the instance like in the previous implementation, we cannot guarantee its value was not changed inside the call to addCompletedStoryPoints. Of course, we can open addCompletedStoryPoints and check there is no call to the add method there, but that's a manual approach that does not scale very well.

The way to make our VOs immutable is returning a new fresh VO as a result of the operation. Here is the implementation plus the usage in SprintReport:

class StoryPoint {
    public function __construct(private int $value) {}

    public function value(): int {
        return $this->value;
    }

    public function add(StoryPoint $storyPoint) {
        return new StoryPoint($this->value + $storyPoint->value());
    }
}

class SprintReport {
    private StoryPoint $completedStoryPoints;

    public function addCompletedStoryPoints($date, StoryPoint $amount) {
        $this->completedStoryPoints = $this->completedStoryPoints->add($amount);
    }
}
Enter fullscreen mode Exit fullscreen mode

Although it may seem a waste of memory and resources, those are not aspects we should care bout in the vast majority of the situations -garbage collection will take care of the instances not referenced soon enough- and the value we get in exchange is far more significant.

Some literature claims immutability is not a mandatory aspect of VOs, although highly desirable. To be honest, I've never found a situation where mutability was justified, but it does not mean there are none. As with all the "rules" in software development, it is on the developer's good reasoning to decide when to bend them in a given context. Default to immutable VOs and if you find a scenario where it is not advisable, please let me know :)

Final word

Hopefully, this post has given a good grasp on the benefits the Value Object pattern provides to any OOP codebase, even using a naive example to illustrate it. Introducing them in greenfield applications or code suffering from primitive obsession pays off from the first minute, so I cannot encourage you enough to use them.

In a following article, we will see how to introduce our StoryPoint Value Object in this made-up Jira codebase we have, applying refactoring techniques in baby steps. Probably the two most powerful tools available in OOP. Stay tuned!

Top comments (0)