Switch your switch statements for something better

fish in bowl
It's only fair to share...Share on Google+0Share on Facebook0Tweet about this on TwitterShare on LinkedIn0Email this to someone

In the spirit of Dijkstra’s seminal work, let me dissect the bold statement given in the title, i.e. the switch statement. More often than not, I found switch statements in the code bases I work with to be a sub-optimal solution, and as usual, this is nothing new. Robert Martin already wrote about the detriments of it in his Clean Code book, but I found that a lot of people still don’t know about this, so I thought it might not hurt to reiterate.

Pet shop example

Before we start the discussion, let’s get an example running: let’s implement a pet shop. With pets. And cages. And of course we get new ones all the time.

Let’s start with a few basic cages and animals:

sealed trait Cage
object NormalCage extends Cage
object Aviary extends Cage

sealed trait Animal
object Dog extends Animal
object Cat extends Animal
object Bird extends Animal

You may feel the need to create an enum for these, but we’re going to extend these classes soon and adding behaviors to enums is a whole other topic, so let’s just stick with this for now.

The task ahead of us is to write a function to determine whether a given cage is suitable for a given animal. After all, aviaries are reserved for birds. For the sake of argument, the following implementation is based on “switch” statements, or more precisely, on pattern matching. There is a fine line being crossed here: pattern matching is way more powerful than switch statements, but as it is used here, it simply does the same thing.

def isSuitableCage(cage : Cage, animal : Animal) : Boolean = cage match {
  case NormalCage =>
    animal match {
      case Bird => false
      case _ => true
    }
  case Aviary =>
    animal match {
      case Bird => true
      case _ => false
    }
}<

If you’re unfamiliar with Scala, the “case _” is just defining a default case for the switch.

Given this method, we can now write a simple method to check our shop’s available cages for a suitable one when an animal is added:

def findSuitableCage(animal : Animal, availableCages : Seq[Cage]) : Option[Cage] =
  availableCages.find(isSuitableCage(_, animal))

Everything’s working and pretty straightforward so far, so why bother?

The problems

As everyone who has ever worked with switch statements should know, we have two essential problems to deal with:

  1. Forgetting a case
  2. Behavior when extending whatever we switched over

Gladly, number 1 is taken care of by most IDEs in most languages these days. In Scala, the keyword sealed ensures that the compiler will warn you that you forgot a case. It works in many more cases than in Java when you missed an enum value, since it includes support for pattern matching, but either way, you should be able to deal with number 1 rather easily.

The second problem, however, is what bites me over and over. Every time, someone extends an enum and adds a value, there is bound to be a switch statement in some obscure place that acts kind of strange now. To see why this happens, let’s start extending the example program.

How about, we extend our pet shop and now include fish. Of course, traditional cages won’t do it and we need a proper bowl filled with water for our fish to sit in. So we add the following lines:

object Bowl extends Cage
object Fish extends Animal

We immediately run into problem 1 as the compiler complains, that our switch for the cages did not include a case for the bowl. Not a problem at all – let’s just add the obvious case:

    case Bowl =>
      animal match {
        case Fish => true
        case _ => false
      }

Easy. Now everything compiles and we’re happy. That wasn’t so hard now, or was it?

Well, let’s just say you shouldn’t be surprised if your fish dies in production, because it was placed in a normal cage. This is a typical error that surfaces lateron, but is based on our chosen switch statement approach. Simply said, switch statements are poison for the evolutional capabilities and correctness of your software.

An alternative solution

Just to be clear: there are oh so many ways to solve this dilemma and get rid of your switch statements. If you cannot think of at least a few, you really should push yourself to try out different design ideas more often.

For this post, I’ll try to fix this problem directly in the type system. What I did to come up with this solution was to put some more thoughts into what it is that makes a cage suitable for an animal. The switch statement is a very direct and simple way that is easy to come up with (which probably is the reason why it is so widespread), but it doesn’t in any way capture why an aviary is suitable for birds really. What is it that makes the normal cage unsuitable for the newly added fish?

Let’s start the same pet shop all over. From the beginning, where we have only dogs, cats and birds in their respective cages, but this time, we also encode the animals’ style of living:

sealed trait LivingStyle
trait LivesOnGround extends LivingStyle
trait Flies extends LivingStyle

sealed trait Animal { self : LivingStyle => }
object Dog extends Animal with LivesOnGround
object Cat extends Animal with LivesOnGround
object Bird extends Animal with Flies

sealed trait Cage {
  def isSuitableFor(animal : Animal) : Boolean
}
object NormalCage extends Cage {
  override def isSuitableFor(animal : Animal) = animal.isInstanceOf[LivesOnGround]
}
object Aviary extends Cage {
  override def isSuitableFor(animal : Animal) = animal.isInstanceOf[Flies]
}

def findSuitableCage(animal : Animal, availableCages : Seq[Cage]) : Option[Cage] =
  availableCages.find(_.isSuitableFor(animal))

We can see that the cages look like they need slightly more code, but really, they just include the suitability check already, so we don’t need an extra function. As you can see, we can already find a suitable cage for our new animals just fine this way.

So far, it’s just code that does the same as the other code before. So let’s see how things change, when we now start to add our fish and its bowl. Let’s go really slowly here, so you can appreciate the power of such an approach:

object Fish extends Animal

This won’t work. The compiler tells us:

Error:(34, 23) illegal inheritance;
 self-type PetShop.Fish.type does not conform to PetShop.Animal's selftype PetShop.Animal with PetShop.LivingStyle
  object Fish extends Animal
                      ^

This is telling us that we ought to think about the style of living of our new animal. When we look at the existing LivingStyle options available, we realize that a fish doesn’t really match these well. So let’s add a new style of living for our fish:

trait Swims extends LivingStyle
object Fish extends Animal with Swims

It compiles again. Awesome. Well we don’t have a cage for the fish added yet, but will it matter? Not really, as none of the other cages will accept a fish now. Unlike the first approach, the normal cage expects a ground living animal, which our fish isn’t. So no harm done and we proceed to add the bowl:

object Bowl extends Cage

Oh right, the compiler reminds us that we need to provide an implementation of the isSuitableFor method to determine which animals a bowl can hold:

object Bowl extends Cage {
  override def isSuitableFor(animal : Animal) = animal.isInstanceOf[Swims]
}

Aaaaaaand.. it’s done. At every step of this process, the type system supported us such that we could not forget anything. Problem 1 of the switch statement just never had a chance. As for problem 2, we did change the behavior, as we can now place a fish inside a bowl. But the important point to note here is that none of the previously existing code was touched. Every single line of the variant without the fish and bowl remained just as before and works for the new cases as well.

As mentioned above, this is by far not the only solution, nor the best, but it easily surpasses the switch-based approach when it comes to extending it and correctness. And did you notice? We can even have turtles now:

object Turtle extends Animal with LivesOnGround with Swims

Summary

Please think again, before writing another switch statement. And if you encounter one in the wild, ask yourself: how would you change the design to make the whole switch statement unnecessary. Yes, it does take more time and effort, but only until you take the problems caused by switch statements into account.

[The feature image is CC-BY-SA 2.0 by Praveen Gupta via flickr.com]

It's only fair to share...Share on Google+0Share on Facebook0Tweet about this on TwitterShare on LinkedIn0Email this to someone