I had a good question from one of our developers this morning about switch statements. He observed that he didn’t see them too often, and was wondering what the reason for this was and if there were any best practices concerning switch statements. This is a great question and something I see come up from time to time.
There is not necessarily anything wrong with switch statements, but logic in switches can get complicated / hard to follow and functions with switches can easily end up doing too many different things or become difficult to quickly understand/debug. In most cases, there’s a better tool for job. This depends on what the code is actually doing, but it’s often the case that the switch can be replaced with a data structure that actually models what you’re doing or be replaced via polymorphism. Consider the following example class using a couple switches:
class BaseballPlayer
{
protected $number;
protected $position;
public function __construct($number, $position)
{
$this->number = $number;
$this->position = $position;
}
public function getName()
{
switch ($this->number) {
case 22:
return 'Robinson Cano';
case 23:
return 'Nelson Cruz';
case 34:
return 'Felix Hernandez';
default:
throw new \Exception('Unknown player');
}
}
public function playBall()
{
switch ($this->position) {
case 'Infield':
$this->stepToBase();
$this->waitForHit();
$this->fieldBall();
break;
case 'DH':
$this->stepToPlate();
$this->hitHomeRun();
break;
case 'Pitcher':
$this->stepToMound();
$this->throwStrike();
break;
default:
throw new \Exception('Not sure how to play position');
}
}
}
Let’s take a look at getName()
. What we’re really doing here is mapping numbers to player names. There’s a perfect data structure for this: the map (aka associative array, dictionary, hash, etc.). We can refactor this using a map, which more clearly expresses the intent of what we’re doing and better models the actual problem at hand.
public function getName()
{
$players = [
22 => 'Robinson Cano',
23 => 'Nelson Cruz',
34 => 'Felix Hernandez',
];
if (!isset($players[$this->number])) {
throw new \Exception('Unknown player');
}
return $players[$this->number];
}
We’re returning the exact same thing, but just more accurately expressing what we’re doing. There’s no branches to follow, we’re simply looking up data in our map. This also prevents us from adding more code that we shouldn’t be adding (e.g. additional logic in some of the cases that more properly belongs somewhere else.
Let’s now consider playBall()
. Unlike getName()
, this isn’t simply a map; we’re actually performing different logic and multiple steps based on the variable we’re switching on. In this case, polymorphism is the perfect solution. We’ll create subclasses for each case we need to handle, them override the method in these subclasses with the logic specific to that type.
class Infielder extends BaseballPlayer
{
public function playBall()
{
$this->stepToBase();
$this->waitForHit();
$this->fieldBall();
}
}
class DH extends BaseballPlayer
{
public function playBall()
{
$this->stepToPlate();
$this->hitHomeRun();
}
}
class Pitcher extends BaseballPlayer
{
public function playBall()
{
$this->stepToMound();
$this->throwStrike();
}
}
These methods now do the one thing they’re supposed to do instead of having complicated logic/branches to follow. If we want to know how a pitcher plays ball, we simply have to look at the playBall()
method. This makes things easier to test/reuse. Additionally, easier to make future changes. If we need to modify this logic, we know exactly where to do it. If we need to add additional logic specific to a Player type (i.e. another method beyond playBall), we now have a specific spot to do so. In this contrived example the benefits may not seem super obvious, but in real world code this makes a huge difference.
If a switch doesn’t fall in one of these cases, take some time to think about the switch: Why do I need this? Do I need so many different branches here or is there a way I could simplify the logic?
In conclusion, there’s nothing inherently wrong with switches, but in the vast majority of time switches come up, there is a better tool for the job and therefore, as a best practice, switches should be avoided. Remember that best practice !=
hard rule; always think critically about what you are doing and consider all options, but approach the problem with the mindset “switch most likely isn’t the right thing to use”.