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

1. Вступление

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

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

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

2. Шаблон стратегии

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

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

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

Во-первых, давайте создадим интерфейс Discounter , который будет реализован каждой из наших стратегий:

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));

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

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

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))
);

Когда мы хотим определить множество Discounters, мы можем объявить их статически в одном месте. 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 , а затем добавим метод combine () :

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

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

Метод combine () - это просто абстракция вокруг применения одного Discounter к результатам this. Для этого он использует встроенный функционал apply ()

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

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

combinedDiscounter.apply(...);

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

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

4. Заключение

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

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