When you close the door of my home, they're closed and you need a key to get in. But what if your door has door handle? You have to also lock them.
Instead of just closing the door you have to close the door and that one more thing. Why is that a bad thing in the code and how to avoid it?
I started refactoring easybook package last week and I found a few interesting snippets there.
Let's start with a simple question: which of following 7 code snippets opens space for potential bug that will take your new colleague 2-3 hours to solve?
<?php
// ...
foreach ($this->publishingItems as $item) {
$item['content'] = $this->decorateContent($item['content']);
}
<?php
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
final class SomeEventSubscriber implements EventSubscriberInterface
{
/**
* @return string[]
*/
public static function getSubscribedEvents(): array
{
return ['processEvent'];
}
public function processEvent(ItemAwareEvent $itemAwareEvent)
{
$item = $itemAwareEvent->getItem();
$item['content'] = 'new content';
}
}
<?php
// ...
$this->bookGenerator->setBookDirectory($bookDirectory);
$this->bookGenerator->generate();
<?php
// ...
protected function setUp(): void
{
$this->epub2Publisher = $this->container->get(Epub2Publisher::class);
}
<?php
class SomeController
{
public function someAction()
{
// ...
$product = ...;
$this->entityManager->persist($product);
$this->entityManager->flush();
}
}
Don't mind the presence of Doctrine in Controller here. That's a code smell we don't look for right now.
<?php
use Symfony\Component\Console\Command\Command;
class SomeCommand extends Command
{
/**
* @var SomeDependency
*/
private $someDependency;
public function __construct(SomeDependency $someDependency)
{
$this->someDependency = $someDependency;
}
// ...
}
<?php
use PhpParser\NodeTraverser;
class SomeNodeTraverser extends NodeTraverser
{
/**
* @var SomeDependency
*/
private $someDependency;
public function __construct(SomeDependency $someDependency)
{
$this->someDependency = $someDependency;
}
public function process()
{
foreach ($this->visitors as $visitor) {
// ...
}
}
}
Do you have your favorite number? Good, try to remember it.
The memory-lock is very difficult to spot. We owe that to author blindness and thinking heuristics (brain short-cuts), that limits our ability to work effectively in the chaos or under pressure. If you've read Thinking, Fast and Slow (must have for anyone curious about his or her brain false positives) or any other book about social psychology, you know where I'm heading.
Why it's difficult to spot? It depends on other code and it might code work if our expectations are met. What does it mean?
Let's take code snippet 7. Will this work or not? Under what condition?
<?php
foreach ($this->visitors as $visitor) {
// ...
}
This pull-request reveals the answer.
Now back to your coding:
I don't. I want to be effective and create a valuable bullet-proof code that can be used only one way. A code that doesn't put the burden on the developer to investigate my code first. A code that is safe to use and cannot be broken (if possible).
So which of those 7 code snippets above are dangerous?
Drum rolls...
Yes, all of them! I knew you'd reveal my poor confusion.
I'm taking the title from my favorite intro UX book because it really punches the line. There are 3 groups of memory-locks that could be done better in the code snippets above.
Snippets 1 and 2.
This code would change the $item['content
] value, but $this->publishingItems
remains unchanged:
<?php
// ...
foreach ($this->publishingItems as $item) {
$item['content'] = $this->decorateContent($item['content']);
}
<?php
// ...
-foreach ($this->publishingItems as $item) {
+foreach ($this->publishingItems as $key => $item) {
$item['content'] = $this->decorateContent($item['content']);
+ $this->publishingItems[$key] = $item;
}
Use object instead:
<?php
// ...
foreach ($this->publishingItems as $item) {
- $item['content'] = $this->decorateContent($item['content']);
+ $item->changeContent($this->decorateContent($item['content']));
}
Objects consume less memory anyway and you are safe - more importantly, anyone extending this code ever after is safer.
Snippets 3 and 5.
You also have to think about the C - the order:
$this->bookGenerator->setBookDirectory($bookDirectory);
$this->bookGenerator->generate();
vs.
$this->bookGenerator->generate();
$this->bookGenerator->setBookDirectory($bookDirectory);
vs.
$this->bookGenerator->generate();
Use one method that handles it both:
<?php
// ...
-$this->bookGenerator->setBookDirectory($bookDirectory);
-$this->bookGenerator->generate();
+$this->bookGenerator->generateFromDirectory($bookDirectory);
Same applied to code snippet 5:
<?php
// ...
-$this->entityManager->persist($product);
-$this->entityManager->flush();
+$this->productRepository->save($product);
I dare you to generate the book or save the product the wrong way now!
Snippets 4, 6 and 7.
This would actually fail:
<?php
foreach ($this->visitors as $visitor) {
// ...
}
Why? Because in parent::__construct()
is set default value for property with null:
<?php
// ...
public function __construct()
{
$this->visitors = [];
}
In this case just add default value to property itself:
-private $visitors;
+private $visitors = [];
There is even coding standard fixer for that.
Also, do not add any logic to constructor apart dependency injection. Constructor injection is the main reason to use the constructor in 99 %, so most people probably don't expect any extra logic there. Create extra method instead or decouple a class, put it into the constructor and call the method on it.
Try to avoid these patterns in code, in door design or in architecture of the application as a whole. One day some programmer will silently thank you when he or she will find what memory lock is (the painful way).
Do you know any other memory locks we should watch out for? Share them in the comment!
"90% of the bugs I produced were for one of the two reasons:
— Programming Wisdom (@CodeWisdom) 20. května 2018
1. Doing multiple things at one place
2. Doing one thing at multiple places" - @pseudo_coder
Happy brain cells liberation!
Do you learn from my contents or use open-souce packages like Rector every day?
Consider supporting it on GitHub Sponsors.
I'd really appreciate it!