Перейти к основному содержимому

Шаблон проектирования стратегии в Java 8

· 4 мин. чтения

1. Введение

В этой статье мы рассмотрим, как мы можем реализовать шаблон проектирования стратегии в Java 8.

Сначала мы дадим обзор шаблона и объясним, как он традиционно реализовывался в старых версиях Java.

Затем мы снова попробуем этот шаблон, только на этот раз с лямбда-выражениями Java 8, что уменьшит многословие нашего кода.

2. Паттерн стратегии

По сути, шаблон стратегии позволяет нам изменять поведение алгоритма во время выполнения.

Как правило, мы начинаем с интерфейса, который используется для применения алгоритма, а затем реализуем его несколько раз для каждого возможного алгоритма.

Допустим, у нас есть требование применять различные типы скидок к покупке в зависимости от того, будет ли это Рождество, Пасха или Новый год. Во-первых, давайте создадим интерфейс дискаунтера , который будет реализован каждой из наших стратегий:

public interface Discounter {
BigDecimal applyDiscount(BigDecimal amount);
}

Предположим, мы хотим применить скидку 50 % на Пасху и скидку 10 % на Рождество. Давайте реализуем наш интерфейс для каждой из этих стратегий:

public static class EasterDiscounter implements Discounter {
@Override
public BigDecimal applyDiscount(final BigDecimal amount) {
return amount.multiply(BigDecimal.valueOf(0.5));
}
}

public static class ChristmasDiscounter implements Discounter {
@Override
public BigDecimal applyDiscount(final BigDecimal amount) {
return amount.multiply(BigDecimal.valueOf(0.9));
}
}

Наконец, давайте попробуем стратегию в тесте:

Discounter easterDiscounter = new EasterDiscounter();

BigDecimal discountedValue = easterDiscounter
.applyDiscount(BigDecimal.valueOf(100));

assertThat(discountedValue)
.isEqualByComparingTo(BigDecimal.valueOf(50));

Это работает довольно хорошо, но проблема в том, что создание конкретного класса для каждой стратегии может быть немного болезненным. Альтернативой может быть использование анонимных внутренних типов, но это все еще довольно многословно и не намного удобнее, чем предыдущее решение:

Discounter easterDiscounter = new Discounter() {
@Override
public BigDecimal applyDiscount(final BigDecimal amount) {
return amount.multiply(BigDecimal.valueOf(0.5));
}
};

3. Использование Java 8

С момента выпуска Java 8 введение лямбда-выражений сделало анонимные внутренние типы более или менее избыточными. Это означает, что создавать стратегии в очереди теперь намного чище и проще.

Кроме того, декларативный стиль функционального программирования позволяет нам реализовывать шаблоны, которые раньше были невозможны.

3.1. Уменьшение многословия кода

Давайте попробуем создать встроенный EasterDiscounter, только на этот раз с использованием лямбда-выражения:

Discounter easterDiscounter = amount -> amount.multiply(BigDecimal.valueOf(0.5));

Как мы видим, наш код стал намного чище и удобнее в сопровождении, достигая того же уровня, что и раньше, но в одной строке. По сути, лямбду можно рассматривать как замену анонимному внутреннему типу .

Это преимущество становится более очевидным, когда мы хотим объявить еще больше дискаунтеров в очереди:

List<Discounter> discounters = newArrayList(
amount -> amount.multiply(BigDecimal.valueOf(0.9)),
amount -> amount.multiply(BigDecimal.valueOf(0.8)),
amount -> amount.multiply(BigDecimal.valueOf(0.5))
);

Когда мы хотим определить множество дискаунтеров, мы можем объявить их всех статически в одном месте. Java 8 даже позволяет нам определять статические методы в интерфейсах, если мы этого хотим.

Поэтому вместо того, чтобы выбирать между конкретными классами или анонимными внутренними типами, давайте попробуем создать все лямбда-выражения в одном классе:

public interface Discounter {
BigDecimal applyDiscount(BigDecimal amount);

static Discounter christmasDiscounter() {
return amount -> amount.multiply(BigDecimal.valueOf(0.9));
}

static Discounter newYearDiscounter() {
return amount -> amount.multiply(BigDecimal.valueOf(0.8));
}

static Discounter easterDiscounter() {
return amount -> amount.multiply(BigDecimal.valueOf(0.5));
}
}

Как мы видим, мы достигаем многого в не очень большом коде.

3.2. Эффективная функциональная композиция

Давайте изменим наш интерфейс Discounter , чтобы он расширял интерфейс UnaryOperator , а затем добавим метод comb () :

public interface Discounter extends UnaryOperator<BigDecimal> {
default Discounter combine(Discounter after) {
return value -> after.apply(this.apply(value));
}
}

По сути, мы рефакторим наш Discounter и используем тот факт, что применение скидки — это функция, которая преобразует экземпляр BigDecimal в другой экземпляр BigDecimal , позволяя нам получить доступ к предопределенным методам . Поскольку UnaryOperator поставляется с методом apply() , мы можем просто заменить им applyDiscount .

Метод comb() — это всего лишь абстракция, связанная с применением одного дискаунтера к результатам этого. Для этого используется встроенный функционал apply() .

Теперь давайте попробуем применить несколько дискаунтеров кумулятивно к сумме. Мы сделаем это с помощью функционала reduce() и нашего Combine():

Discounter combinedDiscounter = discounters
.stream()
.reduce(v -> v, Discounter::combine);

combinedDiscounter.apply(...);

Обратите особое внимание на первый аргумент сокращения . Когда скидки не предоставляются, нам нужно вернуть неизменное значение. Этого можно достичь, предоставив функцию идентификации в качестве дискаунтера по умолчанию.

Это полезная и менее подробная альтернатива стандартной итерации. Если мы рассмотрим методы, которые мы получаем из коробки для функциональной композиции, это также дает нам гораздо больше возможностей бесплатно.

4. Вывод

В этой статье мы объяснили шаблон стратегии, а также продемонстрировали, как мы можем использовать лямбда-выражения для его реализации менее подробным способом.

Реализацию этих примеров можно найти на GitHub . Это проект на основе Maven, поэтому его легко запустить как есть.