Служебный класс (или вспомогательный класс) — это «структура», которая имеет только статические методы и не содержит никакого состояния. 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). Это является следствием нашего процедурного подхода к данным в первом скрипте.
В объектно-ориентированном мире нет данных; Есть только объекты и их поведение!
Похожие сообщения
Вы также можете найти эти сообщения интересными:
- Почему NULL это плохо?
- Избегайте конкатенации строк
- Объекты должны быть неизменными
- Типичные ошибки в коде Java
Ссылка: | Альтернатива ООП классам утилит от нашего партнера по JCG Егора Бугаенко в блоге About Programming . |