Пожалуйста, используйте необязательно‹› по назначению!
1. Обзор
Во-первых, давайте посмотрим, откуда возникает потребность в Optional (и вообще в monads). Для этого мы начнем с аналогии и подумаем о процессе приготовления хлеба.
- во-первых, мы будем делать муку, перемалывая пшеницу;
- после этого мы будем делать хлеб, используя муку из предыдущего шага;
Мы можем выразить это в программных или математических терминах, используя функции:
flour = mill( wheat ) bread = bake( flour )
Или мы можем просто скомпоновать две функции следующим образом:
bread = bake( mill(wheat) )
2. Угловые случаи
К сожалению, каждый шаг обработки может завершиться неудачей по разным причинам. При составлении подобных функций обработка ошибок может вызвать затруднения.
Например, возможно, пшеница не всегда успешно перерабатывается в муку, а мука не всегда правильно выпекается.
В зависимости от используемого языка программирования и контекста это может иметь разные последствия — например, может быть выдано исключение, может быть возвращено значение null и т. д.
Предположим, что в нашем случае, если что-то пойдет не так с одним из процессов, возвращается null.
flour = mill( wheat ) if( flour != null ) { bread = bake( flour ) if( bread != null ) { // .... do something() } }
На данный момент мы больше не можем составлять методы из-за нулевых проверок.
3. Знакомство с монадой
Чтобы исправить это, мы можем обернуть результат предыдущей функции в объект, который продолжит обработку, если данные присутствуют, и ничего не сделает, если данные отсутствуют. Этот объект-оболочка будет «монадой».
Например, мы можем представить список List. Если внутри есть элементы, они будут обработаны в соответствии со следующим шагом. С другой стороны, если список пуст, ничего не произойдет. Список — это монада.
Однако использование списка здесь не имеет большого смысла, потому что мы хотим передавать только один объект за раз от шага к шагу.
Поэтому мы можем построить свой собственный объект-монаду или использовать один из существующих. В языках программирования определено множество монад с функциональными возможностями. В зависимости от языка нужная нам монада может называться как-то вроде Maybe‹›, Either‹› или Optional‹›.
4. map()и flatMap()
Давайте завернем типы возвращаемых функций в Необязательный‹› и попробуем вернуться к композиции функций:
Теперь у нас появится новая проблема: чтобы составить или связать функции, каждая из них должна получить в качестве параметра результат предыдущей.
Следовательно, функция bake() должна принимать тип Optional‹Flour› вместо необработанного типа Flour.
Но, поскольку функция bake упаковывает результат в Optional‹›, результат второго шага будет завернут дважды, результат третьего шага будет завернут 3 раза. раз.. и тд.
Решением этой проблемы является функция flatMap().Цель flatMap() — сохранить структуру плоский при преобразовании из одного типа в другой.
Другими словами, мы можем использовать следующие соглашения для Monad:
- flatten :Monad‹Monad‹A›› →Monad‹A›
- карта: Монада‹A› → Монада‹B›
- flatMap :Monad‹A› → Monad‹Monad‹B›› → Monad‹B›
5. Функциональное программирование против ООП
Составление функций и методы объединения в цепочки
Теперь мы обернули возвращаемый тип для каждого шага в монаду, и это позволяет нам всегда передавать результат на следующий шаг.
Но как нам это сделать? У нас есть два возможных решения для этого.
5.1. Ф.П. Путь (составление функций)
Во-первых, мы можем сделать так, чтобы функции получали и возвращали монады:
public Optional<Flour> mill(Optional<Wheat> wheat) { // ... } public Optional<Bread> bake(Optional<Flour> flour) { // ... }
Это позволит нам скомпоновать функции в стиле функционального программирования:
Optional<Bread> fpBread = bake( mill( harvest() ) );
5.2. O.O.P. Путь (методы цепочки)
Java — это O.O.P. язык, сам необязательный объект предоставляет методы map и flatMap. Это означает, что нет необходимости передавать Необязательный объект в качестве параметра — на самом деле это считается запахом кода и анти-шаблоном (подробнее об этом в следующем разделе). ).
Поэтому мы должны использовать необработанные типы в качестве параметров метода:
public Optional<Flour> mill(Wheat wheat) { // ... } public Optional<Bread> bake(Flour flour) { // ... }
И цепочка вызовов методов вместо составления функций с использованием Optional map() и flatMap():
Optional<Bread> oopBread = harvest() .flatMap(wheat -> mill(wheat)) .flatMap(flour -> bake(flour));
6. Выводы. Какой хлеб лучше?
Когда Ф.П. функции были добавлены в O.O.P. таких языков, как Java, некоторые из них не использовались по назначению, что приводило к запахам кода и анти-шаблонам.
Придерживаясь примера с Java, некоторые разработчики и команды категорически против любого использования монады Optional‹›. Например, эта популярная статья под названием «Нет ничего лучше, чем необязательный тип».
Однако, если мы прочитаем статью и просмотрим фрагменты кода, мы увидим, что большинство проблем вызвано непреднамеренным использованием Optional.
На мой взгляд, Необязательно следует использовать как F.P. monad, а не как служебный класс Java для проверки null. Поэтому я нахожу все сравнение между ним и проверкой нуля немного неудачным.
Я считаю, что функция Optional‹› — отличное дополнение к языку, но она требует дисциплины и некоторого понимания концепции монад .
Эта статья была вдохновлена выступлением Сезара Трона-Лозаи на DEVOX. Я настоятельно рекомендую проверить это для более подробного объяснения:
Спасибо!
Спасибо за прочтение статьи и, пожалуйста, дайте мне знать, что вы думаете! Любая обратная связь приветствуется.
Если вы хотите узнать больше о чистом коде, дизайне, модульном тестировании, функциональном программировании и многом другом, обязательно ознакомьтесь с другими моими статьями.
Если вам нравится мой контент, подумайте о том, чтобы подписаться на список рассылки.
Наконец, если вы хотите стать участником Medium и поддержать мой блог, вот мой реферал.
Удачного кодирования!