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