How to beautifully get rid of switch-case through enumeration

So, directly case: The value of one variable determines the value of another variable.

Initial data: a program that simulates a zoo. It contains several animals represented as an enumeration. animaland a couple of workers described by the interface ZooWorker.

public enum Animal {
	HIPPO,
	PENGUIN,
	MONKEY,
	OWL;
}
public interface ZooWorker {
    void feed(Animal animal);
}

A task: teach workers how to feed animals (essentially implement the interface ZooWorker). The action algorithm is very simple – you need to determine the name of the food that the animal eats, and display a message in the console that the animal is fed and what exactly it is fed.

The first option is written in java 11. It is the most cumbersome and looks like this:

public class Java11Worker implements ZooWorker {

    @Override
    public void feed(Animal animal) {
        String foodName;
        switch (animal) {
            case OWL:
                foodName = "Mouse";
                break;
            case HIPPO:
                foodName = "Grass";
                break;
            case PENGUIN:
                foodName = "Fish";
                break;
            case MONKEY:
                foodName = "Banana";
                break;
            default:
                throw new IllegalArgumentException("Unknown animal!");
        }
        System.out.printf("%s eat: %s%n", animal, foodName);
    }
}

This solution has several problems:

  1. If an animal is added to the enumeration, the above code must also be added.

  2. No one will remind the developer that this needs to be done. That is, if the zoo grows to hundreds of thousands of lines, it is quite possible to forget that when adding an animal to the enumeration, you also need to add code. In the end, this will lead to an error (and it’s good if the default behavior is defined, in this case, at least it is possible to quickly determine the problem area).

  3. With a large number of animals, the switch-case will grow strongly.

  4. Well, the well-known switch-case problem in java 11 is an infinite break, which is easy to miss.

It is possible to slightly refactor the above example and get rid of problem #4 as follows:

public class Java11Worker implements ZooWorker {

    @Override
    public void feed(Animal animal) {
        String foodName = getFoodName(animal);
        System.out.printf("%s eat: %s%n", animal, foodName);
    }

    private String getFoodName(Animal animal) {
        switch (animal) {
            case OWL:
                return  "Mouse";
            case HIPPO:
                return "Grass";
            case PENGUIN:
                return "Fish";
            case MONKEY:
                return "Banana";
            default:
                throw new IllegalArgumentException("Unknown animal!");
        }
    }
}

Looks better, however, other problems remain.

Starting with java 14 and above, it became possible to use a more convenient switch-case format. The above solution in the new format would look like this:

public class Java17Worker implements ZooWorker {

    @Override
    public void feed(Animal animal, int animalCount) {
        String foodName = switch (animal) {
            case OWL -> "Mouse";
            case HIPPO -> "Grass";
            case PENGUIN -> "Fish";
            case MONKEY -> "Banana";
        };
        System.out.printf("%s eat: %s%n", animal, foodName);
    }
}

In addition to getting rid of break, problem No. 2 was solved: if the developer adds an animal to the enumeration, but does not define the necessary behavior in the switch-case, the code simply will not compile. Already better, however, the third and first problems still remain.

In the last solution, we will transfer the dependence of the variables directly to the enum. To do this, let’s change it a bit:

public enum Animal {
  
    HIPPO("Grass"),
    PENGUIN("Fish"),
    MONKEY("Banana"),
    OWL("Mouse");

    @Getter
    private final String foodName;

    Animal(String foodName) {
        this.foodName = foodName;
    }
}

Now you can implement a worker in just one line:

public class EasyWorker implements ZooWorker {
  
    @Override
    public void feed(Animal animal) {
        System.out.printf("%s eat: %s%n", animal, animal.getFoodName());
    }
}

In the presented solution, when adding an element to the enumeration, there is no need to add code, and the developer will definitely not forget to add anything anywhere. In addition, the code will not turn into noodles when the number of elements in the enumeration increases.

Let’s try to expand and complicate our case: now not only the value of another, but also the subsequent algorithm of actions depends on the value of one variable.

A task remains the same – to teach workers how to feed the animals. However, now in order to feed an animal, it is necessary not only to determine the required feed, but also to calculate its volume. To do this, we will change the interface ZooWorker:

public interface ZooWorker {
    void feed(Animal animal, int animalCount);
}

For greater clarity, let’s imagine that the calculation of feed is not always carried out by simple multiplication by a coefficient, but a certain formula is used:

  • For hippos: [количество бегемотов^2]

  • For filins: [количество филинов * 3]

  • For penguins: [(количество пингвинов ^ 3)/2]

  • For monkeys: [количество обезьян * 10]

Below are solutions for already used templates:

switch-case solution on java 11
public class Java11Worker implements ZooWorker {

    @Override
    public void feed(Animal animal, int animalCount) {
        String foodName;
        int foodQuantity;
        switch (animal) {
            case OWL:
                foodName = "Mouse";
                foodQuantity = animalCount * 3;
                break;
            case HIPPO:
                foodName = "Grass";
                foodQuantity = (int) Math.pow(animalCount, 2);
                break;
            case MONKEY:
                foodName = "Banana";
                foodQuantity = animalCount * 10;
                break;
            case PENGUIN:
                foodName = "Fish";
                foodQuantity = (int) (Math.pow(animalCount, 3)/2);
                break;
            default:
                throw new IllegalArgumentException("Unknown animal!");
        }
        System.out.printf("%s eat: %d %s", animal, foodQuantity, foodName);
    }
}

The slightly revised code would look like this:

public class Java11Worker implements ZooWorker {

    @Override
    public void feed(Animal animal, int animalCount) {
        String foodName = getFoodName(animal);
        int foodQuantity = getfoodQuantity(animal, animalCount);
        System.out.printf("%s eat: %d %s", animal, foodQuantity, foodName);
    }

    private String getFoodName(Animal animal) {
        switch (animal) {
            case OWL:
                return  "Mouse";
            case HIPPO:
                return "Grass";
            case MONKEY:
                return "Banana";
            case PENGUIN:
                return "Fish";
            default:
                throw new IllegalArgumentException("Unknown animal!");
        }
    }

    private int getfoodQuantity(Animal animal, int animalCount) {
        switch (animal) {
            case OWL:
                return animalCount * 3;
            case HIPPO:
                return (int) Math.pow(animalCount, 2);
            case MONKEY:
                return animalCount * 10;
            case PENGUIN:
                return (int) (Math.pow(animalCount, 3)/2);
            default:
                throw new IllegalArgumentException("Unknown animal!");
        }
    }
}
switch-case solution on java 17
public class Java17Worker implements ZooWorker {

    @Override
    public void feed(Animal animal, int animalCount) {
        String foodName = switch (animal) {
            case OWL -> "Mouse";
            case HIPPO -> "Grass";
            case PENGUIN -> "Fish";
            case MONKEY -> "Banana";
        };
        int foodQuantity = switch (animal) {
            case OWL -> animalCount * 3;
            case HIPPO -> (int) Math.pow(animalCount, 2);
            case PENGUIN -> (int) (Math.pow(animalCount, 3) / 2);
            case MONKEY -> animalCount * 10;
        };
        System.out.printf("%s eat: %d %s", animal, foodQuantity, foodName);
    }
}

To solve via enum, you need to modify the enumeration:

public enum Animal {

    HIPPO("Grass", animalCount -> (int) Math.pow(animalCount, 2)),
    PENGUIN("Fish", animalCount -> (int) (Math.pow(animalCount, 3) / 2)),
    MONKEY("Banana", animalCount -> animalCount * 10),
    OWL("Mouse", animalCount -> animalCount * 3);

    @Getter
    private final String foodName;
    @Getter
    private final IntFunction<Integer> foodCalculation;

    Animal(String foodName, IntFunction<Integer> foodCalculation) {
        this.foodName = foodName;
        this.foodCalculation = foodCalculation;
    }
}

And, in fact, the worker himself:

public class EasyWorker implements ZooWorker {

    @Override
    public void feed(Animal animal, int animalCount) {
        System.out.printf("%s eat: %d %s", 
                          animal, 
                          animal.getFoodCalculation().apply(animalCount), 
                          animal.getFoodName()
        );
    }
}

Let’s move on to the final example. Suppose the feed calculation is a complex logic that cannot be passed as a lambda.

In this case, for each animal, we will create a separate employee who will feed only him.

Realized workers
public class HippoWorker implements ZooWorker {

    private final String foodName;

    public HippoWorker(String foodName) {
        this.foodName = foodName;
    }

    @Override
    public void feed(int animalCount) {
        //Сложная логика
        int foodQuantity = (int) Math.pow(animalCount, 2);
        System.out.printf("Hippo eat: %d %s", , foodQuantity, foodName);
    }
}
public class MonkeyWorker implements ZooWorker {

    private final String foodName;

    public MonkeyWorker(String foodName) {
        this.foodName = foodName;
    }

    @Override
    public void feed(int animalCount) {
        //Сложная логика
        int foodQuantity = animalCount * 10;
        System.out.printf("Monkey eat: %d %s", foodQuantity, foodName);
    }
}
public class OwlWorker implements ZooWorker {

    private final String foodName;

    public OwlWorker(String foodName) {
        this.foodName = foodName;
    }

    @Override
    public void feed(int animalCount) {
        //Сложная логика
        int foodQuantity = animalCount * 3;
        System.out.printf("Owl eat: %d %s", foodQuantity, foodName);
    }
}
public class PenguinWorker implements ZooWorker {

    private final String foodName;

    public PenguinWorker(String foodName) {
        this.foodName = foodName;
    }

    @Override
    public void feed(int animalCount) {
        //Сложная логика
        int foodQuantity = (int) (Math.pow(animalCount, 3) / 2);
        System.out.printf("Penguin eat: %d %s", foodQuantity, foodName);
    }
}

Let’s imagine how to solve the problem of “feeding all the animals” head-on: for example, in the calling class we collect a list (set) of all workers, and we get the opportunity to call the feed method on the list elements one by one. It would come out something like this:

public class Feeder {

    private final List<ZooWorker> workerList;

    public Feeder(List<ZooWorker> workerList) {
        this.workerList = workerList;
    }

    public void feedAll(int animalCount) {
        workerList.forEach(zooWorker -> zooWorker.feed(animalCount));
    }
}

Looks good, however, what if you need a separate method for each animal, for example, public void feedHippo(int animalCount)or universal public void feedAnimal(Animal animal, int animalCount)? There will be problems at this stage. The solution might be to create a map containing all workers. But then you need to store the keys to it (or hardcode). You can make the value of the enumeration the key directly, but you still have to collect the map somewhere. Another solution could be to implement workers as fields, but their [работников] there may be a lot, feedAnimal will again work on a bulky switch-case. And all these options need to be supported, and when adding a new animal, you will have to look for the code where the feeding logic works out.

However, if we change the enum like this:

public enum Animal {

    HIPPO(new HippoWorker("Grass")),
    PENGUIN(new PenguinWorker("Fish")),
    MONKEY(new MonkeyWorker("Banana")),
    OWL(new OwlWorker("Mouse"));

    private final ZooWorker worker;

    Animal(ZooWorker worker) {
        this.worker = worker;
    }

    public void feed(int animalCount) {
        worker.feed(animalCount);
    }
} 

Everything becomes so simple:

public class Feeder {
    
    public void feedAll(int animalCount) {
        Arrays.stream(Animal.values())
                .forEach(animal -> animal.feed(animalCount));
    }

    public void feedHippo(int animalCount) {
        Animal.HIPPO.feed(animalCount);
    }

    public void feedAnimal(Animal animal, int animalCount) {
        animal.feed(animalCount);
    }
}

Let’s summarize.

We looked at 3 options with increasing complexity, where you can beautifully apply enumeration instead of switch-case. The proposed solutions to the problems are easy to implement and more maintainable and extensible compared to the head-on solution using switch-case.

Similar Posts

Leave a Reply