Любой может написать код, понятный компьютеру. Хорошие программисты пишут код, понятный людям (Мартин Фаулер).

Что такое «служебный класс»?

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

Эти классы кажутся удобными, но в перспективе становятся проблематичными. Особенно в проектах, ориентированных на соблюдение принципов, например, SOLID и предметно-ориентированного проектирования.

Разберемся, почему следует избегать служебных классов и как заменить их решениями, соответствующими рекомендуемым практикам проектирования.

Проблема служебных классов

Служебный класс  —  это набор статических методов, за которыми нет ни состояния, ни какой-либо значимой концепции предметной области. Типичные примеры: StringUtils, MathUtils или даже CPFUtils. Но за, казалось бы, полезными классами скрывается ряд проблем:

  1.  Нарушение принципа единственной ответственности. Служебными классами часто аккумулируются несвязанные задачи. Так, в классе CPFUtils обнаруживаются методы для валидации CPF, его форматирования и даже генерирования номера CPF. Это чревато слабой связностью и сложностями в сопровождении.
  2. Нарушение инкапсуляции. Служебными классами не инкапсулируются поведения в контексте предметной области, а предоставляется отделенная от предметной области обобщенная функциональность. Этим нарушаются принципы предметно-ориентированного проектирования, по которым в каждом фрагменте кода отражается значимая концепция предметной области приложения.
  3. Усложнение модульного тестирования. Статические методы в служебных классах трудно имитировать и внедрять как зависимости. Это чревато усложнением тестирования и появлением лишних зависимостей в тестовых средах.
  4. Низкая контекстуальная переиспользуемость. Служебные классы ни расширяемы, ни полиморфны. Например, чтобы обработать CPF в различных контекстах, потребуется заново реализовать логику или создать дополнительные статические методы, увеличив избыточность и риск несоответствий.

Практический пример: валидация CPF

Для справки, CPF  —  Cadastro de Pessoas Físicas  —  это бразильский аналог идентификационного номера налогоплательщика, состоит из 11 цифр и используется для идентификации при проведении финансовых и юридических операций.

Вот пример проверки валидности CPF при помощи служебного класса:

public class CPFUtils {

public static boolean isValid(String cpf) {
// Логика валидации CPF
return cpf != null && cpf.matches("\\d{11}") && validateDigits(cpf);
}

public static String format(String cpf) {
// CPF форматируется как «xxx.xxx.xxx-xx»
return cpf.replaceAll("(\\d{3})(\\d{3})(\\d{3})(\\d{2})", "$1.$2.$3-$4");
}

private static boolean validateDigits(String cpf) {
// Логика валидации контрольных цифр CPF
return true; // В демонстрационных целях упрощена
}
}

Этот класс рабочий, но подвержен проблемам:

  • Собраны разные задачи: валидация, форматирование, проверка цифр ― все в одном классе.
  • Нет концепции предметной области: CPF рассматривается как простая строка, а не значимый объект предметной области.
  • Ограничения тестирования: имитация или изменение поведения статических методов в тестах громоздка.

Предметно-ориентированный подход

Вместо служебного класса создадим объект Value, это CPF как часть домена:

import java.util.Objects;

public class CPF {

private final String value;

public CPF(String value) {
if (!isValid(value)) {
throw new IllegalArgumentException("Invalid CPF: " + value);
}
this.value = format(value);
}

private boolean isValid(String cpf) {
return cpf != null && cpf.matches("\\d{11}") && validateDigits(cpf);
}

private boolean validateDigits(String cpf) {
// Логика валидации контрольных цифр CPF
return true; // В демонстрационных целях упрощена
}

private String format(String cpf) {
return cpf.replaceAll("(\\d{3})(\\d{3})(\\d{3})(\\d{2})", "$1.$2.$3-$4");
}

public String getValue() {
return value;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CPF cpf = (CPF) o;
return Objects.equals(value, cpf.value);
}

@Override
public int hashCode() {
return Objects.hash(value);
}

@Override
public String toString() {
return value;
}
}

Использование класса CPF в модели предметной области

Интегрируем этот класс CPF в модель предметной области, например в класс Person:

public class Person {

private String name;
private CPF cpf;

public Person(String name, String cpf) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name cannot be null or blank");
}
this.name = name;
this.cpf = new CPF(cpf);
}

public String getName() {
return name;
}

public CPF getCpf() {
return cpf;
}

@Override
public String toString() {
return "Person{name='" + name + "', cpf=" + cpf + '}';
}
}

Пример использования:

public class Main {

public static void main(String[] args) {
try {
Person person = new Person("John Doe", "12345678909");
// Если «person» инстанцирован корректно, «cpf» точно валиден

System.out.println(person);
} catch (IllegalArgumentException e) {
System.err.println("Error: " + e.getMessage());
}
}
}

Преимущества подхода

  1. Гарантированные инкапсуляция и валидность: объектом CPF гарантируется валидность во время создания.
  2. Значимое представление предметной области: в классе Person четче отражается концепция предметной области.
  3. Бизнес-логика чище: логика валидации инкапсулируется в классе CPF, благодаря чему класс Person фокусируется на своей роли.
  4. Простота сопровождения: новые поведения, например маскирование CPF, добавляются внутри класса CPF, не сказываясь на других частях кода.

Заключение

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

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

Так что при создании очередного XYZUtils задайтесь вопросом: «А не сделать ли его объектом предметной области?»

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Lucas Fernandes: Why You Should Avoid Utility Classes in Your Projects

Предыдущая статьяОптимизация кэширования в TrendNow: объединение OkHttp Cache и базы данных Room. Часть 7
Следующая статьяКак создать Open Source библиотеку Android