Статьи

ООП Альтернатива служебным классам

Служебный класс (или вспомогательный класс) — это «структура», которая имеет только статические методы и не содержит никакого состояния. StringUtils , IOUtils , FileUtils от Apache Commons ; Iterables и Iterators из Guava , и Files из JDK7 являются прекрасными примерами служебных классов.

Эта идея дизайна очень популярна в мире Java (а также в C #, Ruby и т. Д.), Поскольку служебные классы предоставляют общую функциональность, используемую повсеместно.

Здесь мы хотим следовать принципу СУХОЙ и избежать дублирования. Поэтому мы помещаем общие блоки кода в служебные классы и повторно используем их при необходимости:

1
2
3
4
5
6
// This is a terrible design, don't reuse
public class NumberUtils {
  public static int max(int a, int b) {
    return a > b ? a : b;
  }
}

Действительно, это очень удобная техника!

Утилиты Зла Злые

Однако в объектно-ориентированном мире служебные классы считаются очень плохой (некоторые даже могут сказать «ужасной») практикой.

Было много дискуссий на эту тему; назвать несколько: Злые ли вспомогательные классы? Ник Малик, Почему помощники, синглтоны и служебные классы в основном плохи от Саймона Харта, Избегание служебных классов от Маршала Уорда, Kill That Util Class! Дхавал Далал, вспомогательные классы — это запах кода от Роба Бэгби.

Кроме того, на StackExchange есть несколько вопросов о служебных классах: Если класс «Утилиты» является злом, куда мне поместить мой общий код? Утилиты Зла .

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

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

Процедурный пример

Например, вы хотите прочитать текстовый файл, разбить его на строки, обрезать каждую строку и затем сохранить результаты в другом файле. Это можно сделать с помощью FileUtils от Apache Commons:

1
2
3
4
5
6
7
8
void transform(File in, File out) {
  Collection<String> src = FileUtils.readLines(in, "UTF-8");
  Collection<String> dest = new ArrayList<>(src.size());
  for (String line : src) {
    dest.add(line.trim());
  }
  FileUtils.writeLines(out, dest, "UTF-8");
}

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

Объектно-ориентированная альтернатива

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

01
02
03
04
05
06
07
08
09
10
11
12
public class Max implements Number {
  private final int a;
  private final int b;
  public Max(int x, int y) {
    this.a = x;
    this.b = y;
  }
  @Override
  public int intValue() {
    return this.a > this.b ? this.a : this.b;
  }
}

Это процедурный звонок:

1
int max = NumberUtils.max(10, 5);

Станет объектно-ориентированным:

1
int max = new Max(10, 5).intValue();

Картошка, картошка? На самом деле, нет; просто читайте дальше …

Объекты вместо структур данных

Вот как я бы разработал ту же функцию преобразования файлов, что и выше, но в объектно-ориентированном виде:

1
2
3
4
5
6
7
8
9
void transform(File in, File out) {
  Collection<String> src = new Trimmed(
    new FileLines(new UnicodeFile(in))
  );
  Collection<String> dest = new FileLines(
    new UnicodeFile(out)
  );
  dest.addAll(src);
}

FileLines реализует Collection<String> и инкапсулирует все операции чтения и записи файлов. Экземпляр FileLines ведет себя как коллекция строк и скрывает все операции ввода-вывода. Когда мы повторяем это — файл читается. Когда мы addAll() в него addAll() — файл пишется.

Trimmed также реализует Collection<String> и инкапсулирует коллекцию строк ( шаблон Decorator ). Каждый раз, когда извлекается следующая строка, она обрезается.

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

С нашей стороны, как пользователи библиотеки, это может быть не так важно, но для их разработчиков это обязательно. Гораздо проще разрабатывать, поддерживать и тестировать FileLines класс FileLines , чем использовать метод readLines() в более 80 методах и в 3000 строк служебного класса FileUtils . Серьезно, посмотрите на его исходный код .

Объектно-ориентированный подход обеспечивает ленивое выполнение. Файл in не читается, пока не потребуются его данные. Если мы не сможем открыть out за какой-то ошибки ввода / вывода, первый файл даже не будет затронут. Все шоу начинается только после того, как мы вызываем addAll() .

Все строки во втором фрагменте, кроме последнего, создают экземпляры и объединяют более мелкие объекты в более крупные. Такая композиция объектов довольно дешевая для процессора, поскольку она не вызывает каких-либо преобразований данных.

Кроме того, очевидно, что второй скрипт выполняется в пространстве O (1), а первый — в O (n). Это является следствием нашего процедурного подхода к данным в первом скрипте.

В объектно-ориентированном мире нет данных; Есть только объекты и их поведение!

Похожие сообщения

Вы также можете найти эти сообщения интересными:

Ссылка: Альтернатива ООП классам утилит от нашего партнера по JCG Егора Бугаенко в блоге About Programming .