Work in the constructor
The constructor should put an object into a workable state. If this is not possible, the constructor should throw an exception. This prevents an object from existing in an invalid state.
<?php declare(strict_types=1);
final class SomethingPositive
{
private int $positiveValue;
public function __construct(int $mustBePositive)
{
if ($mustBePositive < 0) {
throw new InvalidArgumentException('The greatest teacher, failure is.');
}
$this->positiveValue = $mustBePositive;
}
// ...
}
In this example, we know that the value stored in the positiveValue
attribute is positive if the SomethingPositive
object exists. The object is in a guaranteed valid state or we could not create it in the first place:
createObject.php:6: class SomethingPositive#2 (1) { private int $positiveValue => int(1) }
As long as the constructor parameter is positive, this works. But woe betide us if we try to sell our software a negative value as positive:
PHP Fatal error: Uncaught InvalidArgumentException: The greatest teacher, failure is. in SomethingPositive.php:10 Stack trace: #0 createObject-will-fail.php(5): SomethingPositive->__construct() #1 {main} thrown in SomethingPositive.php on line 10
However, a constructor should only initialize an object and not do any "real work". "Real work" would be expensive calculations, for example, because these make object creation time-consuming and therefore slow.
Expensive initialization
If you are of the opinion that you have an object that needs to be initialized expensively, then you should opt for lazy initialization instead. In this case, the expense is only incurred when an object is actually used and we do not speculatively execute code whose results are not needed at all.
In the following example, we generate random numbers in the constructor, which can then be retrieved from the object one after the other. The whole thing is admittedly a bit pointless and not really expensive, but I wanted to have a non-trivial example, hence the position
. And let's just pretend that generating 1000 random numbers is super expensive:
<?php declare(strict_types=1);
final class ExpensiveConstructor
{
private int $position = 0;
private array $randomValues = [];
public function __construct()
{
for ($i = 0; $i < 1000; $i++) {
$this->randomValues[] = random_int(1, 1000);
}
}
public function getRandomValue(): int
{
$position = $this->position;
$this->position++;
return $this->randomValues[$position];
}
}
Let's try it out:
<?php declare(strict_types=1);
require __DIR__ . '/autoload.php';
$randomValues = new ExpensiveConstructor;
for ($i = 0; $i < 3; $i++) {
var_dump($randomValues->getRandomValue());
}
expensiveRandomValues.php:8: int(99) expensiveRandomValues.php:8: int(335) expensiveRandomValues.php:8: int(422)
Works, but was expensive. We have generated 997 random numbers for free.
We can modify this so that the array is only initialized when we actually access it:
<?php declare(strict_types=1);
final class LazyInitialization
{
private int $position = 0;
private ?array $randomValues = null;
public function getRandomValue(): int
{
$this->initialize();
$position = $this->position;
$this->position++;
return $this->randomValues[$position];
}
private function initialize(): void
{
if ($this->randomValues !== null) {
return;
}
for ($i = 0; $i < 1000; $i++) {
$this->randomValues[] = random_int(1, 1000);
}
}
}
Essentially, we have only moved what was previously in the constructor to the initialize()
method. This is a very good example of how we often write the right code but do so in the wrong place.
Of course, different random numbers come out this time, but this does not detract from the fact that the whole thing works as expected:
<?php declare(strict_types=1);
require __DIR__ . '/autoload.php';
$randomValues = new LazyInitialization;
for ($i = 0; $i < 3; $i++) {
var_dump($randomValues->getRandomValue());
}
lazyRandomValues.php:8: int(513) lazyRandomValues.php:8: int(761) lazyRandomValues.php:8: int(737)
If we look at LazyInitialization
with understanding, it becomes clear that there is no point in generating 1000 random numbers in advance. We can simply generate them individually on demand:
<?php declare(strict_types=1);
final class AdHoc
{
public function getRandomValue(): int
{
return random_int(1, 1000);
}
}
This should also work:
<?php declare(strict_types=1);
require __DIR__ . '/autoload.php';
$randomValues = new AdHoc;
for ($i = 0; $i < 3; $i++) {
var_dump($randomValues->getRandomValue());
}
adHocRandomValues.php:8: int(959) adHocRandomValues.php:8: int(207) adHocRandomValues.php:8: int(45)
Yes. It's nice when it becomes so clear that the original example was not only stupid, but also unnecessarily complicated. But it is, of course, a nice plea for not initializing objects in advance, but rather executing calculations on demand if possible.
Under no circumstances should the constructor do I/O, for example by establishing a database connection, communicating with a remote service or accessing the file system. This would not only be particularly slow, but would also make it more difficult to reuse the object because we would have to fulfill external dependencies before creating the object.
Here is an example of the lazy initialization design pattern in conjunction with database connections.