Missing in PHP7: Value objects

This is the third post in my Missing in PHP7 series. The previous one is about named parameters.

A Value Object does not have an identity, which means that if you have two of them with the same data, they are considered equal (take two latitude, longitude pairs for instance). Generally they are immutable and do not have methods beyond simple getters.

Such objects are a key building block in Domain Driven Design, and one of the common types of objects even in well designed codebases that do not follow Domain Driven Design. My current project at Wikimedia Deutschland by and large follows the “Clean Architecture” architecture, which means that each “use case” or “interactor” comes with two value objects: a request model and a response model. Those are certainly not the only Value Objects, and by the time this relatively small application is done, we’re likely to have over 50 of them. This makes it real unfortunate that PHP makes it such a pain to create Value Objects, even though it certainly does not prevent you from doing so.

Let’s look at an example of such a Value Object:

As you can see, this is a very simple class. So what the smag am I complaining about? Three different things actually.

1. Initialization sucks

If you’ve read my previous post in the series, you probably saw this one coming. Indeed, I mentioned Value Objects at the end of that post. Why does it suck?

The lack of named parameters forces one to use a positional list of non-named arguments, which is bad for readability and is error prone. Of course one can create a PersonName Value Object with first- and last name fields, and some kind of partial email message Value Object. This only partially mitigates the problem though.

There are some ways around this, though none of them are nice. An obvious fix with an equally obvious downside is to have a Builder using a Fluent Interface for each Value Object. To me the added clutter sufficiently complicates the program to undo the benefits gained from removing the positional unnamed argument lists.

Another approach to avoid the positional list is to not use the constructor at all, and instead rely on setters. This does unfortunately introduce two new problems. Firstly, the Value Object becomes mutable during its entire lifetime. While it might be clear to some people those setters should not be used, their presence suggests that there is nothing wrong with changing the object. Having to rely on such special understanding or on people reading documentation is certainly not good. Secondly, it becomes possible to construct an incomplete object, one that misses required fields, and pass it to the rest of the system. When there is no automated checking going on, people will end up doing this by mistake, and the errors might be very non-local, and thus hard to trace the source of.

Some time ago I tried out one approach to tackle both these problems introduced by using setters. I created a wonderfully named Trait to be used by Value Objects which use setters in the format of a Fluent Interface.

The trait provides a static newInstance method, enabling construction of the using Value Object as follows:

The trait also provides some utility functions to check if the object was fully initialized, which by default will assume that a field with a null value was not initialized.

More recently I tried out another approach, also using a trait to be used by Value Objects: FreezableValueObject. One thing I wanted to change here compared to the previous approach is that the users of the initialized Value Object should not have to do anything different from or additional to what they would do for a more standard Value Object initialized via constructor call. Freezing is a very simple concept. An object starts out as being mutable, and then when freeze is called, modification stops being possible. This is achieved via a freeze method that when called sets a flag that is checked every time a setter is called. If a setter is called when the flag is set, an exception is thrown.

To also verify initialization is complete in the code that constructs the object, the trait provides a assertNoNullFields method which can be called together with freeze. (The name assertFieldsInitialized would actually be better, as the former leaks implementation details and ends up being incorrect if a class overrides it.)

A downside this second trait approach has over the first is that each Value Object needs to call the method that checks the freeze flag in every setter. This is something that is easy to forget, and thus another potential source of bugs. I have yet to investigate if the need for this can be removed via some reflection magic.

It’s quite debatable if any of these approaches pay for themselves, and it’s clear none of them are even close to being nice.

2. Duplication and clutter

For each part of a value object you need a constructor parameter (or setter), a field and a getter. This is a lot of boilerplate, and the not needed flexibility the class language construct provides, creates ample room for inconsistency. I’ve come across plenty of bugs in Value Objected caused by assignments to the wrong field in the constructor or returning of the wrong field in getters.

“Duplication is the primary enemy of a well-designed system.”

― Robert C. Martin

(I actually disagree with the (wording of the) above quote and would replace “duplication” by “Complexity of interpretation and modification”.)

3. Concept missing from language

It’s important to convey intent with your code. Unclear intent causes time being wasted in programmers trying to understand the intent, and time being wasted in bugs caused by the intent not being understood. When Value Objects are classes, and many other things are classes, it might not be clear if a given class is intended to be a Value Object or not. This is especially a problem when there are more junior people in a project. Having a dedicated Value Object construct in the language itself would make intent unambiguous. It also forces conscious and explicit action to change the Value Object into something else, eliminating one avenue of code rot.

“Clean code never obscures the designers’ intent but rather is full of crisp abstractions and straightforward lines of control.”

― Grady Booch, author of Object-Oriented Analysis

I can haz

See also


3 Comments on “Missing in PHP7: Value objects

  1. Pingback: Missing in PHP7 – Entropy Wins

  2. Pingback: Missing in PHP7: Named parameters – Entropy Wins

  3. Pingback: Missing in PHP7: Collections – Entropy Wins

Leave a Reply