Tiny tactics for big wins

2023-06-09

Rumble in the jungle

Ali was supposed to lose when he fought Foreman in 1974.
The oddsmakers had Ali as a 4-1 underdog because Foreman was undefeated,
hit like a truck and had recently beaten Ali's rival Joe Frazier. But Ali wasn't coming to lose, he had a strategy, he was going to tire Foreman out and beat him late in the fight.
He prepared tactics to help tire out Foreman, he was going to lean on him and use rope-a-dope to let Foreman punch himself out, and as history shows, it worked.
Ali knocked out an exhausted Foreman in the eighth round.

Strategy vs Tactics

There's a difference between strategies and tactics, strategies are high level and focus on a long term goal while tactics are low level and can be used to make various strategies succeed. Most coding blogs that write about improving code quality are focused on strategies, like the SOLID principles, Dumb components or Strategy pattern itself for example.

Strategies provide great value but are tough to implement in an existing codebase.
To get the full value from a strategy you want the entire codebase to follow it, and making a large codebase consisting of different patterns follow a strategy is not easy.
There's also the task of convincing your team and management that implementing the strategy is worth the time, and depending on your team that may be an even bigger task.

Tactics provide value straight away.
You don't need to explain anything, you don't need to convince anyone and you don't need to make sure other parts of the code use the same tactics. You can get fast wins without taking on a big rewrite.

Two of my favorite tactics

1. Early returns

Early returns is the code version of "its better to ask for forgiveness than to ask for permission".
Early returners don't ask for permission to do something, they just do it unless stopped. It's the opposite of most peoples "If i have permission to do X, i will do X"

// Default thinking
if ($condition) {
	doThing();
}

Early returning reverses that logic into "if nothing stops me, I will do X".

// Early return thinking
if (! $condition) {
	return;
}
doThing();

Hard to see any benefit here, let's move on to bigger examples.

// Default thinking
if ($user->sober() && $user->legalDrivingAge()) {
	$user->drive();
}
// Early return thinking
if (!$user->sober() || !$user->legalDrivingAge()) {
	return;
}

$user->drive();

Still, no benefit in readability or simplicity, but there's one small important difference here.
We're using || instead of && operators, and thanks to that we can break out our if statement into two separate statements.

if (! $user->sober()) {
	return;
}

if (! $user->legalDrivingAge()) {
	return;
}

$user->drive();

Now we're getting somewhere, some of the benefits are already showing here, but it's in code with multiple levels of indentation that early returns really shine.

// Default thinking
public function sendUnsentEmails()
{
	$user = auth()->user();
	if ($user) {
		$emails = $user->getEmails();
		if ($emails) {
			foreach ($emails as $email) {
				if ($email->isNotSent()) {
					$email->send();
				}
			}
		}
	}
}
// Early returns thinking
public function sendUnsentEmails()
{
	$user = auth()->user();
	if (! $user) {
		return;
	}

	if (! $emails) {
		return;
	}

	foreach ($emails as $email) {
		if ($email->isSent()) {
			continue;
		}
		$email->send();
	}
}

Here, even if you took a few steps back from your monitor you would see that the early return style looks better.
Summarizing benefits of early returns

  1. Decreases indentation
  2. Easier to read and see what the code does
  3. Provides the reader with early outs. If you hit a return you can stop reading there because no more code is being executed.
  4. Easier to debug which if statement is failing.

2. Helper functions

Functions are cheap, you should use them any way you can to make the code more readable. One way to do this is to move if statements into a function and give that function a name explaining what you're checking.

Here's an example of how that could look in steps

Step 1. Terrible

$user = auth()->user();
if (
		$user->ageAt('2024-11-05') > 18 &&
		$user->country === 'USA' &&
		($user->registered || $user->inNorthDakota())
	) {
		// do something
	}

Step 2. Better


$user = auth()->user();

if ($user->ageAt('2024-11-05') < 18) {
	return false;
}

if ($user->country !== 'USA') {
	return false;
}

if (! $user->registered || $user->inNorthDakota()) {
	return false;
}

// do something ...

Reading these statements it's not impossible to guess what it does,
but that's what you're doing, guessing, and thinking.
You shouldn't have to think to figure out what this code does.

Step 3. Good


// in User.php
public function canVote()
{
	if ($this->ageAt('2024-11-05') < 18) {
		return false;
	}

	if ($this->country !== 'USA') {
		return false;
	}

	if (! $this->registered || $this->inNorthDakota()) {
		return false;
	}

	return true;
}

// Other class
$user = auth()->user();


if (! $user->canVote()) {
	return false;
}

// do something ...

With this structure you never have to think, only read.

Now, how about you try writing some of these tactics and let me know how it worked out for you.