Мемоизация | Функциональное программирование
от aNNiMON
Недавно мы в команде столкнулись с такой проблемой. Есть сервис, который преобразует небольшие порции данных из одного вида в другой. Скажем, из записи Input в запись Output.
Проблема вот в чём. В зависимости от настроек, часть полей должна скрываться. Получение значения из настроек выполняется довольно быстро, но из-за того, что у нас несколько уровней вложенности данных, а настройку читаем внутри, это замедляет работу. Настройка меняется не часто, но и кэшировать её не хотелось бы. В идеале хочется обновлять значение каждый раз перед началом преобразования. Решение с прокидыванием параметра в каждый метод сразу отметаем, так как это ухудшает код, а ссылки на методы заменяются на лямбды:
Как поступить иначе? Можно сделать дополнительное поле со значением:
Так как поле перезаписывается каждый раз при старте преобразования, никаких проблем с неактуальным значением параметра не будет.
Но как сделать, чтобы вообще исключить чтение параметра, если такой необходимости нет? В этом поможет функциональное программирование — Supplier и мемоизация.
Supplier позволит обернуть реальное чтение параметра в некий контейнер, который без надобности вызываться не будет. Это ещё зовётся ленивостью.
А мемоизация позволит закешировать значение, если оно уже было однажды получено.
Если в таком supplier не вызывать метод get(), то и реальное значение не будет никогда прочитано.
В остальных случаях, вне зависимости от количества обращений к методу get(), чтение будет происходить лишь один раз:
- record Input(String id, LocalDate date, List<User> users) {}
- record User(String id, String fullname) {}
- record Output(String id, LocalDate date, List<ExternalUser> users) {}
- record ExternalUser(String id, String fullname) {}
- public class SomeDataExportMapper {
- private final Preferences preferences;
- public List<Output> export(List<Input> input) {
- return input.stream().map(this::toOutput).toList();
- }
- private Output toOutput(Input input) {
- return Output.builder()
- .id(input.id())
- .date(input.date())
- .users(input.users().stream().map(this::toExternalUser).toList())
- .build();
- }
- private ExternalUser toExternalUser(User user) {
- String pattern = getPseudonymizationPattern();
- return ExternalUser.builder()
- .id(input.id())
- .fullname(pattern != null ? pattern : user.fullname())
- .build();
- }
- private String getPseudonymizationPattern() {
- return preferences.retrieve(StringParam.PSEUDONYMIZATION_PATTERN);
- }
- }
Проблема вот в чём. В зависимости от настроек, часть полей должна скрываться. Получение значения из настроек выполняется довольно быстро, но из-за того, что у нас несколько уровней вложенности данных, а настройку читаем внутри, это замедляет работу. Настройка меняется не часто, но и кэшировать её не хотелось бы. В идеале хочется обновлять значение каждый раз перед началом преобразования. Решение с прокидыванием параметра в каждый метод сразу отметаем, так как это ухудшает код, а ссылки на методы заменяются на лямбды:
- public class SomeDataExportMapper {
- public List<Output> export(List<Input> input) {
- final String pseudonymizationPattern = getPseudonymizationPattern();
- return input.stream().map(i -> toOutput(i, pseudonymizationPattern)).toList();
- }
- private Output toOutput(Input input, String pseudonymizationPattern) {
- return Output.builder()
- .users(input.users().stream().map(u -> toExternalUser(u, pseudonymizationPattern)).toList())
- // ...
- }
- }
Как поступить иначе? Можно сделать дополнительное поле со значением:
- public class SomeDataExportMapper {
- private String pseudonymizationPattern;
- public List<Output> export(List<Input> input) {
- pseudonymizationPattern = getPseudonymizationPattern();
- return input.stream().map(this::toOutput).toList();
- }
- // ...
- private ExternalUser toExternalUser(User user) {
- return ExternalUser.builder()
- .id(input.id())
- .fullname(pseudonymizationPattern != null ? pseudonymizationPattern : user.fullname())
- .build();
- }
- }
Так как поле перезаписывается каждый раз при старте преобразования, никаких проблем с неактуальным значением параметра не будет.
Но как сделать, чтобы вообще исключить чтение параметра, если такой необходимости нет? В этом поможет функциональное программирование — Supplier и мемоизация.
Supplier позволит обернуть реальное чтение параметра в некий контейнер, который без надобности вызываться не будет. Это ещё зовётся ленивостью.
А мемоизация позволит закешировать значение, если оно уже было однажды получено.
- public class MemoizingSupplier<T> implements Supplier<T> {
- private Supplier<T> delegate;
- private T value;
- public static <T> Supplier<T> memoize(Supplier<T> s) {
- return s instanceof MemoizingSupplier ? s : new MemoizingSupplier<>(s);
- }
- private MemoizingSupplier(Supplier<T> s) {
- this.delegate = s;
- }
- @Override
- public T get() {
- if (delegate != null) {
- value = delegate.get();
- delegate = null;
- }
- return value;
- }
- }
Если в таком supplier не вызывать метод get(), то и реальное значение не будет никогда прочитано.
В остальных случаях, вне зависимости от количества обращений к методу get(), чтение будет происходить лишь один раз:
- public class SomeDataExportMapper {
- private final Preferences preferences;
- private Supplier<String> pseudonymizationPatternSupplier;
- public List<Output> export(List<Input> input) {
- pseudonymizationPatternSupplier = getPseudonymizationPatternSupplier();
- return input.stream().map(this::toOutput).toList();
- }
- // Output toOutput(Input input) { .. }
- private ExternalUser toExternalUser(User user) {
- String pattern = pseudonymizationPatternSupplier.get();
- return ExternalUser.builder()
- .id(input.id())
- .fullname(pattern != null ? pattern : user.fullname())
- .build();
- }
- private Supplier<String> getPseudonymizationPatternSupplier() {
- return memoize(() -> preferences.retrieve(StringParam.PSEUDONYMIZATION_PATTERN));
- }
- }