OOP Alternative to Utility Classes

The following text is a partial translation of the original English article, performed by ChatGPT (gpt-3.5-turbo) and this Jekyll plugin:

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

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

Здесь мы хотим следовать принципу DRY (Don’t Repeat Yourself) и избегать дублирования. Поэтому мы помещаем общие блоки кода в утилитарные классы и повторно используем их при необходимости:

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

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

Проведено много дискуссий на эту тему; некоторые из них: Are Helper Classes Evil? Ника Малика, Why helper, singletons and utility classes are mostly bad Саймона Харта, Avoiding Utility Classes Маршала Уорда, Kill That Util Class! Дхавала Далала, Helper Classes Are A Code Smell Роба Багби.

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

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

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

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

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

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

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

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

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

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

FileLines implements Collection<String> and encapsulates all file reading and writing operations. An instance of FileLines behaves exactly as a collection of strings and hides all I/O operations. When we iterate it—a file is being read. When we addAll() to it—a file is being written.

Trimmed also implements Collection<String> and encapsulates a collection of strings (Decorator pattern). Every time the next line is retrieved, it gets trimmed.

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

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

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

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

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

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

Translated by ChatGPT gpt-3.5-turbo/42 on 2023-11-18 at 05:12

sixnines availability badge   GitHub stars