Утилитарные классы - добро или зло?

100
15 февраля 2021, 10:10

Есть вот такой спорный вопрос.

Многие используют утилитарные классы, которые состоят из статических методов. Это всего функции со входом и результатом. Такие используют и библиотеки Java, например, Math. Многие делают свои классы типа Utils, которые содержат в себе набор используемых в разных местах инструментов. Я не вижу минусов, с точки зрения SOLID: S класс создан для общей по смыслу задачи O да L неприменимо. Обычно не используется наследование. I неприменимо. Нет интерефейса D да. Также они отвечают принципу KISS, и легко модульно тестируются при желании. На методы также можно ссылаться для функционального подхода (напр., Function<String,String> delCommentsConverter = QueryUtils::deleteComments). Ну в общем, полноценные роботяги.

Но также есть мнение, что это неверные подход, не по ООП, а Java это ОО-язык. Что это не класс, и вызов методов напрямую - это очень плохо. Типа нужно сначала получить инстанс, и только тогда работать с ним. Я не вижу в этом преимуществ, наоборот - растёт код, неудобно пользоваться.

А как вы думаете?

Answer 1

Имхо, статические методы должны:

1) делать очень маленькую специфичную работу;

2) не должны напрямую относиться к бизнес-логике;

3) как следствие 1) и 2) — их не надо мокать;

4) их можно тестировать отдельно.

Давайте рассмотрим каждый пункт отдельно.

Делать очень маленькую специфичную работу

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

Не должны напрямую относиться к бизнес-логике

Ваша бизнес-логика должна обладать гибкостью. Например, сегодня вы используете один калькулятор зарплат, а завтра — другой. По идее, это должно достигаться просто подменой одной реализации калькулятора другой. Отсутсвие O и L из SOLID по сути связывает статические методы, вы не можете подложить другую реализацию статического метода в рантайме, если только этот статический метод не нарушает предыдущий пункт. Поэтому, если вам надо сделать что-то, что не относится напрямую к бизнес логике и никогда не планируется измениться (например, округлить число, сортировать массив, возвести число в степень, посчитать расстояние между точками, распарсить дату их строки) — вы смело можете писать статические методы. Но если вам надо реализовать бизнес-логику (найти победителя в игре, посчитать налоговую декларацию, обновить значение поля А в зависимости от значения поля Б на форме, отправить сообщение юзеру при возникновении какого-либо события) — то эта логика должна быть максимально гибкой, основные её части должны быть заменяемы, там где надо даже в райтейме — то есть вы не можете делать её на статических методах.

Как следствие 1) и 2) — их не надо мокать

При тестировании логики, которая использует ваши статические методы, нет никакого смысла их мокать (опять же, если речь идет не о получении текущей даты/времени), так как эти методы к логике не относятся, никогда не получат альтернативной реализации и всегда на один и тот же запрос вернут один и тот же ответ. Вам никогда не понадобится имитация того, что строка "10" была распарсена в 15 или 20. "10" всегда при парсинге будет 10.

Их можно тестировать отдельно

Тут, по идее, очевидно, если вы следовали предыдущим советам, то у вас ваши статические методы — небольшие, не будут меняться, не имеют состояния и не относятся напрямую к бизнес-логике. В этом случае для тестирования этих методов вам не надо тянуть классы бизнес-логики или мокать их как-либо: вы можете спокойно тестировать эти методы без привязки к логике приложения.

Ещё немножко текста в похожем вопросе

Answer 2

Конечно, статические методы не являются объектно-ориентированным решением, тут апологеты ООП правы.

Однако, есть вопросы к самой объектной парадигме. Она идеально подходит для решения одних задач, и совершенно не подходит для других. Собственно, любая парадигма имеет границы применимости.

Именно поэтому концепция всё есть объект обрастает многочисленными но. Пример: в DDD разделяют сущности (entities) и службы (services). Службы предназначены для описания процессов, и они не обладают состоянием. С точки зрения «правильного» ООП объекты без состояния — это нонсенс.

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

Возвращаемся к статическим методам и утилитарным классам. В Java и C# они существуют потому, что это простой способ выразить концепцию функции. Можно спорить о том, делать ли sin функцией класса Double или нет, но в классической математике sin это не метод действительного числа, а функция, которая умеет работать с действительным числом.

Статические методы позволяют выразить эту концепцию и упростить код.

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

Решения этой проблемы могут быть разными. Например, вместо прямого вызова DateTime.Now в C# можно предусмотреть класс TimeProvider с виртуальным методом Now(). Можно передавать ссылку на функцию. В конце концов, можно не тестировать вызов функций sin или sqrt, а проверять, что результат вычислений попадает в ожидаемый диапазон.

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

В целом, в практических целях использование статических методов вполне оправдано.

READ ALSO
Если ли встроенный способ перебора с добавлением в Set, Map - коллекции?

Если ли встроенный способ перебора с добавлением в Set, Map - коллекции?

Для List коллекции на такой случай есть расширенный итератор ListIterator

108
Обфускация api-key от firebase

Обфускация api-key от firebase

Каким образом можно обфусцировать api-key от firebase в android приложении на javaApi-ключ лежит в google-services

116
DriverManager автоматически ищет драйвера только при старте?

DriverManager автоматически ищет драйвера только при старте?

При попытке получить connection через DriverManager получал SQLExceptionЭто все на tomcat

92
Как сохранять состояние view и presenter

Как сохранять состояние view и presenter

Как сохранить presenter при смене конфигурации телефона, например при повороте? Прочитал много способов, но так и не понял, какой лучше всего использоватьЗнаю...

125