1. Обзор
Вспомогательные классы содержат только статические члены, которые мы группируем вместе по определенной теме . Таким образом, сами классы не имеют состояния, а их члены содержат код, предназначенный для повторного использования на нескольких уровнях.
В этом руководстве мы объясним, почему статические анализаторы кода сообщают, что служебные классы не должны иметь общедоступных конструкторов. Мы рассмотрим решение этой проблемы путем реализации частного конструктора. Кроме того, мы изучим, какие аннотации Lombok могут помочь нам в их создании. Мы также покажем, как отключить эти предупреждения.
Наконец, мы оценим некоторые альтернативные подходы к реализации служебных классов в Java.
2. Полезные классы
В отличие от классов, определяющих объекты, служебные классы не сохраняют никаких данных или состояния. Они содержат только поведение . Утилиты содержат только статические элементы. Все их методы являются статическими, а данные передаются только в качестве аргументов метода.
2.1. Почему служебные классы?
В объектно-ориентированном программировании мы пытаемся смоделировать нашу проблемную область и сгруппировать вместе семейства схожих функций.
Мы также можем написать чистые функции для моделирования общего поведения в нашей кодовой базе, особенно при использовании функционального программирования . В отличие от объектных методов, эти чистые функции не связаны с экземпляром какого-либо объекта. Однако им нужен дом. В Java нет определенного типа, выделенного для размещения набора функций, поэтому мы часто создаем служебный класс.
Отличными примерами популярных служебных классов в Java являются Arrays
и Collections
из java.util
,
а также StringUtils
из org.apache.commons.lang3
.
2.2. Реализация на Java
Java не предоставляет специального ключевого слова или способа создания служебных классов. Таким образом, мы обычно создаем служебный класс как обычный класс Java, но только со статическими членами :
public final class StringUtils {
public static boolean isEmpty(String source) {
return source == null || source.length() == 0;
}
public static String wrap(String source, String wrapWith) {
return isEmpty(source) ? source : wrapWith + source + wrapWith;
}
}
В нашем примере мы пометили служебный класс как public
и final
. Утилиты обычно публикуются, поскольку они предназначены для повторного использования на нескольких уровнях.
Ключевое слово final
предотвращает создание подклассов. Поскольку служебные классы не предназначены для наследования , мы не должны создавать их подклассы.
2.3. Предупреждение об открытом конструкторе
Давайте попробуем проанализировать наш пример служебного класса с помощью SonarQube , популярного инструмента статического анализа кода. Мы можем запустить анализ SonarQube в проекте Java, используя подключаемый модуль инструмента сборки, в данном случае Maven:
mvn clean verify sonar:sonar -Dsonar.host.url=http://localhost:9000 -Dsonar.login=XYXYXYXY
Статический анализ кода приводит к сильному запаху кода. SonarQube предупреждает нас о необходимости скрыть неявный публичный конструктор в нашем служебном классе :
Хотя мы не добавили конструктор в наш служебный класс, Java неявно добавил общедоступный конструктор по умолчанию. Таким образом, позволяя пользователям API создавать его экземпляр:
StringUtils utils = new StringUtils();
Это неправильное использование наших служебных классов, поскольку они не предназначены для создания экземпляров. Поэтому правило SonarQube советует нам добавить приватный конструктор, чтобы скрыть общедоступный по умолчанию.
3. Добавление частного конструктора
Теперь давайте устраним обнаруженный запах кода, добавив частный конструктор в наш служебный класс.
3.1. Частный конструктор по умолчанию
Давайте добавим частный конструктор без аргументов в наш служебный класс. Мы никогда не будем использовать этот частный конструктор . Таким образом, рекомендуется генерировать исключение в случае его вызова:
public final class StringUtils {
private StringUtils() {
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
}
// public static methods
}
Следует отметить, что закрытый конструктор также не может быть протестирован. Таким образом, этот подход приведет к одной непокрытой строке кода в наших измерениях покрытия кода.
3.2. Использование Lombok NoArgsConstructor
Мы можем использовать аннотацию NoArgsConstructor
Lombok для автоматического создания частного конструктора : `` ****
@NoArgsConstructor(access= AccessLevel.PRIVATE)
public final class StringUtils {
// public static methods
}
Таким образом, мы можем избежать добавления вручную дополнительной строки непокрытого кода.
3.3. Использование служебного класса Lombok
Мы также можем использовать аннотацию UtilityClass
Lombok, которая помечает весь класс как утилиту :
@UtilityClass
public class StringUtils {
// public static methods
}
В этом случае Ломбок автоматически:
- создать частный конструктор, который выдает исключение
- пометить как ошибку любые явные конструкторы, которые мы добавляем
отметить финал
класса ``
Следует отметить, что в настоящее время аннотация UtilityClass
все еще является экспериментальной функцией.
4. Отключение предупреждения
Если мы решим не следовать рекомендуемому решению, у нас также есть возможность отключить предупреждение общедоступного конструктора.
4.1. Подавление предупреждения
Давайте воспользуемся аннотацией Java SuppressWarnings
, чтобы отключить предупреждение на уровне одного класса :
@SuppressWarnings("java:S1118")
public final class StringUtils {
// public static methods
}
Мы должны передать правильный идентификатор правила SonarQube в качестве параметра значения. Мы можем найти его в пользовательском интерфейсе сервера SonarQube:
4.2. Деактивация правила
В стандартном профиле качества SonarQube мы не можем деактивировать ни одно из предопределенных правил. Таким образом, чтобы отключить предупреждение на уровне полного проекта , нам сначала нужно создать собственный профиль качества:
В нашем пользовательском профиле качества мы можем найти и деактивировать любое из предопределенных правил Java.
5. Альтернативные реализации
Давайте рассмотрим некоторые возможные альтернативные способы создания утилит помимо использования классов.
5.1. Статические методы интерфейса
Начиная с Java 8 мы можем определять и реализовывать статические
методы в интерфейсах :
public interface StringUtils {
static boolean isEmpty(String source) {
return source == null || source.length() == 0;
}
static String wrap(String source, String wrapWith) {
return isEmpty(source) ? source : wrapWith + source + wrapWith;
}
}
Поскольку мы не можем создавать экземпляры интерфейсов, мы устранили проблему создания экземпляров служебных классов. Однако мы создаем другую проблему. Поскольку интерфейсы предназначены для реализации другими классами, пользователь API может по ошибке реализовать этот интерфейс.
Кроме того, интерфейсы не могут содержать приватные константы и статические инициализаторы.
5.2. Статические методы перечисления
Перечисления — это контейнеры управляемых экземпляров. Однако мы можем создать утилиту в виде перечисления с нулевыми экземплярами, содержащими только статические методы :
public enum StringUtils {;
public static boolean isEmpty(String source) {
return source == null || source.length() == 0;
}
public static String wrap(String source, String wrapWith) {
return isEmpty(source) ? source : wrapWith + source + wrapWith;
}
}
Поскольку мы не можем создавать экземпляры перечислимых типов, мы устранили проблему создания экземпляров служебных классов. С другой стороны, как следует из названия, перечисляемые типы предназначены для создания фактических перечислений, а не служебных классов.
6. Заключение
В этой статье мы рассмотрели служебные классы и объяснили, почему у них не должно быть общедоступных конструкторов .
В примерах мы рассмотрели реализацию частного конструктора вручную и использование аннотаций Lombok. Далее мы увидели, как подавить и отключить соответствующее предупреждение SonarQube. Наконец, мы рассмотрели два альтернативных способа создания утилит с использованием интерфейсов и перечислений.
Как всегда, исходный код доступен на GitHub .