В этой статье пойдёт речь о шаблоне проектирования, который позволит нам избавиться от больших switch-ей в наших проектах.
Представьте, что мы пишем нечто вроде калькулятора. У нас есть список операций. В нашем примере четыре элемента перечисления, но на самом деле их может быть очень много:
1 2 3 4 5 6 |
enum Operation { ADD, SUBTRACT, MULTIPLY, DIVIDE } |
И метод вычисления результата выражения с двумя операндами, реализующий разные варианты операций с помощью оператора switch:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
private double methodWithSwitch(Operation operation, double x, double y) { switch (operation) { case ADD: return x + x; case SUBTRACT: return x - y; case MULTIPLY: return x * y; case DIVIDE: return x / y; default: throw new IllegalArgumentException( "Invalid operation: " + operation); } } |
В данном случае выглядит неплохо, но различных видов операций может быть очень много. При дальнейшем добавлении новых значений в Operation наш switch может стать очень большим, и его будет очень сложно читать.
В этом случае имеет смысл применить шаблон проектирования isApplicable / apply. Он заключается в том, что мы пишем базовый интерфейс с методом isApplicable, который проверяет, будет ли этот экземпляр работать с нашей операцией, и методом apply, который будет осуществлять обработку в случае, если isApplicable вернёт true.
Создадим базовый интерфейс:
1 2 3 4 |
interface Calculator { boolean isApplicable(Operation operation); double apply(double x, double y); } |
У нас есть четыре операции, которые будут иметь следующие реализации:
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 46 47 48 49 50 51 |
class AddCalculator implements Calculator { @Override public boolean isApplicable(Operation operation) { return operation == Operation.ADD; } @Override public double apply(double x, double y) { return x + y; } } class SubtractCalculator implements Calculator { @Override public boolean isApplicable(Operation operation) { return operation == Operation.SUBTRACT; } @Override public double apply(double x, double y) { return x - y; } } class MultiplyCalculator implements Calculator { @Override public boolean isApplicable(Operation operation) { return operation == Operation.MULTIPLY; } @Override public double apply(double x, double y) { return x * y; } } class DivideCalculator implements Calculator { @Override public boolean isApplicable(Operation operation) { return operation == Operation.DIVIDE; } @Override public double apply(double x, double y) { return x / y; } } |
Пример использования:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
List<Calculator> calculators = List.of( new AddCalculator(), new SubtractCalculator(), new MultiplyCalculator(), new DivideCalculator()); private double methodWithIsApplicableApply(Operation operation, double x, double y) { return calculators.stream() .filter(calculator -> calculator.isApplicable(operation)) .reduce((firstCalculator, secondCalculator) -> { throw new IllegalStateException("Found several calculators."); }) .map(calculator -> calculator.apply(x, y)) .orElseThrow(() -> new IllegalArgumentException("operation " + operation + " is not supported.")); } |
Обратите внимание, что мы использовали способ создания неизменяемых коллекций, добавленный в Java 9.
Про Stream API и лямбда выражения читайте в моём учебнике.
Подобный шаблон проектирования полагается использовать в случаях, когда switch получается слишком большим и трудночитаемым. В качестве бонуса получаем возможность легкого добавления новых Calculator-ов, так как мы можем сделать их бинами Spring Framework, а в List через @Autowired получать сразу все существующие Calculator-ы.