The old switcheroo

One problem that has caused more bugs in previous software projects than I’d like to admit was the problem of incomplete if/then and switch blocks.

To illustrate this problem, let’s consider the following minimalistic Java classes:


public abstract class Animal {
	public abstract void callTo(String message);
}

public class Cat extends Animal {
	public void callTo(String message) {
		...
	}
}

public class Dog extends Animal {
	public void callTo(String message) {
		...
	}
}

Now let’s assume that somewhere else in the application, we want to do something based on the class of an object. It’s a one-off action, so it cannot be implemented as a member function. Let’s say we want to attract the animal’s attention.


Animal a = getRandomAnimal();
if (a instanceof Dog) {
	a.callTo("Here, boy!");
} else if (a instanceof Cat) {
	a.callTo("I have food!");
}

The problem that arises is of course inevitable: what happens when we introduce a new subclass of Animal, such as Hamster? In the above code snippet, the new class would simply be ignored, which is clearly not acceptable. But how can we ensure that all possible subclasses of Animal are covered, no matter how many we decide to add later?

Superficially, the following code might look like it provides a solution:


Animal a = getRandomAnimal();
if (a instanceof Dog) {
	a.callTo("Here, boy!");
} else if (a instanceof Cat) {
	a.callTo("I have food!");
} else {
	throw new Exception("Unknown animal type");
}

This is certainly a step forward in that the code recognises an unknown class and refuses to proceed. However, any code blocks that are not updated to include new subclasses will throw an exception during runtime, i.e. while the client is using the software. In any non-trivial code base, it is practically guaranteed that at least one such code block will be overlooked during testing, which means that the software has a hard-coded bug just waiting to happen.

What we need is a way to make sure that the code cannot be compiled until all code has been updated to include the new subclass. I call my solution the Switcher Pattern. The implementation of this pattern depends heavily on the language that the code is written in. In Java, I use anonymous classes.


public abstract class AnimalSwitcher {
	protected Animal _animal;
	public AnimalSwitcher (Animal A) {
		_animal = A;
		if (A instanceof Cat) {
			this.onCat();
		} else if (A instanceof Dog) {
			this.onDog();
		}
	}

	public abstract void onCat();
	public abstract void onDog();
}

Client code then uses the following syntax:


Animal A = getRandomAnimal();
AnimalSwitcher sw = new AnimalSwitcher(A) {
	public void onDog() {
		_animal.callTo("Here, boy!");
	}
	public void onCat() {
		_animal.callTo("I have food!");
	}
};

Let us now assume we want to introduce the new subclass Hamster. Once the new class has been added to AnimalSwitcher (including the abstract method onHamster), all anonymous implementations will refuse to compile until the newly created abstract method has been implemented.


Animal A = getRandomAnimal();
AnimalSwitcher sw = new AnimalSwitcher(A) {
	public void onDog() {
		_animal.callTo("Here, boy!");
	}
	public void onCat() {
		_animal.callTo("I have food!");
	}
	public void onHamster() {
		_animal.callTo("I have a nice running wheel!");
	}
};

Using this pattern, we only have to add the new subclass-based behaviour a single time, namely in the Switcher class. After that, we just have to diligently work our way through the error list generated by the compiler.

This pattern does not just apply when handling a complete set of sibling classes. It can also be used for subsets of sibling classes or even for entirely unrelated classes.


public abstract class Animal {}
public class Cat extends Animal {}
public class Dog extends Animal {}
public class Horse extends Animal {}
public class Cow extends Animal {}

public abstract class Furniture {}
public class Sofa extends Furniture {}

public abstract class PetSwitcher {
	public PetSwitcher (Animal A) {
		/* Implementation details omitted */
	}

	public abstract void onCat();
	public abstract void onDog();
}

public abstract class BarnyardAnimalSwitcher {
	public BarnyardAnimalSwitcher (Animal A) {
		/* Implementation details omitted */
	}

	public abstract void onHorse();
	public abstract void onCow();
}

public abstract class StuffInLivingRoomSwitcher {
	public StuffInLivingRoomSwitcher (Object A) {
		/* Implementation details omitted */
	}

	public abstract void onCat();
	public abstract void onSofa();
}

Naturally, the benefits of Switcher classes depend on well-written code, as even the best design can be destroyed by poor implementation.

Comments

Leave a Reply