Immutable Objects in PHP
When I first learned to program, I made many objects that were mutable. I made lots of getters and lots of setters. I could create objects using a constructor and mutate and morph the heck out of that object in all kinds of ways. Unfortunately, this led to many problems. My code was harder to test, it was harder to reason about, and my classes became chock full of checks to ensure that it was in a consistent state anytime anything changed. Of course, my classes had to be backed by unit tests and this often resulted in a combinatorial explosion. This how I look when I try to imagine all of the possibilities and how to test them.
Now, I favor creating immutable objects. The folks over in the functional camp are super excited about this concept. However, it’s been around for quite a while and it has so many benefits.
Right from the get-go, immutable objects make it so much easier to write and reason about your code. Propellerheads like to say that the class invariant is established once during object construction and then remains unchanged. That’s just a fancy way of saying that you place all of your consistency checks in the constructor and then your done.
Another benefit of using immutable objects is that the state of your program will be consistent even if an exception occurs. How many times have we all ran accross a situation where an exception occurs in our code and we are left with picking up the pieces. It’s like a bad relationship where you have to bail someone out of jail after they get caught driving 80mph while completely hammered. Anywho…
When iterating through collections, immutable objects are amazing at keeping the bug count low. There is nothing worse than having an object that you have a reference to getting changed because some other part of the code iterates through a collection to modify each object.
When using immutable objects, caching gets so much easier. Specifically, cache invalidation gets easier.
The last benefit of immutability is one that is not really important in the PHP world. Specifically, concurrency or code running in an async fashion. However, this benefit is huge, and perhaps the most important, in languages that do offer those capabilities.
When thinking about the possibilities, it’s like my mind is blown.
Ok, so that all well and good, but you may be thinking, how do I make an immutable class? Or better yet, how do I make one that isn’t a pain to work with? Well friends, step right up and let me show. Here are the rules that we will use:
- Everything that class needs to be instantiated must be passed in to the constructor when the object is created. If this gives you nightmares about huge constructor lists, just set that concern aside for a moment, grasshopper. We’ll address that later.
- No more setters. Evar.
- All fields must be private. But, you should have been doing this one anyway.
- Anytime you want to do something that changes the state of the current object (i.e. $this), then create a new object instead and return it.
Now, you may be thinking that immutability has a performance cost, and you would be correct. In general, immutable objects increases the load on the garbage collector. Additionally, creating a new object can take longer than just mutating an existing object. However, the benefits often outweigh the costs.
Let’s take a look at an example of a immutable class:
class ImmutableClass
{
/** @var int */
private $paramA;
/** @var string */
private $paramB;
/** @var string */
private $paramC;
/**
* @param int $paramA
* @param string $paramB
* @param string $paramC
*/
public function __construct($paramA, $paramB, $paramC)
{
// Validate paramA...
// Validate paramB...
// Validate paramC...
$this->paramA = $paramA;
$this->paramB = $paramB;
$this->paramC = $paramC;
// Validate class state...
}
/**
* @return int
*/
public function getParamA()
{
return $this->paramA;
}
/**
* @return ImmutableClass
*/
public function changeState()
{
// Perform work...
$paramA = $this->paramA;
$newParamA = $paramA + 5;
return new ImmutableClass($newParamA, $this->paramB, $this->paramC);
}
}
There are a few important items of note about this class:
- The constructor is used to create the class.
- Validation of class state is handled in the constructor only.
- No setters.
- Getters are ok.
- Anything that could modify the class’s state results in a new object being created instead.
Testing a class that is written to be immutable is fairly straight forward.
- Test the constructor to ensure that the exceptions are thrown for bad input data.
- Test any getters.
- Test any methods that create new objects and ensure those objects are created with the correct state.
- Test any side-effects.
Creating Immutable Objects
(i.e. How do I deal with long constructor lists?)
To create an immutable object we use the build design pattern. Don’t worry, the builder design pattern is very very similar to the factory pattern and is a variation on the theme. Here is an example:
class ImmutableClassBuilder
{
/** @var int */
private $paramA;
/** @var string */
private $paramB;
/** @var string */
private $paramC;
/**
* @param int $paramA
*
* @return $this
*/
public function withParamA($paramA)
{
$this->paramA = $paramA;
return $this;
}
/**
* @param string $paramB
*
* @return $this
*/
public function withParamB($paramB)
{
$this->paramB = $paramB;
return $this;
}
/**
* @param string $paramC
*
* @return $this
*/
public function withParamC($paramC)
{
$this->paramC = $paramC;
return $this;
}
/**
* @return ImmutableClass
*/
public function build()
{
$paramA = $this->paramA;
$paramB = $this->paramB;
$paramC = $this->paramC;
return new ImmutableClass($paramA, $paramB, $paramC);
}
}
Here is example of using it.
$builder = new ImmutableClassBuilder();
$builder->withParamA(1);
$builder->withParamB('2');
$builder->withParamC('3');
$immutableClass = $builder->build()
Here is another example of using it.
$immutableClass = (new ImmutableClassBuilder())
->withParamA(1)
->withParamB('2')
->withParamC('3')
->build();
That pretty much all there is to it. Simple huh!