Статьи

Упражнение по рефакторингу с использованием лямбда-выражений Java8

Наконец, Java с ее восьмым основным выпуском получит лямбда-выражения, а затем станет более функциональным. Вместе с Раулем-Габриэлем Урмой и Аланом Майкрофтом я начал писать книгу на эту тему. В любом случае простого использования лямбда-выражения недостаточно, чтобы утверждать, что вы занимаетесь функциональным программированием и, что более важно, использовать его возможности. Теперь Java поддерживает новую парадигму программирования, а затем люди, которые, как и я, занимались императивным объектно-ориентированным программированием на Java в течение десятка лет, должны сделать довольно большой сдвиг в уме, чтобы эффективно использовать эти новые методы. Например, в предыдущей статьеЯ предположил, что пришло время прекратить использовать нулевую ссылку или, по крайней мере, начать использовать ее более экономно, и заменить ее более безопасной и более явной конструкцией, такой как Option.  

На мой взгляд, хороший способ научиться использовать лямбда-выражения в работе более идиоматическим способом — это читать статьи, написанные на других функциональных языках, лучше Scala, поскольку они наиболее похожи на Java, и пытаться переопределить их в Java 8 образцы и решения, предложенные там. Это то, что я сделал с настоящим постом, который является просто переводом на Java статьи Scala, написанной Дебасишем Гошем.

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

public class SalaryCalculator {
  // B = basic + 20%
  public double plusAllowance(double d) {
  return d * 1.2;
  }

  // C = B + 10%
  public double plusBonus(double d) {
  return d * 1.1;
  }

  // D = C - 30%
  public double plusTax(double d) {
  return d * 0.7;
  }

  // E = D - 10%
  public double plusSurcharge(double d) {
  return d * 0.9;
  }
}

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

public double calculate(double basic, boolean... bs) {
  double salary = basic;
  if (bs[0]) salary = plusAllowance(salary);
  if (bs[1]) salary = plusBonus(salary);
  if (bs[2]) salary = plusTax(salary);
  if (bs[3]) salary = plusSurcharge(salary);
  return salary;
}

Здесь bs — массив из 4 логических значений (для краткости я пропустил проверку длины массива), где каждый логический параметр указывает, должен ли применяться соответствующий метод или нет. Так, например:

calculate(1000.0, true, false, false, true);

рассчитывает чистую зарплату, начиная с базовой 1000 и применяя только надбавку (1-й метод) и надбавку (4-й). Пока все хорошо, но, как я уже сказал, предлагаемое решение слишком важно для удовлетворения наших новых функциональных предпочтений. Прежде всего, чтобы переписать его в более функциональном стиле, возможно, избавившись от изменяемой переменной заработной платы и 4 условных ветвей, отметим, что все 4 метода, используемые для расчета чистой заработной платы, на самом деле являются специальным типом функции. называется эндоморфизмом, где единственный входной параметр и возвращаемое значение имеют одинаковый тип. В терминах Java эндоморфизм может быть просто определен как:

interface Endomorphism<A> extends Function<A, A> { }

На самом деле в стандартном API Java 8 уже существует интерфейс UnaryOperator, определенный точно так же. Я переименовал его в «Эндоморфизм», потому что считаю это имя более правильным. Однако, если вы предпочитаете, вы можете просто заменить мой интерфейс Endomorphism на стандартный UnaryOperator, один в остальной части статьи, и все остальное будет работать точно так же. Для достижения нашей цели нам также понадобится вторая абстракция — моноид:

interface Monoid<A> {
  A append(A a1, A a2);
  A zero();
}

В теории категорий моноид определяется как полугруппа с тождеством. На планете Земля это просто ассоциативная операция над типом, который также имеет ноль. Теперь можно объединить эти две абстракции и определить моноид для типа эндоморфизма следующим образом:

interface EndoMonoid<A> extends Monoid<Endomorphism<A>> {
  @Override
  default Endomorphism<A> append(Endomorphism<A> a1, Endomorphism<A> a2) {
  return (A a) -> a2.apply(a1.apply(a));
  }

  @Override
  default Endomorphism<A> zero() {
  return a -> a;
  }
}

Здесь ассоциативная операция представляет собой композицию двух функций (я не использовал напрямую метод compose (), определенный на интерфейсе функций, который эндоморфизм расширяет только потому, что он возвращает функцию вместо эндоморфизма), а ноль — просто тождественная функция , Это фактически ноль нашего моноида, потому что композиция любого эндоморфизма с ним возвращает сам эндоморфизм. Обратите внимание, что я определил другой интерфейс вместо класса, потому что, используя ключевое слово default, Java 8 позволяет добавить поведение, то есть реализованные методы, в интерфейс при условии, что они не имеют состояния. В конце концов мы можем реализовать этот последний интерфейс с реальным классом, единственная цель которого состоит в том, чтобы инкапсулировать эндоморфизм и предоставить несколько удобных методов, которые позволят нам свободно вычислять чистую зарплату,и конечно же функциональный стиль.

public class FluentEndoMonoid<A> implements EndoMonoid<A> {
  private final Endomorphism<A> endo;

  public FluentEndoMonoid(Endomorphism<A> endo) {
  this.endo = endo;
  }
   
  public FluentEndoMonoid(Endomorphism<A> endo, boolean b) {
  this.endo = b ? endo : zero();
  }

  public FluentEndoMonoid<A> add(Endomorphism<A> other) {
  return new FluentEndoMonoid<A>(append(endo, other));
  }
   
  public FluentEndoMonoid<A> add(Endomorphism<A> other, boolean b) {
  return add(b ? other : zero());
  }

  public Endomorphism<A> get() {
  return endo;
  }

  public static <A> FluentEndoMonoid<A> endo(Endomorphism<A> f, boolean b) {
  return new FluentEndoMonoid<A>(f, b);
  }
}

В частности, метод add (), принимающий как эндоморфизм, так и логическое значение, объединяет текущий эндоморфизм с предоставленным, если логическое значение истинно или с нулем (тождественная функция), в противном случае. Теперь мы готовы написать функциональную версию прежнего метода расчета:

public double calculate(double basic, boolean... bs) {
  return endo((Endomorphism<Double>) this::plusAllowance, bs[0])
  .add(this::plusBonus, bs[1])
  .add(this::plusTax, bs[2])
  .add(this::plusSurcharge, bs[3])
  .get()
  .apply(basic);
}

Как и ожидалось, основными преимуществами этого второго решения являются отсутствие как изменяемого состояния, так и альтернативной ветви оценки. Тем не менее, есть еще один положительный эффект от этого функционально-управляемого рефактора, который стоит подчеркнуть: удаляя последний вызов apply () из прежней цепочки быстрых вызовов, вы можете получить метод, возвращающий функцию для каждой из 16 возможных комбинаций 4 логического значения. значения в массиве. Таким образом, если, например, вам нужно рассчитать много зарплат, для которых должны применяться только надбавки и надбавки, вы можете получить функцию, передающую этому методу массив [true, false, false, true], а затем вычислить все эти зарплаты, просто вызвав несколько раз apply () для возвращаемой функции с разными базовыми зарплатами.