Utility Classes Have Nothing to Do With Functional Programming

The following text is a partial translation of the original English article, performed by ChatGPT (gpt-3.5-turbo) and this Jekyll plugin:

Меня недавно обвинили в том, что я против функционального программирования, потому что я называю утилитарные классы антипаттерном. Это абсолютно неверно! Хорошо, я действительно считаю их ужасным антипаттерном, но они никак не связаны с функциональным программированием. Я считаю, что есть две основные причины для этого. Во-первых, функциональное программирование декларативно, в то время как методы утилитарных классов являются императивными. Во-вторых, функциональное программирование основано на лямбда-исчислении, где функцию можно присвоить переменной. Методы утилитарных классов не являются функциями в этом смысле. Я расшифрую эти утверждения через минуту.

В Java существует два основных варианта замены этих уродливых утилитарных классов, активно продвигаемых Guava, Apache Commons и другими. Первый вариант - использование традиционных классов, а второй - лямбда-выражения Java 8. Теперь давайте посмотрим, почему утилитарные классы далеки от функционального программирования и откуда возникло это недоразумение.

Вот типичный пример утилитарного класса Math из Java 1.0:

Вот как вы будете использовать это, когда вы хотите вычислить абсолютное значение числа с плавающей точкой.

Что с ним не так? Нам нужна функция, и мы получаем ее из класса Math. Внутри этого класса находится много полезных функций, которые могут быть использованы для множества типичных математических операций, таких как вычисление максимума, минимума, синуса, косинуса и т. д. Это очень популярная концепция; просто посмотрите на любой коммерческий или открытый продукт. Утилитарные классы используются повсюду с момента изобретения Java (этот класс Math был представлен в первой версии Java). Технически ничего не так. Код будет работать. Но это не объектно-ориентированное программирование. Вместо этого это императивное и процедурное программирование. Нас это волнует? Ну, это зависит от вас, чтобы решить. Посмотрим, в чем разница.

В основном существуют два различных подхода: декларативный и императивный.

Императивное программирование сосредоточено на описании как программа работает в терминах операторов, изменяющих состояние программы. Мы только что увидели пример императивного программирования выше. Вот еще один пример (это чисто императивное/процедурное программирование, которое никак не связано с ООП):

Декларативное программирование сосредотачивается на том, что программа должна достичь, не указывая, как это сделать в терминах последовательности действий, которые нужно предпринять. Вот как будет выглядеть тот же код на Лиспе, функциональном языке программирования:

В чем подвох? Просто разница в синтаксисе? Вовсе нет.

Существует много определений разницы между императивным и декларативным стилями, но я попытаюсь дать свое собственное. В сценарии с функцией/методом f взаимодействуют три основных роли: покупатель, упаковщик результата и потребитель результата. Допустим, я вызываю эту функцию так:

Здесь метод calc() является покупателем, метод Math.f() - упаковщиком результата, а метод foo() - потребителем. Независимо от используемого стиля программирования, всегда участвуют эти три лица в процессе: покупатель, упаковщик и потребитель.

Представьте, что вы покупатель и хотите приобрести подарок для своей подруги/подруги. Первый вариант - посетить магазин, заплатить 50 долларов, позволить им упаковать этот парфюм для вас, а затем доставить его другу (и получить взамен поцелуй). Это императивный стиль.

Второй вариант - посетить магазин, заплатить 50 долларов и получить подарочную карту. Затем вы представляете эту карту другу/подруге (и получаете взамен поцелуй). Когда он или она решит обменять ее на парфюм, он или она посетит магазин и получит его. Это декларативный стиль.

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

Более того, ваш друг может использовать этот подарочный сертификат как самостоятельный продукт, никогда не посещая магазин. Он или она может вместо этого подарить его кому-то другому или просто обменять его на другой сертификат или продукт. Сам по себе подарочный сертификат становится продуктом!

Так что разница заключается в том, что получает потребитель - либо готовый к использованию продукт (повелительное наклонение), либо ваучер на продукт, который позже можно превратить в реальный продукт (утвердительное наклонение).

Утилитарные классы, например, Math из JDK или StringUtils из Apache Commons, возвращают готовые к использованию продукты немедленно, в то время как функции в Lisp и других функциональных языках возвращают “ваучеры”. Например, если вы вызываете функцию max в Lisp, фактический максимум между двумя числами будет вычислен только тогда, когда вы начнете его использовать:

Пока эта print на самом деле не начнет выводить символы на экран, функция max не будет вызвана. Это x - “путевка”, возвращенная вам при попытке “купить” максимум между 1 и 5.

Однако следует отметить, что вложение статических функций Java друг в друга не делает их декларативными. Код все еще является императивным, потому что его выполнение дает результат здесь и сейчас:

“Хорошо”, вы можете сказать, “я понял, но почему декларативный стиль лучше императивного? В чем суть?” Я к этому иду. Позвольте мне сначала показать разницу между функциями в функциональном программировании и статическими методами в ООП. Как упоминалось выше, это второе большое отличие между утилитарными классами и функциональным программированием.

В любом функциональном языке программирования вы можете сделать это:

Потом, позже, вы можете вызвать это x

Статические методы в Java не являются функциями в терминах функционального программирования. Вы не можете делать ничего подобного с помощью статического метода. Вы не можете передавать статический метод как аргумент в другой метод. В основном, статические методы являются процедурами или, проще говоря, инструкциями на языке Java, сгруппированными под уникальным именем. Единственный способ получить к ним доступ - вызвать процедуру и передать все необходимые аргументы. Процедура будет вычислять что-то и возвращать результат, который немедленно готов к использованию.

И теперь мы подходим к последнему вопросу, который я слышу, как вы спрашиваете: “Хорошо, утилитарные классы не являются функциональным программированием, но они выглядят как функциональное программирование, они работают очень быстро и очень легко использовать. Почему бы не использовать их? Зачем стремиться к совершенству, когда 20 лет истории Java доказывают, что утилитарные классы - это основной инструмент каждого разработчика на Java?”

Помимо фундаментализма ООП, которым меня очень часто обвиняют (кстати, я фундаменталист ООП), есть несколько очень практических причин:

  • Эффективность. Утилитарные классы, из-за своей императивной природы, гораздо менее эффективны, чем их декларативные альтернативы. Они просто выполняют все вычисления здесь и сейчас, используя ресурсы процессора, даже когда это еще необходимо. Вместо того, чтобы возвращать обещание разбить строку на части, StringUtils.split() разбивает ее прямо сейчас. И она разбивает ее на все возможные части, даже если только первая требуется “покупателю”.

  • Читаемость. Утилитарные классы обычно огромные (попробуйте прочитать исходный код StringUtils или FileUtils из Apache Commons). Весь идеал разделения ответственностей, который делает ООП таким красивым, отсутствует в утилитарных классах. Они просто помещают все возможные процедуры в один огромный файл .java, который становится совершенно неподдерживаемым, когда в нем накапливается более десяти статических методов.

В заключение, позвольте мне повторить: Утилитарные классы не имеют ничего общего с функциональным программированием. Они просто наборы статических методов, которые являются императивными процедурами. Постарайтесь держаться как можно дальше от них и использовать надежные, цельные объекты, независимо от того, сколько из них вам придется объявить и насколько они маленькие.

Translated by ChatGPT gpt-3.5-turbo/42 on 2023-12-16 at 15:39

sixnines availability badge   GitHub stars