Pull to refresh

Comments 29

Антон, извините, сразу же исправили, но вы успели открыть раньше. Больше так не будем!
Например, класс «Ищет текстовые файлы на диске и считает количество букв Z в каждом из них» — хороший кандидат на декомпозицию “ищет на диске” + “считает количество букв”.

Всякий раз, на аналогичном этапе анализа вида «а будут ли еще классы вида „ищем текстовый файл и do something с ним чтонибудь“», если в течении 1 минуты на ум ни приходит ни одного варианта, то смело делаем класс «Ищет текстовые файлы на диске и считает количество букв Z в каждом из них» и ставим todo с пометкой вернуться через пару месяцев.
Со временем (с развитием бизнес-логики), этот класс зачастую так и остается «сиротой».
ИМХО, не стоит раздувать чрезмерно архитектуру, в конце концов, у нас же есть обязанность регулярно проводить рефакторинг :)
Ну тут мы упираемся в ограниченность примера, понятно, что тут обе функции пакуются под один зонтик. В реальности обычно функции бывают достаточно жирные, для того чтобы их разделить.
А разве это не 2 разных зонтика?
один что то ищет, другой что то меняет, я бы разделил, если бы себе писал.
Считает кол-во букв. Да и поиск файлов тоже может быть «узкоспециализированным», не факт например, что будет нужна маска для поиска.
Ну я в общем об этом и говорю в статье :-)

Согласен.
Я бы тоже в реальном проекте скорее поставил на "внутреннюю инкапсуляцию")
Каждая потенциальная обязанность инкапсулируется в своем приватном методе, с разграничением по данным. Если вдруг будущее настанет — перенести метод в новый класс, интерфейс, внедрение зависимости — это несколько тривиальных модификаций.


Конечно, package private классы тоже могли бы решить проблему, но я в это "не верю": чаще всего заходишь в пакет, а там тысячи и тысячи классов из разных областей

ну вот поэтому и надо разделить, они разные вещи делают, которые потом ещё сильнее разойдутся по логике, так что если клиент стоящий и (или) планируется дальнейшая поддержка, то лучше себе облегчить работу, а если пофиг, то во одну процедуру всё конечно.
Так я про это и пишу, надо на этапе анализа понять вероятность «сильно разойдутся» или нет… По моей практике достаточно часто 2 класса так и остаются. Зато часто можно было написать что то типа вот такого простого и оно так и осталось бы сугубо утилитарной вещью без всякого особого поведения.
int count = SearchAnalyzer.getInstance().filter(filePattern).count(char);

SearchAnalyzer.getInstance().findFiles(pattern).count(char); — как раз хороший пример дизайна классов.
Активация связи происходит в третьем классе и метод findFiles наверняка возвращает какой-то осмысленный класс с отдельным контрактом.
В рамках спринга SearchAnalyzer.getInstance() это скорее @ Component, но это в общем уже детали.
плохо было бы

int count = SearchAnalyzer.getInstance().findFilesAndCount(pattern,char);
Сорри, я потом пару раз отредактировал свой пост, понял что не совсем правильно изложил свою мысль.
А, ну значит я Вас не понял, согласен.
При этом статические методы по сравнению с IoC-бинами дают очень незначительное преимущество в скорости вызова метода. Причем на этом, пожалуй, преимущества и заканчиваются.
Вопрос от целевой аудитории этого туториала (от меня то есть): а в чем преимущества 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.
Фактически это развитие метода уточки, и вполне возможно, что черновик на второй итерации выбрасывается, а код переписывается так, что становится чище и лучше без нарушений правил. Это хорошо, ведь цель — развитие и улучшение кода. Она достигнута силами одного человека, который стал чуточку умнее.
Согласен, но нельзя сбрасывать со счетов различия субкультур. Так получилось, что у нас с начальниками плохо. Иногда случайно нанимаем, но они надолго не приживаются (надеюсь я не выдаю страшную тайну). В этом есть свои плюсы и минусы, но в общем как сложилось, так сложилось. Ну и понятно что в качестве коллеги в ряде случаем можно использовать резинового утенка.
Тарантулы тем полезны, что, будучи настоянными на спирту, служат противоядием от своих же укусов...

Из древнего сборника лайфхаков…
Например очень хороший <...> контракт класса может выглядеть так: «Класс имеет один метод, который <...> сочиняет качественный рассказ». Его реализация крайне сложна, но сложность скрыта внутри класса.
Я правильно понимаю, что вы считаете, что чем выше внутренняя сложность класса, тем лучше?
Если буквально следовать этому тезису, то класс на 3000+ строчек — это хорошо и ваша мечта. Это прямо противоречит
Совсем хорошо, когда код, <...>, умещается на одном экране внутри одного метода с понятным названием
Метод с каллбэком на 1000+ строк кода мало кого радует, это очевидно. Но вы упустили эту крайность.

IMO, среди очевидных и банальных советов должно быть место для ограничения на количество строк в одном методе/классе.
Это хороший и правильный совет. метод должен сохранять какую-то разумную сложности. У меня совсем немного про это есть. Но если честно я на этом не концентрируюсь т.к. я такого кода уже наверное лет 10 не видел. Сейчас в массе народ склонен выносить сложность в путанный и не очевидный дизайн связей. А вот чтобы класс на 1000+ строк — в той части IT которую я вижу это как-то само собой ушло.

UPD: Вообще тема — как разбить что-то имеющее простой фасад и занимающее 3000+ строк на небольшие разумные кусочки — во многом пересекается с темой статьи, просто тут надо шире трактовать понятие модуля приложения. Это может быть Сервис, модуль Java, библиотека, пакет, фреймворк, класс, функция (классика «а за деревом дерево, а за деревом дерево, ...»). У меня в черной версии было про это, но я убрал т.к. решил, что статья начинает расползаться.
Sign up to leave a comment.