agorlov.github.io

My homepage

View My GitHub Profile

Перевод поста Егора Бугаенко OOP Alternative to Utility Classes от 05.05.2014

Оригинал: https://www.yegor256.com/2014/05/05/oop-alternative-to-utility-classes.html

Объектная альтернатива классам-утилитам

Класс-утилита (или классы-хелперы helper-class) это “структура” которая содержит только статические методы и не инкапсулирет состояния. StringUtils, IoUtils, FileUtils из Apache Commons; Iterables и Iterators из Guava, и Files из JDK7 отличные примеры классов-утилит.

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

What's Wrong About Utility Classes? (webinar #6); 2 September 2015.

Мы хотим следовать принципу DRY и избегать дублирования. Поэтому, мы помещаем общие блоки кода в классы-утилиты и повторно их используем, когда нужно:

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

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

Классы-утилиты — зло

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

Было много дискуссий на эту тему, подборка:

Are Helper Classes Evil? Nick Malik, Why helper, singletons and utility classes are mostly bad Simon Hart, Avoiding Utility Classes Marshal Ward, Kill That Util Class! Dhaval Dalal, Helper Classes Are A Code Smell Rob Bagby.

Кроме того, есть несколько вопросов на StackExchange о классах утилитах: If a “Utilities” class is evil, where do I put my generic code?, Utility Classes are Evil.

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

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

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

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

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");
}

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

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

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

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;
  }
}

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

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

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

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

Масло масленое? Не совсем, просто продолжай читать…

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

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

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() , файл записывается.

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

Все учавствующие классы в примере довольно маленькие: Trimmed, FileLines и UnicodeFile`.

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

Objects vs. Static methods (webinar #1); 8 April 2015.

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

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

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

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

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