Refactoring Conditional Structures with Map

dans clean code, java, refactoring par Nadia Humbert-Labeaumaz

When working on already existing codebases, I often encounter pieces of code that look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Day {
  public void start(Weather weather) {
    switch(weather) {
      case RAINY:
          takeAnUmbrella();
          break;
      case SUNNY:
          takeAHat();
          break;
      case STORMY:
          stayHome();
          break;
      default:
          doNothing();
          break;
    }
  }
}

Basically, depending on the weather, an action has to be taken. This kind of code is pretty hard to test and to maintain. The goal of this article is to refactor it using a Map.

What is the Problem?

Using conditional structures like this might be a sign of bad design. Indeed, this code tends to grow indefinitely as new cases have to be handled and the same code has to be modified over and over. A time will come when the code will be so bloated that it will be very hard to add new behaviour. This is a violation of the Open Closed Principle which stipulates that the code should be open for extension but closed for modification: you should be able to add new behaviour to your code without modifying it.

Transform the Imperative Algorithm into Data

By analyzing this code, it becomes clear that this algorithm is no more than a Map: for a certain weather (the key), a piece of code has to be executed (the value). A first refactoring can be done to make this conceptual Map concrete:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Day {

  private final Map<Weather, Runnable> startOfTheDayActions = new HashMap<>();

  public Day() {
    startOfTheDayActions.put(Weather.RAINY, this::takeAnUmbrella);
    startOfTheDayActions.put(Weather.SUNNY, this::takeAHat);
    startOfTheDayActions.put(Weather.STORMY, this::stayHome);
  }

  public void start(Weather weather) {
    startOfTheDayActions.getOrDefault(weather, this::doNothing).run();
  }
}

This is a first step and the code is already much clearer. Now, the mapping between the weather and the action to perform is explicit and the start method will not have to be modified very often. When a new case must be handled, it’s just a new entry in the Map.

Nonetheless, this do not solve all problems. The class still has to be modified to add a new entry. To go further, the Map can be passed as a parameter of the constructor.

1
2
3
4
5
6
7
8
9
10
11
12
public class Day {

  private final Map<Weather, Runnable> startOfTheDayActions = new HashMap<>();

  public Day(Map<Weather, Runnable> startOfTheDayActions) {
    this.startOfTheDayActions = startOfTheDayActions;
  }

  public void start(Weather weather) {
    startOfTheDayActions.getOrDefault(weather, this::doNothing).run();
  }
}

Now the only responsibility of the class is to use the mapping to perform the correct action. This mapping is now the responsibility of another class.

Note about Spring Framework

If you are using Spring Framework and the Day class is a @Component, you can simply inject the Map as any other dependency.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class Day {

  private final Map<Weather, Runnable> startOfTheDayActions = new HashMap<>();

  public Day(@Qualifier("startOfTheDayActions") Map<Weather, Runnable> startOfTheDayActions) {
    this.startOfTheDayActions = startOfTheDayActions;
  }

  public void start(Weather weather) {
    startOfTheDayActions.getOrDefault(weather, this::doNothing).run();
  }
}
1
2
3
4
5
6
7
8
9
10
@Configuration
public class ActionConfig {

  @Bean("startOfTheDayActions")
  public Map<Weather, Runnable> startOfTheDayActions() {
    Map<Weather, Runnable> actions = new HashMap<>();
    // Create mapping
    return actions;
  }
}

Conclusion

This refactoring is very easy to do but it can reduce the complexity of a method in a very efficient way. I think that the code should reveal intention and should not be bloated with conditional structures when it is not necessary.

Nadia Humbert-Labeaumaz

Nadia est coordinatrice d'équipes et de projets. Elle accompagne les entreprises à apporter en continu de la valeur métier et de la qualité au travail produit.

Contactez-nous

Crafties réalise des audits de code, des formations, des accompagnements et des développements. Vous pouvez nous contacter à contact@crafties.fr pour tout besoin.