Spis treści

Jak uniknąć switcha w Springu

W Javie często spotykamy sytuację, w której potrzebujemy obsłużyć różne typy obiektów.

Sytuacja

Wyobraźmy sobie sytuację, w której chcemy obsłużyć różne typy tokenów:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public enum TokenType {
    FOO, BAR
}

public interface TokenStrategy {
    TokenType getTokenType();
    void handleToken();
}

public class FooTokenStrategy implements TokenStrategy {
    @Override
    public TokenType getTokenType() {
        return TokenType.FOO;
    }

    @Override
    public void handleToken() {
        // ...
    }
}

public class BarTokenStrategy implements TokenStrategy {
    @Override
    public TokenType getTokenType() {
        return TokenType.BAR;
    }

    @Override
    public void handleToken() {
        // ...
    }
}

@Configuration
class TokenStrategyConfig {
    @Bean
    FooTokenStrategy fooTokenStrategy() {
        return new FooTokenStrategy();
    }

    @Bean
    BarTokenStrategy barTokenStrategy() {
        return new BarTokenStrategy();
    }
}

Proste rozwiązanie: instrukcja switch

Najprostszym rozwianiem jest zastosowanie instrukcji switch, która pozwala zróżnicować zachowanie na podstawie wartości enuma TokenType:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class TokenHandler {
    private final FooTokenStrategy fooTokenStrategy;
    private final BarTokenStrategy barTokenStrategy;

    public TokenHandler(FooTokenStrategy fooTokenStrategy, BarTokenStrategy barTokenStrategy) {
        this.fooTokenStrategy = fooTokenStrategy;
        this.barTokenStrategy = barTokenStrategy;
    }

    void handleToken(TokenType tokenType) {
        switch (tokenType) {
            case FOO -> fooTokenStrategy.handleToken();
            case BAR -> barTokenStrategy.handleToken();
        }
    }
}

@Configuration
@Profile("switch")
class TokenConfig {
    @Bean
    TokenHandler tokenHandler(FooTokenStrategy fooTokenStrategy, BarTokenStrategy barTokenStrategy) {
        return new TokenHandler(fooTokenStrategy, barTokenStrategy);
    }
}

Problem ze switch-em

To podejście nie jest najlepsze. Łamiemy Open-closed principle, ponieważ dla każdego nowego typu tokenu konieczne jest zmodyfikowanie metody TokenHandler#handleToken.

Pierwsze rozwiązanie

Aby tego uniknąć możemy użyć polimorfizmu:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class TokenHandler {
    private final Map<TokenType, TokenStrategy> strategies;

    public TokenHandler(Map<TokenType, TokenStrategy> strategies) {
        this.strategies = strategies;
    }

    void handleToken(TokenType tokenType) {
        strategies.get(tokenType).handleToken();
    }
}

@Configuration
@Profile("map")
class TokenConfig {
    @Bean
    TokenHandler tokenHandler(FooTokenStrategy fooTokenStrategy, BarTokenStrategy barTokenStrategy) {
        Map<TokenType, TokenStrategy> strategies = new HashMap<>();
        strategies.put(TokenType.FOO, fooTokenStrategy);
        strategies.put(TokenType.BAR, barTokenStrategy);
        return new TokenHandler(strategies);
    }
}

Dzięki zmianom do obsługi nowego tokena wystarczy wprowadzić nowy typ rozszerzający TokenStrategy, natomiast nie jest już konieczna modyfikacja istniejącego kodu zawierającego instrukcje switch.

Ulepszenie

Jest to w pełni akceptowalne rozwiązanie, jednak dalej trzeba ręcznie wstawiać nową strategię w metodzie tworzącej beana TokenConfig#tokenHandler. Co ciekawe da się to zrobić automatycznie z użyciem Springa:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class TokenHandler {
    private final Map<TokenType, TokenStrategy> strategies;

    public TokenHandler(Map<TokenType, TokenStrategy> strategies) {
        this.strategies = strategies;
    }

    void handleToken(TokenType tokenType) {
        strategies.get(tokenType).handleToken();
    }
}

@Configuration
@Profile("autowired")
class TokenConfig {
    @Autowired
    List<TokenStrategy> strategies;

    @Bean
    TokenHandler tokenHandler() {
        Map<TokenType, TokenStrategy> strategyMap = new HashMap<>();
        for (TokenStrategy strategy : strategies) {
            strategyMap.put(strategy.getTokenType(), strategy);
        }
        return new TokenHandler(strategyMap);
    }
}

Kod

Kod dostępny jest na Bitbucket: https://bitbucket.org/tusinski/how-to-avoid-switch-statement-in-spring