Мемоизация | Функциональное программирование

от
Java    functional programming, memoization, supplier

Недавно мы в команде столкнулись с такой проблемой. Есть сервис, который преобразует небольшие порции данных из одного вида в другой. Скажем, из записи Input в запись Output.

  1. record Input(String id, LocalDate date, List<User> users) {}
  2. record User(String id, String fullname) {}
  3.  
  4. record Output(String id, LocalDate date, List<ExternalUser> users) {}
  5. record ExternalUser(String id, String fullname) {}
  6.  
  7. public class SomeDataExportMapper {
  8.     private final Preferences preferences;
  9.  
  10.     public List<Output> export(List<Input> input) {
  11.         return input.stream().map(this::toOutput).toList();
  12.     }
  13.  
  14.     private Output toOutput(Input input) {
  15.         return Output.builder()
  16.                 .id(input.id())
  17.                 .date(input.date())
  18.                 .users(input.users().stream().map(this::toExternalUser).toList())
  19.                 .build();
  20.     }
  21.  
  22.     private ExternalUser toExternalUser(User user) {
  23.         String pattern = getPseudonymizationPattern();
  24.         return ExternalUser.builder()
  25.                 .id(input.id())
  26.                 .fullname(pattern != null ? pattern : user.fullname())
  27.                 .build();
  28.     }
  29.  
  30.     private String getPseudonymizationPattern() {
  31.         return preferences.retrieve(StringParam.PSEUDONYMIZATION_PATTERN);
  32.     }
  33. }

Проблема вот в чём. В зависимости от настроек, часть полей должна скрываться. Получение значения из настроек выполняется довольно быстро, но из-за того, что у нас несколько уровней вложенности данных, а настройку читаем внутри, это замедляет работу. Настройка меняется не часто, но и кэшировать её не хотелось бы. В идеале хочется обновлять значение каждый раз перед началом преобразования. Решение с прокидыванием параметра в каждый метод сразу отметаем, так как это ухудшает код, а ссылки на методы заменяются на лямбды:
  1. public class SomeDataExportMapper {
  2.     public List<Output> export(List<Input> input) {
  3.         final String pseudonymizationPattern = getPseudonymizationPattern();
  4.         return input.stream().map(i -> toOutput(i, pseudonymizationPattern)).toList();
  5.     }
  6.  
  7.     private Output toOutput(Input input, String pseudonymizationPattern) {
  8.         return Output.builder()
  9.                 .users(input.users().stream().map(u -> toExternalUser(u, pseudonymizationPattern)).toList())
  10.                 // ...
  11.     }
  12. }

Как поступить иначе? Можно сделать дополнительное поле со значением:
  1. public class SomeDataExportMapper {
  2.     private String pseudonymizationPattern;
  3.  
  4.     public List<Output> export(List<Input> input) {
  5.         pseudonymizationPattern = getPseudonymizationPattern();
  6.         return input.stream().map(this::toOutput).toList();
  7.     }
  8.  
  9.     // ...
  10.  
  11.     private ExternalUser toExternalUser(User user) {
  12.         return ExternalUser.builder()
  13.                 .id(input.id())
  14.                 .fullname(pseudonymizationPattern != null ? pseudonymizationPattern : user.fullname())
  15.                 .build();
  16.     }
  17. }

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

Но как сделать, чтобы вообще исключить чтение параметра, если такой необходимости нет? В этом поможет функциональное программирование — Supplier и мемоизация.

Supplier позволит обернуть реальное чтение параметра в некий контейнер, который без надобности вызываться не будет. Это ещё зовётся ленивостью.
А мемоизация позволит закешировать значение, если оно уже было однажды получено.
  1. public class MemoizingSupplier<T> implements Supplier<T> {
  2.     private Supplier<T> delegate;
  3.     private T value;
  4.  
  5.     public static <T> Supplier<T> memoize(Supplier<T> s) {
  6.         return s instanceof MemoizingSupplier ? s : new MemoizingSupplier<>(s);
  7.     }
  8.  
  9.     private MemoizingSupplier(Supplier<T> s) {
  10.         this.delegate = s;
  11.     }
  12.  
  13.     @Override
  14.     public T get() {
  15.         if (delegate != null) {
  16.             value = delegate.get();
  17.             delegate = null;
  18.         }
  19.         return value;
  20.     }
  21. }

Если в таком supplier не вызывать метод get(), то и реальное значение не будет никогда прочитано.
В остальных случаях, вне зависимости от количества обращений к методу get(), чтение будет происходить лишь один раз:
  1. public class SomeDataExportMapper {
  2.     private final Preferences preferences;
  3.     private Supplier<String> pseudonymizationPatternSupplier;
  4.  
  5.     public List<Output> export(List<Input> input) {        
  6.         pseudonymizationPatternSupplier = getPseudonymizationPatternSupplier();
  7.         return input.stream().map(this::toOutput).toList();
  8.     }
  9.  
  10.     // Output toOutput(Input input) { .. }
  11.  
  12.     private ExternalUser toExternalUser(User user) {
  13.         String pattern = pseudonymizationPatternSupplier.get();
  14.         return ExternalUser.builder()
  15.                 .id(input.id())
  16.                 .fullname(pattern != null ? pattern : user.fullname())
  17.                 .build();
  18.     }
  19.  
  20.     private Supplier<String> getPseudonymizationPatternSupplier() {
  21.         return memoize(() -> preferences.retrieve(StringParam.PSEUDONYMIZATION_PATTERN));
  22.     }
  23. }
  • +3
  • views 269