Комментарии 29
Например, класс «Ищет текстовые файлы на диске и считает количество букв Z в каждом из них» — хороший кандидат на декомпозицию “ищет на диске” + “считает количество букв”.
Всякий раз, на аналогичном этапе анализа вида «а будут ли еще классы вида „ищем текстовый файл и do something с ним чтонибудь“», если в течении 1 минуты на ум ни приходит ни одного варианта, то смело делаем класс «Ищет текстовые файлы на диске и считает количество букв Z в каждом из них» и ставим todo с пометкой вернуться через пару месяцев.
Со временем (с развитием бизнес-логики), этот класс зачастую так и остается «сиротой».
ИМХО, не стоит раздувать чрезмерно архитектуру, в конце концов, у нас же есть обязанность регулярно проводить рефакторинг :)
Согласен.
Я бы тоже в реальном проекте скорее поставил на "внутреннюю инкапсуляцию")
Каждая потенциальная обязанность инкапсулируется в своем приватном методе, с разграничением по данным. Если вдруг будущее настанет — перенести метод в новый класс, интерфейс, внедрение зависимости — это несколько тривиальных модификаций.
Конечно, package private классы тоже могли бы решить проблему, но я в это "не верю": чаще всего заходишь в пакет, а там тысячи и тысячи классов из разных областей
int count = SearchAnalyzer.getInstance().filter(filePattern).count(char);
Активация связи происходит в третьем классе и метод findFiles наверняка возвращает какой-то осмысленный класс с отдельным контрактом.
В рамках спринга SearchAnalyzer.getInstance() это скорее @ Component, но это в общем уже детали.
плохо было бы
int count = SearchAnalyzer.getInstance().findFilesAndCount(pattern,char);
При этом статические методы по сравнению с IoC-бинами дают очень незначительное преимущество в скорости вызова метода. Причем на этом, пожалуй, преимущества и заканчиваются.Вопрос от целевой аудитории этого туториала (от меня то есть): а в чем преимущества IoC-бинов перед статическими классами? Вы про это так и не сказали.
Советую почитать про IoC контейнеры и зачем они нужны.Что это, зачем и как работает, я примерно представляю. Но вопрос возник в связи с тем, что дальше по тексту у вас идет
Действительно, в Java-мире сложилась традиция — вспомогательные функции работы со строками и потоками ввода-вывода выносить в статичные методы и собирать под зонтиком SomethingUtils-классов. Но мне такая традиция кажется достаточно замшелой.Хотелось бы понять, в чем именно замшелость.
1. Эпичный класс SomethingHelper, который наверное еще и partial, в котором смешалась в кучу вся хоть отчасти касающаяся некой сущности логика. Это спагетти, в котором невозможно найти нужный метод.
2. Поскольку это статический класс, то он не умеет управлять циклом жизни своих зависимостей, например, если кто-то триста лет назад использовал валидацию через БД, то теперь некий метод принимает там стратегию работы с БД, и это уже не просто Helper/Utils, а полноценная помесь Singleton и Strategy. Ухх.
3. Написание юнит-тестов на классы, которые пользуются этими Utils превращается в извращение, потому что какой-нибудь из методов ConvertNameToPhone наверняка валидирует то, что в имени есть три слова, каждое с большой буквы, и ваши юнит-тесты, которые должны проверять, что медиатор прочитал данные, и если ничего не нашел, то отправил сообщение в MQ, начинают пестреть «Ивановым Николаем Петровичем» и десятком других полей, которые для теста не имеют никакого значения, но ужасно необходимы Utils-методу.
К сожалению, уже подгорает и продолжить список не могу, но уверен, что в других компаниях свои антипаттерны хелперов.
Позволю себе влезть в затевающуюся дискуссиюДа нету никакой дискуссии! Дискуссия была бы, если бы я в этом что-то понимал и у меня была бы своя точка зрения. А я действительно только учусь (и считаю, что этот момент в статье раскрыт плохо).
Но большое спасибо вам и отписавшимся ниже, теперь стало намного понятнее.
1. из одной функции или процедуры вызвать другую, а из нее третью.
2. связывание компонент (в нашем случае классов) с помощь какого-то декларативного способа.
второй способ в целом удобнее, заодно он позволяет делать еще две очень удобные вещи
1. конфигурирование
2. управление жизненным циклом. Если компонентам для начала работы необходимо как-то разогреться, то Spring IoC очень удобен.
Сначала абстрактно.
Статические методы — это использование процедурного подхода. Если вы пишете в ОО парадигме с функциональной примесью (Java8+Spring), не стоит возвращаться в предыдущий век.
Потом обобщенно
Quilin привел доводы, которые уместны и для мира Java. Для меня на втором месте по проблемности — это юнит-тесты на класс, вызывающий статические методы. Интеграционные тесты (покрывающие взаимодействие двух модулей) пропорционально усложняются. В итоге, покрытие тестами проекта в целом становится дороже и на тесты забивают.
На первом месте по проблемам — это поддержка и развитие кода со статическими методами. Сейчас сценарий один и нет перспектив, что он будет меняться. Через полгода появляются хотелки, через год концепция поменялась, мы решили что за месяц сможем перестроиться если рефакторить будете по неоплачиваемым выходным, через полтора тимлид вынужден назначать штрафника для доработки, но он предпочтет уволиться. Я утрирую, но не сильно. Код становится негибким, "шумным" — детали реализации протекают на другие слои.
Пример из реальной жизни. Переделал пример к задаче из поста "распаковать PDF и сохранить параграфы в БД" для сокращения текста.
Класс ZipExtractor был написан в рамках другой задачи. Он распаковывал архив на диск, возвращая имя временного файла. Задачу "извлечь текст из PDF" Алекс решил вынести в отдельный класс, т.к. в будущем он мог понадобиться и для других задач. Ничего сложного, Алекс быстро написал статический метод
public class PdfProcessorUtils {
// Принимает имя файла PDF и возвращает текст из него в виде потока строк
static Stream<Stream> extractPDF(String fileName){...}
Тимлид Боб на ревью потребовал переделать на обычный метод. Алекс, ворча, убрал слово static
и везде дописал new PdfProcessorUtils (). Сразу же понял, что хотя метод вызывается пять раз, конструктор достаточно написать дважды.
Через две итерации оказалось, что от одного заказчика файлы приходят не в UTF-8. Можно добавить параметр в метод extractPDF(String fileName, String encoding)
или указывать кодировку в конструкторе. Боб ответил, что кодировка определяется однажды, не меняется за время жизни объекта, т.е. это внутреннее состояние, поэтому правильно задавать ее в конструкторе. Оказалось, что только в одном случае из двух нужно делать проверку и явно задавать кодировку. В этой ветке метод вызывался трижды. Вместо 3 раз String encoding
использовалась только в одном месте — там, где это было необходимо.
Затем зарубежный филиал попросил прикрутить выбор локали. Алекс быстро создал конструктор с двумя параметрами. Через некоторое время оба параметра вынесли в настройки сервера, а PdfProcessorUtils переименовали в PdfTextExtractor. Он стал полноценным бином и работал долго и счастливо.
Можно ли было обойтись статическими методами? Да, но это плохо для чтения-отладки кода, ведь выбор кодировки и вызов метода разделяет 3 экрана в худшем случае. Код становится "шумным": показывает детали, неважные на этом уровне.
В завершение я хотел бы поправить DataArt: В приложении на Spring и сроком жизни 5+ лет лучше не писать статических методов вообще.
Я хотел бы, чтобы работало правило статический метод может пройти код-ревью только при наличии письменного разрешения тимлида. Написанного ручкой. Напомню, мы говорим в контексте
Время, которое процессор тратит на обработку одного запроса, важно, но не критично — прибавка в 0,1 % погоды не сделает. В нашем распоряжении нет терабайтов памяти, но если приложение займет лишние 50–100 Кбайт, катастрофой это не станет
В этом случае преимущества статического метода перед new MyClass().doSomething() не имеют значения, а сэкономленные часы и нервные клетки заметны.
В завершение я хотел бы поправить DataArt: В приложении на Spring и сроком жизни 5+ лет лучше не писать статических методов вообще.
Это безусловно хорошая идея, но в реалиях аутсорсинга — не всегда получается энфорсить правила столь строго. Я поэтому предпочитаю писать статью как набор рекомендаций в стиле «Если мы не хотите, чтобы вас сбила машина, возможно вам не стоит перебегать дорогу на красный свет».
С другой стороны, чтобы подчеркнуть значимость важных деталей, стоит использовать сильные, категоричные формы (пальцы переломаю, будет уволен, ни за что и никогда, с письменного разрешения). Они хороши для узкой аудитории (своей команды). Или для новичков, чтобы показать какое зло больше (А не стоит делать, возможно Б не лучший вариант, за В получишь линейкой по пальцам, Г никогда не делай, за Д уволю нахрен, ну и дальше насколько кровожадности хватит)
Разумеется, исключения возможны и будут. Чтобы жесткие высказывания не загоняли в угол, а агрессия не приводила к срачам с мордобоем, важно показывать обходной путь. Этот путь должен быть достаточно затратным, чтобы не превратиться в основной. И в то же время доступным, чтобы пройти по нему когда нарушение правила действительно оправданно.
В идеале хотелось бы, чтобы процесс принятия решения не сильно отвлекал других. Поэтому мне не нравится тест может ли коллега за 5 минут понять зачем это всё. Для новичка, или в сложных случаях, такой сценарий может привести к простою коллеги в течении часов. Поэтому мне нравится тест представь, что тебе нужно получить письменное разрешение начальника. Сможешь ли ты его убедить? В этом случае тратится время одного разработчика.
Проигрывание в голове диалога — начальный уровень. Нежелание идти к начальству «по пустякам» даст дополнительный стимул к поиску альтернатив. Неуверенность и продумывание возможных аргументов «против» позволяет понять, действительно ли нарушение правила — лучший выбор.
Настоящий эффект от техники получается, когда вместо диалога в голове пишется письмо-обоснование. На стадии черновика проработаются альтернативы и доводы «за». Часть конечного текста может стать комментарием в коде, commit message или заметкой в wiki — даже если не будет использована по прямому предназначению: обоснованию своих действий на code-review.
Фактически это развитие метода уточки, и вполне возможно, что черновик на второй итерации выбрасывается, а код переписывается так, что становится чище и лучше без нарушений правил. Это хорошо, ведь цель — развитие и улучшение кода. Она достигнута силами одного человека, который стал чуточку умнее.
Тарантулы тем полезны, что, будучи настоянными на спирту, служат противоядием от своих же укусов...
Из древнего сборника лайфхаков…
Например очень хороший <...> контракт класса может выглядеть так: «Класс имеет один метод, который <...> сочиняет качественный рассказ». Его реализация крайне сложна, но сложность скрыта внутри класса.Я правильно понимаю, что вы считаете, что чем выше внутренняя сложность класса, тем лучше?
Совсем хорошо, когда код, <...>, умещается на одном экране внутри одного метода с понятным названиемМетод с каллбэком на 1000+ строк кода мало кого радует, это очевидно. Но вы упустили эту крайность.
IMO, среди очевидных и банальных советов должно быть место для ограничения на количество строк в одном методе/классе.
UPD: Вообще тема — как разбить что-то имеющее простой фасад и занимающее 3000+ строк на небольшие разумные кусочки — во многом пересекается с темой статьи, просто тут надо шире трактовать понятие модуля приложения. Это может быть Сервис, модуль Java, библиотека, пакет, фреймворк, класс, функция (классика «а за деревом дерево, а за деревом дерево, ...»). У меня в черной версии было про это, но я убрал т.к. решил, что статья начинает расползаться.
Дизайн классов: что такое хорошо?