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