Это часть 8, последняя часть серии под названием «Функциональная Java на примере».
Пример, который я разрабатываю в каждой части серии, — это своего рода «обработчик каналов», который обрабатывает документы. В последней части мы видели сопоставление с образцом, используя библиотеку Vavr, и обрабатывали сбои как данные , например, выбирали альтернативный путь и возвращались к функциональному потоку.
В этом последнем посте серии я довожу функции до крайности : все становится функцией.
Если вы пришли впервые, лучше начать читать с самого начала. Это помогает понять, с чего мы начали и как продвигались вперед на протяжении всей серии.
Это все части:
- Часть 1. От императива к декларативному
- Часть 2 — Расскажите историю
- Часть 3 — Не используйте исключения для управления потоком
- Часть 4 — Предпочитают неизменность
- Часть 5 — Переместить ввод / вывод на улицу
- Часть 6 — Функции как параметры
- Часть 7 — Относитесь к сбоям как к данным
- Часть 8 — Больше чистых функций
Я буду обновлять ссылки по мере публикации каждой статьи. Если вы читаете эту статью через синдикацию контента, пожалуйста, проверьте оригинальные статьи в моем блоге .
Каждый раз также код добавляется в этот проект GitHub .
Максимизация движущихся частей
Возможно, вы слышали следующую фразу Micheal Feathers :
ОО делает код понятным за счет инкапсуляции движущихся частей. FP делает код понятным, сводя к минимуму движущиеся части.
Хорошо, давайте немного забудем о восстановлении после сбоя в предыдущей части и продолжим с версией, как показано ниже:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
class FeedHandler { List<Doc> handle(List<Doc> changes, Function<Doc, Try<Resource>> creator) { changes .findAll { doc -> isImportant(doc) } .collect { doc -> creator.apply(doc) }.map { resource -> setToProcessed(doc, resource) }.getOrElseGet { e -> setToFailed(doc, e) } } } private static boolean isImportant(doc) { doc.type == 'important' } private static Doc setToProcessed(doc, resource) { doc.copyWith( status: 'processed' , apiId: resource.id ) } private static Doc setToFailed(doc, e) { doc.copyWith( status: 'failed' , error: e.message ) } } |
Заменить на функциональные типы
Мы можем заменить каждый метод ссылкой на переменную типа функционального интерфейса , например, Predicate
или BiFunction
.
А) Мы можем заменить метод, который принимает 1 аргумент, который возвращает логическое значение .
1
2
3
|
private static boolean isImportant(doc) { doc.type == 'important' } |
предикатом
1
2
3
|
private static Predicate<Doc> isImportant = { doc -> doc.type == 'important' } |
Б) и мы можем заменить метод, который принимает 2 аргумента и возвращает результат
1
2
3
4
5
6
7
|
private static Doc setToProcessed(doc, resource) { ... } private static Doc setToFailed(doc, e) { ... } |
с бифункцией
1
2
3
4
5
6
7
|
private static BiFunction<Doc, Resource, Doc> setToProcessed = { doc, resource -> ... } private static BiFunction<Doc, Throwable, Doc> setToFailed = { doc, e -> ... } |
Чтобы фактически вызвать логику, инкапсулированную в (Bi) функцию, мы должны вызвать apply
для нее. Результат следующий:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
class FeedHandler { List<Doc> handle(List<Doc> changes, Function<Doc, Try<Resource>> creator) { changes .findAll { isImportant } .collect { doc -> creator.apply(doc) .map { resource -> setToProcessed.apply(doc, resource) }.getOrElseGet { e -> setToFailed.apply(doc, e) } } } private static Predicate<Doc> isImportant = { doc -> doc.type == 'important' } private static BiFunction<Doc, Resource, Doc> setToProcessed = { doc, resource -> doc.copyWith( status: 'processed' , apiId: resource.id ) } private static BiFunction<Doc, Throwable, Doc> setToFailed = { doc, e -> doc.copyWith( status: 'failed' , error: e.message ) } } |
Перемещение всего ввода в саму функцию
Мы перемещаем все в сигнатуру метода, чтобы вызывающий метод FeedHandler мог предоставить собственную реализацию этих функций.
Подпись метода изменится с:
1
2
|
List<Doc> handle(List<Doc> changes, Function<Doc, Try<Resource>> creator) |
в
1
2
3
4
5
|
List<Doc> handle(List<Doc> changes, Function<Doc, Try<Resource>> creator, Predicate<Doc> filter, BiFunction<Doc, Resource, Doc> successMapper, BiFunction<Doc, Throwable, Doc> failureMapper) |
Во-вторых, мы переименовываем наши оригинальные (статические) переменные Predicate и BiFunction.
-
isImportant
-
setToProcessed
-
setToFailed
новым постоянным в верхней части класса, отражая их новую роль, соотв.
-
DEFAULT_FILTER
-
DEFAULT_SUCCESS_MAPPER
-
DEFAULT_FAILURE_MAPPER
Клиент может полностью контролировать, используется ли реализация по умолчанию для определенных функций, или когда необходимо взять на себя пользовательскую логику.
Например, когда нужно настроить только обработку ошибок, метод handle
может быть вызван так:
01
02
03
04
05
06
07
08
09
10
11
12
|
BiFunction<Doc, Throwable, Doc> customFailureMapper = { doc, e -> doc.copyWith( status: 'my-custom-fail-status' , error: e.message ) } new FeedHandler().handle(..., FeedHandler.DEFAULT_FILTER, FeedHandler.DEFAULT_SUCCESS_MAPPER, customFailureMapper ) |
Если ваш язык поддерживает это, вы можете убедиться, что ваш клиент на самом деле не должен предоставлять каждый параметр, назначая значения по умолчанию. Я использую Apache Groovy, который поддерживает присвоение значений по умолчанию параметрам в методе:
1
2
3
4
5
|
List<Doc> handle(List<Doc> changes, Function<Doc, Try<Resource>> creator, Predicate<Doc> filter = DEFAULT_FILTER, BiFunction<Doc, Resource, Doc> successMapper = DEFAULT_SUCCESS_MAPPER, BiFunction<Doc, Throwable, Doc> failureMapper = DEFAULT_FAILURE_MAPPER) |
Посмотрите на код, прежде чем мы собираемся применить еще одно изменение:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
class FeedHandler { private static final Predicate<Doc> DEFAULT_FILTER = { doc -> doc.type == 'important' } private static final BiFunction<Doc, Resource, Doc> DEFAULT_SUCCESS_MAPPER = { doc, resource -> doc.copyWith( status: 'processed' , apiId: resource.id ) } private static final BiFunction<Doc, Throwable, Doc> DEFAULT_FAILURE_MAPPER = { doc, e -> doc.copyWith( status: 'failed' , error: e.message ) } List<Doc> handle(List<Doc> changes, Function<Doc, Try<Resource>> creator, Predicate<Doc> filter = DEFAULT_FILTER, BiFunction<Doc, Resource, Doc> successMapper = DEFAULT_SUCCESS_MAPPER, BiFunction<Doc, Throwable, Doc> failureMapper = DEFAULT_FAILURE_MAPPER) { changes .findAll { filter } .collect { doc -> creator.apply(doc) .map { resource -> successMapper.apply(doc, resource) }.getOrElseGet { e -> failureMapper.apply(doc, e) } } } } |
Введите либо
Вы заметили следующую часть?
1
2
3
4
5
6
7
8
|
.collect { doc -> creator.apply(doc) .map { resource -> successMapper.apply(doc, resource) }.getOrElseGet { e -> failureMapper.apply(doc, e) } } |
Помните, что тип creator
1
|
Function<Doc, Try<Resource>> |
Это означает, что он возвращает Try
. Мы представили Try в части 7 , заимствуя его из таких языков, как Scala.
К счастью, переменная «doc» из collect { doc
все еще находится в области действия, чтобы передать ее нашим successMapper
и failureMapper
которые в ней нуждаются , но есть несоответствие между сигнатурой метода Try#map
, которая принимает функцию , и нашим successMapper
, который БиФункция . То же самое касается Try#getOrElseGet
— ему также нужна только функция .
Из Try Javadocs:
- map (функция <? super T, extends U> mapper)
- getOrElseGet (Функция <? super Throwable, расширяет T> прочее)
Проще говоря, нам нужно идти от
- BiFunction <Doc, Resource, Doc> successMapper
- BiFunction <Doc, Throwable, Doc> faultMapper
в
- Функция <Resource, Doc> successMapper
- Функция <Throwable, Doc> faultMapper
сохраняя при этом возможность использовать исходный документ в качестве входных данных .
Давайте введем два простых типа, инкапсулирующих 2 аргумента 2 BiFunctions:
1
2
3
4
5
6
7
8
9
|
class CreationSuccess { Doc doc Resource resource } class CreationFailed { Doc doc Exception e } |
Мы меняем аргументы от
- BiFunction <Doc, Resource, Doc> successMapper
- BiFunction <Doc, Throwable, Doc> faultMapper
вместо функции :
- Функция <CreationSuccess, Doc> successMapper
- Функция <CreationFailed, Doc> faultMapper
Метод handle
теперь выглядит так:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
List<Doc> handle(List<Doc> changes, Function<Doc, Try<Resource>> creator, Predicate<Doc> filter, Function<CreationSuccess, Doc> successMapper, Function<CreationFailed, Doc> failureMapper) { changes .findAll { filter } .collect { doc -> creator.apply(doc) .map(successMapper) .getOrElseGet(failureMapper) } } |
… но это еще не работает .
При getOrElseGet
сделать map
и getOrElseGet
требуют соотв.
- Функция <Resource, Doc> successMapper
- Функция <Throwable, Doc> faultMapper
Вот почему мы должны изменить его на другую известную конструкцию FP, называемую Either .
К счастью, у Вавра тоже есть Либо . Его Javadoc говорит:
Любой представляет значение двух возможных типов.
Тип Either обычно используется для различения между значением, которое является правильным («правильным») или ошибочным.
Абстрагируется довольно быстро:
Либо это либо Либо. Левый, либо Либо. Правый. Если данный Either является Right и спроецирован на Left, операции Left не влияют на значение Right. Если данный Either является Left и проецируется вправо, операции Right не влияют на значение Left. Если слева проецируется слева или справа проецируется справа, операции имеют эффект.
Позвольте мне объяснить выше загадочную документацию. Если мы заменим
1
|
Function<Doc, Try<Resource>> creator |
по
1
|
Function<Doc, Either<CreationFailed, CreationSuccess>> creator |
мы присваиваем CreationFailed
аргументу «left», который по соглашению обычно содержит ошибку (см. документацию по Haskell на любом из них ), а CreationSuccess
— это «правильное» (и «правильное») значение.
Во время выполнения реализация возвращала Try
, но теперь она может вернуть Either.Right в случае успеха, например
1
2
3
4
5
6
|
return Either.right( new CreationSuccess( doc: document, resource: [id: '7' ] ) ) |
или Either.Left с исключением в случае сбоя — и оба, включая исходный документ тоже . Да.
Потому что теперь в конечном итоге типы совпадают, мы наконец сквош
1
2
3
4
5
6
7
8
|
.collect { doc -> creator.apply(doc) .map { resource -> successMapper.apply(doc, resource) }.getOrElseGet { e -> failureMapper.apply(doc, e) } } |
в
1
2
3
4
5
|
.collect { doc -> creator.apply(doc) .map(successMapper) .getOrElseGet(failureMapper) } |
Метод handle
теперь выглядит так:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
List<Doc> handle(List<Doc> changes, Function<Doc, Either<CreationFailed, CreationSuccess>> creator, Predicate<Doc> filter, Function<CreationSuccess, Doc> successMapper, Function<CreationFailed, Doc> failureMapper) { changes .findAll { filter } .collect { doc -> creator.apply(doc) .map(successMapper) .getOrElseGet(failureMapper) } } |
Вывод
Я могу сказать, что я достиг большинства целей, которые я поставил в начале:
- Да, мне удалось избежать переназначения переменных
- Да, мне удалось избежать изменчивых структур данных
- Да, мне удалось избежать состояния (ну, по крайней мере, в FeedHandler)
- Да, мне удалось отдать предпочтение функциям (используя некоторые из встроенных функциональных типов Java и некоторые сторонние библиотеки Vavr)
Мы переместили все в сигнатуру функции, чтобы вызывающий метод FeedHandler мог напрямую передавать правильные реализации. Если вы вернетесь назад к первоначальной версии, то заметите, что у нас все еще есть все обязанности при обработке списка изменений:
- фильтрация списка документов по некоторым критериям
- создание ресурса для каждого документа
- сделать что-то, когда ресурс был успешно создан
- делать что-то еще, когда ресурс не может быть создан
Тем не менее, в первой части эти обязанности были выписаны в обязательном порядке , утверждение для утверждения, все объединены в один метод большой handle
. Теперь, в конце, каждое решение или действие представляется функцией с абстрактными именами, такими как «фильтр», «создатель», «successMapper» и «failMapper». По сути, она стала функцией более высокого порядка, взяв одну из нескольких функций в качестве аргумента. Ответственность за предоставление всех аргументов была перенесена на уровень вверх по стеку к клиенту. Если вы посмотрите на проект GitHub, вы заметите, что для этих примеров мне приходилось постоянно обновлять модульные тесты.
Спорные части
На практике я, вероятно, не стал бы писать свой (Java) бизнес-код, например, как класс FeedHandler
отношении использования передачи в универсальных функциональных типах Java (т. BiFunction
Function
, BiFunction
, Predicate
, Consumer
, Supplier
), если мне не нужно все это чрезвычайная гибкость. Все это происходит за счет читабельности. Да, Java является языком со статической типизацией, поэтому, используя дженерики, нужно быть явным во всех параметрах типа , что приводит к сложной сигнатуре функции:
1
2
3
4
5
|
handle(List<Doc> changes, Function<Doc, Either<CreationFailed, CreationSuccess>> creator, Predicate<Doc> filter, Function<CreationSuccess, Doc> successMapper, Function<CreationFailed, Doc> failureMapper) |
В простом JavaScript у вас не было бы ни одного из типов, и вам нужно было бы прочитать документацию, чтобы узнать, что ожидается от каждого аргумента.
1
|
handle = function (changes, creator, filter, successMapper, failureMapper) |
Но эй, это компромисс. Groovy, также язык JVM, позволил бы мне опустить информацию о типах во всех примерах этой серии, и даже позволил мне использовать Closures (как лямбда-выражения в Java), которые являются ядром парадигмы функционального программирования в Groovy.
Более экстремальным было бы указание всех типов на уровне класса для максимальной гибкости, позволяющей клиенту указывать разные типы для разных экземпляров FeedHandler
.
1
2
3
4
5
|
handle(List<T> changes, Function<T, Either<R, S>> creator, Predicate<T> filter, Function<S, T> successMapper, Function<R, T> failureMapper) |
Когда это уместно?
- Если у вас есть полный контроль над вашим кодом, когда он используется в определенном контексте для решения конкретной проблемы, это будет слишком абстрактно, чтобы приносить какие-либо выгоды.
- Однако, если бы я открыл для себя библиотеку или инфраструктуру с открытым исходным кодом (или, может быть, внутри организации для других команд или отделов), которая используется в различных случаях использования, о которых я не могу думать заранее, вероятно, дизайн для гибкости стоило того. Пусть вызывающие абоненты решают, как отфильтровать, и что является успехом или неудачей, может быть разумным шагом.
В конечном счете, выше немного затрагивается дизайн API , да, и разделение , но «превращение всего в функцию» в типичном Java-проекте Enterprise ™, вероятно, требует некоторого обсуждения с вами и вашими товарищами по команде. Некоторые коллеги с годами привыкли к более традиционному идиоматическому способу написания кода.
Хорошие части
- Я бы определенно предпочел неизменяемые структуры данных (и «ссылочную прозрачность»), чтобы помочь рассуждать о состоянии, в котором находятся мои данные. Подумайте о
Collections.unmodifiableCollection
для коллекций. В моих примерах я использовал Groovy@Immutable
для POJO, но в простых библиотеках Java, таких как Immutables , AutoValue или Project Lombok, можно использовать. - Самым большим улучшением на самом деле стало создание более функционального стиля: создание кода, рассказывающего историю , которая состояла в основном из разделения задач и надлежащего присвоения имен. Это хорошая практика в любом стиле программирования (даже OO: D), но это действительно очистило хаос и позволило вообще ввести (чистые) функции.
- В Java мы настолько привыкли к обработке исключений особым образом, что разработчикам, таким как я, сложно придумывать другие решения. Функциональный язык, такой как Haskell, просто возвращает коды ошибок, потому что «Никлаус Вирт рассматривал исключения как реинкарнацию GOTO и таким образом опускал их» . В Java можно использовать
CompletableFuture
или… - Определенные типы, такие как
Try
andEither
, которые можно использовать в своей собственной кодовой базе, представляя стороннюю библиотеку, такую как Vavr, могут помочь в создании большего количества опций, написанных в стиле FP! Я был очень очарован элегантностью написания путей «успеха» или «неудачи» бегло и быть очень читабельным.
Java — это не Scala, не Haskell или Clojure для F #, и изначально она следовала парадигме объектно-ориентированного программирования (ООП), точно так же, как C ++, C #, Ruby и т. Д., Но после введения лямбда-выражений в Java 8 и в сочетании с некоторыми удивительными библиотеки с открытым исходным кодом, разработчики в настоящее время определенно могут выбирать и смешивать лучшие элементы, которые могут предложить OOP и FP .
Уроки, извлеченные из выполнения серии
Я начал эту серию очень давно . Еще в 2017 году я провел несколько рефакторингов в стиле FP для фрагмента кода, что вдохновило меня на поиск примера для серии статей под названием «Функциональная Java на примере» . Это стало кодом FeedHandler
который я использовал в каждой статье.
Я уже тогда делал все индивидуальные изменения кода, но в то время, когда я планировал писать реальные посты в блоге, я часто думал: «Я просто не могу показать только рефакторинг, я должен что-то объяснить!» Вот где я как бы заложил ловушку для себя, потому что со временем у меня становилось все меньше и меньше времени, чтобы сидеть и писать . (Любой, кто когда-либо писал блог, знает разницу во времени, когда он просто делится смыслом и пишет понятные абзацы понятного английского языка ?)
В следующий раз, когда я подумаю о создании серии, я вернусь в Google для некоторых из этих уроков:
- Не включайте оглавление (TOC) вверху каждой статьи, если вы не готовы обновлять все ссылки каждый раз в каждой ранее опубликованной партии при публикации новой статьи. И если вы перепишете их в корпоративном блоге компании, это в 2 раза больше работы ?
- Со временем вы можете прийти к выводу, что вы предпочитаете отклоняться от основного варианта использования, вашего Большого примера кодирования, с которого вы начинали. Я предпочел бы продемонстрировать гораздо больше концепций FP — таких как каррирование, запоминание, лень, а также другое мышление при использовании техник FP — но я не мог вписаться в это в рамках ранее выполненных рефакторингов и TOC, которые я установил в начале , Если вы пишете о конкретной концепции, обычно можно найти соответствующий пример, помогающий объяснить конкретную концепцию под рукой, и при этом относящийся к читателю. Со временем, как я понял, приходит понимание того, что лучше написать о следующем и какие более подходящие примеры использовать. В следующий раз я должен найти способ дать (лучше: позволить) себе творческую свободу на этом пути ?
Прочитайте больше
- Функциональное мышление: парадигма над синтаксисом Удивительная книга Нила Форда, в которой показан новый способ мышления ФП, а также различные подходы к решению проблем.
- Функциональное программирование за 40 минут Youtube-видео Русса Олсена с объяснением «этим математикам требуется 379 страниц, чтобы доказать 1 + 1 = 2. Давайте посмотрим, какие хорошие идеи мы можем украсть у них »?
- Почему функциональное программирование не является нормой? Youtube видео Ричарда Фельдмана, где он объясняет, почему ООП стал очень популярным и почему FP не является нормой. Он является членом основной команды Elm и, как вы можете сказать, имеет некоторое сходство с FP.
- Инверсия (сцепного) контроля Статья для размышлений об «управляемых функциях». Вы хотели абстрагироваться?
Если у вас есть какие-либо комментарии или предложения, я хотел бы услышать о них!
Удачного программирования! ?
Опубликовано на Java Code Geeks с разрешения Теда Винке, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Функциональная Java на примере | Часть 8 — Больше чистых функций Мнения, высказанные участниками Java Code Geeks, являются их собственными. |