Автор: Денис Цыплаков, Solution Architect, DataArt

За годы работы я обнаружил, что программисты из раза в раз повторяют одни и те же ошибки. К сожалению, книги, посвященные теоретическим аспектам разработки, избежать их не помогают: в книгах обычно нет конкретных, практических советов. И я даже догадываюсь, почему…

Первая рекомендация, которая приходит в голову, когда речь заходит, например, о логировании или дизайне классов, очень простая: «Не делать откровенной ерунды». Но опыт показывает, что ее определенно недостаточно. Как раз дизайн классов в этом случае хороший пример — вечная головная боль, возникающая из-за того, что каждый смотрит на этот вопрос по-своему. Поэтому я и решил собрать в одной статье базовые советы, следуя которым, вы избежите ряда типичных проблем, а главное, избавите от них коллег. Если некоторые принципы покажутся вам банальными (потому что они действительно банальны!) — хорошо, значит, они уже засели у вас в подкорке, и вашу команду можно поздравить.

Оговорюсь, на самом деле, мы сосредоточимся на классах исключительно для простоты. Почти то же самое можно сказать о функциях или любых других строительных блоках приложения.
Если приложение работает и выполняет задачу, значит, его дизайн хорош. Или нет? Зависит от целевой функции приложения; то, что вполне годится для мобильного приложения, которое надо один раз показать на выставке, может совершенно не подойти для трейдинговой платформы, которую какой-нибудь банк развивает годами. В какой-то мере, ответом на поставленный вопрос можно назвать принцип SOLID, но он слишком общий — хочется каких-то более конкретных инструкций, на которые можно ссылаться в разговоре с коллегами.

Целевое приложение


Поскольку универсального ответа быть не может, предлагаю сузить область. Давайте считать, что мы пишем стандартное бизнес-приложение, которое принимает запросы через HTTP или другой интерфейс, реализует какую-то логику над ними и далее либо делает запрос в следующий по цепочке сервис, либо где-то сохраняет полученные данные. Для простоты давайте считать, что мы используем Spring IoC Framework, благо он сейчас достаточно распространен и остальные фреймворки на него изрядно похожи. Что мы можем сказать о таком приложении?

  • Время, которое процессор тратит на обработку одного запроса, важно, но не критично — прибавка в 0,1 % погоды не сделает.
  • В нашем распоряжении нет терабайтов памяти, но если приложение займет лишние 50–100 Кбайт, катастрофой это не станет.
  • Конечно, чем короче время старта, тем лучше. Но принципиальной разницы между 6 сек и 5.9 сек тоже нет.

Критерии оптимизации


Что важно для нас в этом случае?

Код проекта, скорее всего, будет использоваться бизнесом несколько, а может, и более десяти лет.

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

В какой-то момент часть кода или весь проект могут быть слиты с кодовой базой другого проекта.

В среде менеджеров принято считать, что такого рода вопросы решаются с помощью документации. Документация, безусловно, хороша и полезна, ведь так здорово, когда вы начинаете работу над проектом, на вас висит пять открытых тикетов, проджект-менеджер спрашивает, как там у вас с прогрессом, а вам надо прочитать (и запомнить) каких-то 150 страниц текста, написанных далеко не гениальными литераторами. У вас, конечно, было несколько дней или даже пара недель на вливание в проект, но, если использовать простую арифметику, — с одной стороны 5,000,000 байт кода, с другой, скажем, 50 рабочих часов. Получается, что в среднем надо было вливать в себя 100 Кбайт кода в час. И тут все очень сильно зависит от качества кода. Если он чистый: легко собирается, хорошо структурирован и предсказуем, то вливание в проект кажется заметно менее болезненным процессом. Не последнюю роль в этом играет дизайн классов. Далеко не последнюю.

Чего мы хотим от дизайна классов


Из всего перечисленного можно сделать много интересных выводов про общую архитектуру, стек технологий, процесс разработки и т. д. Но мы с самого начала решили поговорить о дизайне классов, давайте разберемся, что полезного мы можем извлечь из сказанного ранее применительно к нему.

  • Хочется, чтобы разработчик, досконально не знакомый с кодом приложения, мог, глядя на класс, понять, что этот класс делает. И наоборот — глядя на функциональное или нефункциональное требование, мог бы быстро догадаться, в каком месте приложения находятся классы, за него отвечающие. Ну и желательно, чтобы реализация требований не была «размазана» по всему приложению, а была сосредоточена в одном классе или компактной группе классов. Объясню на примере, что за именно антипаттерн я имею ввиду. Предположим, нам надо проверять, что 10 запросов определенного типа могут исполняться только пользователями, у которых на счету больше 20 очков (неважно, что бы это ни значило). Плохой путь реализации такого требования — в начале каждого запроса вставить проверку. Тогда логика будет размазана на 10 методов, в разных контроллерах. Хороший способ — создать фильтр или WebRequestInterceptor и проверять все в одном месте.
  • Хочется, чтобы изменения в одном классе, не затрагивающие контракт класса, не затрагивали, ну или (будем реалистами!) хотя бы не очень сильно затрагивали и другие классы. Иначе говоря, хочется инкапсуляции реализации контракта класса.
  • Хочется, чтобы при изменении контракта класса можно было, пройдя по цепочке вызовов и сделав find usages, найти классы, которые это изменение затрагивает. Т. е. хочется, чтобы у классов не было косвенных зависимостей.
  • По возможности хочется, чтобы процессы обработки запросов, состоящие из нескольких одноуровневых шагов не размазывались по коду нескольких классов, а были описаны на одном уровне. Совсем хорошо, если код, описывающий такой процесс обработки, умещается на одном экране внутри одного метода с понятным названием. Например нам надо в строке найти все слова, для каждого слова сделать вызов в сторонний сервис, получить описание слова, применить к описанию форматирование и сохранить результаты в БД. Это одна последовательность действий из 4-х шагов. Очень удобно разбираться в коде и менять его логику, когда есть метод, где эти шаги идут один за другим.
  • Очень хочется, чтобы одинаковые вещи в коде были реализованы одинаковым образом. Например, если мы обращаемся в БД сразу из контроллера, лучше так делать везде (хотя хорошей практикой такой дизайн я бы не назвал). А если мы уже ввели уровни сервисов и репозиториев, то лучше напрямую из контроллера в БД не обращаться.
  • Хочется, чтобы количество классов/интерфейсов, не отвечающих непосредственно за функциональные и нефункциональные требования, было не очень большим. Работать с проектом, в котором на каждый класс с логикой есть два интерфейса, сложная иерархия наследования из пяти классов, фабрика класса и абстрактная фабрика классов, довольно тяжело.

Практические рекомендации


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

Статичные методы


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

В принципе, ничего плохого в использовании статических методов нет. Если поведение метода полностью зависит от его параметров, почему бы действительно не сделать его статическим. Но нужно учесть тот факт, что мы используем Spring IoC, который служит для связывания компонентов нашего приложения. Spring IoC оперирует понятиями бинов (Beans) и их областей применимости (Scope). Этот подход можно смешивать со статическими методами, сгруппированными в классы, но разбираться в таком приложении и тем более что-то в нем менять (если, например, понадобится передать в метод или класс какой-то глобальный параметр) может быть весьма затруднительно.

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

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

Тут читатель может спросить: «А как же классы StringUtils и IOUtils?» Действительно, в Java-мире сложилась традиция — вспомогательные функции работы со строками и потоками ввода-вывода выносить в статичные методы и собирать под зонтиком SomethingUtils-классов. Но мне такая традиция кажется достаточно замшелой. Если вы будете следовать ей, большого вреда, конечно, не ожидается — все Java-программисты к этому привыкли. Но и смысла в таком ритуальном действии нет. С одной стороны, почему бы не сделать бин StringUtils, с другой, если не делать бин и все вспомогательные методы сделать статичными, давайте уже делать статичные зонтичные классы StockTradingUtils и BlockChainUtils. Начав выносить логику в статичные методы, провести границу и остановиться сложно. Я советую не начинать.

Наконец, не стоит забывать, что к Java 11 многие вспомогательные методы, десятилетиями кочевавшие за разработчиками из проекта в проект, либо стали частью стандартной библиотеки, либо объединились в библиотеки, например, в Google Guava.

Атомарный, компактный контракт класса


Есть простое правило, применимое к разработке любой программной системы. Глядя на любой класс, вы должны быть способны быстро и компактно, не прибегая к долгим раскопкам, объяснить, что этот класс делает. Если уместить объяснение в один пункт (необязательно, впрочем, выраженный одним предложением) не получается, возможно, стоит подумать и разбить этот класс на несколько атомарных классов. Например, класс «Ищет текстовые файлы на диске и считает количество букв Z в каждом из них» — хороший кандидат на декомпозицию “ищет на диске” + “считает количество букв”.

С другой стороны, не стоит делать слишком мелких классов, каждый из которых рассчитан на одно действие. Но какого же размера тогда должен быть класс? Базовые правила таковы:

  • Идеально, когда контракт класса совпадает с описанием бизнес-функции (или подфункции, смотря как у нас устроены требования). Это не всегда возможно: если попытка соблюсти это правило ведет к созданию громоздкого, неочевидного кода, класс лучше разбить на более мелкие части.
  • Хорошая метрика для оценки качества контракта класса — отношение его внутренней сложности к сложности контракта. Например очень хороший (пусть и фантастический) контракт класса может выглядеть так: «Класс имеет один метод, который получает на входе строку с описанием тематики на русском языке и в качестве результата сочиняет качественный рассказ или даже повесть на заданную тему». Здесь контракт прост и в целом понятен. Его реализация крайне сложна, но сложность скрыта внутри класса.

Почему это правило важно?

  • Во-первых, умение внятно объяснить самому себе, что делает каждый из классов, всегда полезно. К сожалению, далеко не в каждом проекте разработчики могут такое проделать. Часто можно услышать, что вроде: «Ну, это такая обертка над классом Path, которую мы зачем-то сделали и иногда используем вместо Path. Она еще имеет метод, который умеет удваивать в пути все File.separator — нам этот метод нужен при сохранении отчетов в облако, и он почему-то оказался в классе Path».
  • Человеческий мозг способен единовременно оперировать не более чем пятью–десятью объектами. У большинства людей — не больше семи. Соответственно, если для решения задачи разработчику нужно оперировать более чем семью объектами, он либо что-то упустит, либо будет вынужден упаковать несколько объектов под один логический «зонтик». И если упаковывать все равно придется, почему бы не сделать это сразу, осознанно, и не дать этому зонтику осмысленное название и четкий контракт.

Как проверить, что у вас все достаточно гранулярно? Попросите коллегу уделить вам 5 (пять) минут. Возьмите часть приложения, над созданием которой вы сейчас работаете. Для каждого из классов объясните коллеге, что именно этот класс делает. Если вы не укладываетесь в 5 минут, или коллега не может понять, зачем тот или иной класс нужен — возможно, вам стоит что-то изменить. Ну или не менять и провести опыт еще раз, уже с другим коллегой.

Зависимости между классами


Предположим, нам надо для PDF-файла, упакованного в ZIP-архив, выделить связанные участки текста длиннее 100 байт и сохранить их в базу данных. Популярный антипаттерн в таких случаях выглядит так:

  • Есть класс, который раскрывает ZIP-архив, ищет в нем PDF-файл и возвращает его в виде InputStream.
  • Этот класс имеет ссылку на класс, который ищет в PDF абзацы текста.
  • Класс, работающий с PDF, в свою очередь имеет ссылку на класс, сохраняющий данные в БД.

С одной стороны, все выглядит логично: получил данные, вызвал напрямую следующий класс в цепочке. Но при этом в контракт класса вверху цепочки примешиваются контракты и зависимости всех классов, которые идут в цепочке за ним. Гораздо правильнее сделать эти классы атомарными и не зависящими друг от друга, и создать еще один класс, который собственно реализует логику обработки, связывая эти три класса между собой.

Как делать не надо:



Что здесь не так? Класс, работающий с ZIP-файлами, передает данные классу, обрабатывающему PDF, а тот, в свою очередь, — классу, работающему с БД. Значит, класс, работающий с ZIP, в результате зачем-то зависит от классов, работающих с БД. Кроме того, логика обработки размазана по трем классам, и чтобы ее понять, надо по всем трем классам пробежаться. Что делать, если вам понадобится абзацы текста, полученные из PDF, передать третьестороннему сервису через REST-вызов? Вам надо будет менять класс, который работает с PDF, и втягивать в него еще и работу с REST.

Как надо делать:



Здесь у нас есть четыре класса:

  • Класс, который работает только с ZIP-архивом и возвращает список PDF-файлов (тут можно возразить — возвращать файлы плохо — они большие и сломают приложение. Но давайте в этом случае читать слово «возвращает» в широком смысле. Например, возвращает Stream из InputStream).
  • Второй класс отвечает за работу с PDF.
  • Третий класс ничего не знает и не умеет, кроме сохранения параграфов в БД.
  • И четвертый класс, состоящий буквально из нескольких строчек кода, содержит всю бизнес-логику, которая умещается на одном экране.

Еще раз подчеркиваю, в 2019 году в Java есть как минимум два хороших (и несколько менее
хороших) способа не передавать файлы и полный список всех параграфов как объекты в памяти. Это:

  1. Java Stream API.
  2. Callbacks. Т. е. класс с бизнес-функцией не передает данные напрямую, а говорит ZIP Extractor: вот тебе callback, ищи в ZIP-файле PDF-файлы, для каждого файла создавай InputStream и вызывай с ним переданный callback.

Неявное поведение


Когда мы не пытаемся решить совершенно новую, ранее никем не решенную задачу, а напротив, делаем что-то, что другие разработчики уже делали ранее несколько сотен (или сотен тысяч) раз, у всех членов команды есть некие ожидания относительно цикломатической сложности и ресурсоемкости решения. Например, если нам надо в файле найти все слова, начинающиеся с буквы z, это последовательное, однократное чтение файла блоками с диска. Т. е. если ориентироваться на https://gist.github.com/jboner/2841832 —такая операция займет несколько микросекунд на 1 Мб, ну может быть, в зависимости от среды программирования и загруженности системы несколько десятков или даже сотню микросекунд, но никак не секунду. Памяти на это потребуется несколько десятков килобайт (оставляем за скобками вопрос, что мы делаем с результатами, это забота другого класса), и код, скорее всего, займет примерно один экран. При этом мы ожидаем, что никаких других ресурсов системы использовано не будет. Т. е. код не будет создавать нити, писать данные на диск, посылать пакеты по сети и сохранять данные в БД.

Это обычные ожидание от вызова метода:

zWordFinder.findZWords(inputStream). ...

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

Если у вас нет никакой разумной причины для неявного поведения — перепишите класс.

Как понять ожидания от сложности и ресурсоемкости метода? Нужно прибегнуть к одному из этих простых способов:

  1. С опытом приобрести достаточно широкий кругозор.
  2. Спросить у коллеги — это всегда можно сделать.
  3. Перед стартом разработки проговорить с членами команды план реализации.
  4. Задать себе вопрос: «А не использую ли я в этом методе _слишком_ много избыточных ресурсов?» Обычно этого бывает достаточно.

Излишне увлекаться оптимизацией тоже не стоит — экономия 100 байтов при используемых классом 100,000 не имеет особенного смысла для большинства приложений.

Это правило открывает нам окно в богатый мир оверинжениринга, скрывающем ответы на вопросы вида «почему не стоит тратить месяц, чтобы сэкономить 10 байт памяти в приложении, которому для работы требуется 10 Гбайт». Но эту тему здесь я развивать не стану. Она достойна отдельной статьи.

Неявные имена методов


В Java-программировании на текущий момент сложилось несколько неявных соглашений по поводу имен классов и их поведения. Их не так много, но лучше их не нарушать. Попробую перечислить те из них, которые приходят мне на ум:

  • Конструктор — создает экземпляр класса, может создавать какие-то достаточно разветвленные структуры данных, но при этом не работает с БД, не пишет на диск, не посылает данные по сети (оговорюсь, все это может делать встроенный логгер, но это отдельная история и в любом случае лежит она на совести конфигуратора логирования).
  • Getter — getSomething() — возвращает какую-то структуру памяти из глубин объекта. Опять же не пишет на диск, не делает сложных вычислений, не посылает данных по сети, не работает с БД (за исключением случая, когда это lazy поле ORM, и это как раз одна из причин, почему lazy поля стоит использовать с большой осторожностью).
  • Setter — setSomething (Something something) — устанавливает значение структуры данных, не делает сложных вычислений, не посылает данных по сети, не работает с БД. Обычно от сеттера вообще не ожидается неявного поведения или потребления сколько-нибудь значительных вычислительных ресурсов.
  • equals() и hashcode() — не ожидается вообще ничего, кроме простых вычислений и сравнений в количестве, линейно зависимом от размера структуры данных. Т. е. если мы вызываем hashcode для объекта из трех примитивных полей, ожидается, что будет выполнено N*3 простых вычислительных инструкций.
  • toSomething() — также ожидается, что это метод, преобразующий один тип данных в другой, и для преобразования ему требуется только количество памяти, сопоставимое с размерами структур, и процессорное время, линейно зависящее от размера структур. Тут надо заметить, что не всегда преобразование типов можно сделать линейно, скажем, преобразование пиксельной картинки в SVG-формат может быть весьма нетривиальным действием, но в таком случае лучше назвать метод по-другому. Например, название computeAndConvertToSVG() выглядит несколько неуклюжим, зато сразу наводит на мысль, что там внутри происходят какие-то значительные вычисления.

Приведу пример. Недавно я делал аудит приложения. По логике работы я знаю, что приложение где-то в коде подписывается на RabbitMQ-очередь. Иду по коду сверху вниз — не могу найти это место. Ищу непосредственно обращение к rabbit, начинаю подниматься вверх, дохожу до места в business flow, где подписка собственно происходит — начинаю ругаться. Как это выглядит в коде:

  1. Вызывается метод service.getQueueListener(tickerName) — возвращаемый результат игнорируется. Это могло бы насторожить, но такой фрагмент кода, где игнорируются результаты работы метода, в приложении не единственный.
  2. Внутри tickerName проверяется на null и вызывается другой метод getQueueListenerByName(tickerName).
  3. Внутри него из хэша по имени тикера берется экземпляр класса QueueListener (если его нет, он создается), и у него вызывается метод getSubscription().
  4. А вот уже внутри метода getSubscription() собственно и происходит подписка. Причем происходит она где-то в самой середине метода размером в три экрана.

Скажу прямо — не пробежав всей цепочки и не прочтя внимательного десяток экранов кода, догадаться, где же происходит подписка, было нереально. Если бы метод назывался subscribeToQueueByTicker(tickerName), это сэкономило бы мне немало времени.

Утилитарные классы


Есть прекрасная книга Design Patterns: Elements of Reusable Object-Oriented Software (1994), ее часто называют GOF (Gang of Four, по количеству авторов). Польза этой книги прежде всего в том, что она дала разработчикам из разных стран единый язык для описания шаблонов дизайна классов. Теперь вместо «класс гарантированно существующий только в одном экземпляре и имеющий статическую точку доступа» можно сказать «синглтон». Эта же книга нанесла заметный урон неокрепшим умам. Вред этот хорошо описывает цитата с одного из форумов «Коллеги, мне надо сделать веб-магазин, скажите, с использования каких шаблонов мне надо начать». Иначе говоря, некоторые программисты склонны злоупотреблять шаблонами проектирования, и там, где можно было обойтись одним классом, иногда создают сразу пять или шесть — на всякий случай, «для большей гибкости».

Как решить, нужна вам абстрактная фабрика классов (или другой паттерн сложнее интерфейса) или нет? Есть несколько простых соображений:

  1. Если вы пишете прикладное приложение на Spring, в 99 % случаев не нужна. Spring предлагает вам более высокоуровневые строительные блоки, используйте их. Максимум, что вам может пригодится, это абстрактный класс.
  2. Если пункт 1 все же не дал вам четкого ответа — помните, что каждый шаблон — это +1000 очков к сложности приложения. Тщательно проанализируйте, перевесит ли польза от использования шаблона вред от него же. Обращаясь к метафоре, помните, каждое лекарство не только лечит, но и немножечко вредит. Не надо пить все таблетки сразу.


Хороший пример того, как делать не надо, можете посмотреть здесь.

Заключение


Подводя итог, хочу заметить, что перечислил самые базовые рекомендации. Я бы вообще не стал оформлять их в виде статьи — настолько они очевидны. Но за прошедший год мне слишком часто попадались приложения, в которых многие из этих рекомендаций нарушались. Давайте писать простой код, который легко читать и легко поддерживать.

Комментарии (26)


  1. Anton23
    14.03.2019 19:23

    Спасибо за оригинальный шрифт, очень приятно читать

    Заголовок спойлера
    (нет)


    1. DataArt Автор
      14.03.2019 19:28
      +3

      Антон, извините, сразу же исправили, но вы успели открыть раньше. Больше так не будем!


      1. Anton23
        14.03.2019 19:29

        ^^


  1. tuxi
    14.03.2019 19:59
    +1

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

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


    1. Semenych
      14.03.2019 20:09

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


      1. 1c80
        14.03.2019 20:31

        А разве это не 2 разных зонтика?
        один что то ищет, другой что то меняет, я бы разделил, если бы себе писал.


        1. tuxi
          14.03.2019 20:33

          Считает кол-во букв. Да и поиск файлов тоже может быть «узкоспециализированным», не факт например, что будет нужна маска для поиска.


        1. Semenych
          14.03.2019 21:01

          Ну я в общем об этом и говорю в статье :-)


    1. alatushkin
      15.03.2019 07:28

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


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


  1. 1c80
    14.03.2019 20:39

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


    1. tuxi
      14.03.2019 23:36

      Так я про это и пишу, надо на этапе анализа понять вероятность «сильно разойдутся» или нет… По моей практике достаточно часто 2 класса так и остаются. Зато часто можно было написать что то типа вот такого простого и оно так и осталось бы сугубо утилитарной вещью без всякого особого поведения.

      int count = SearchAnalyzer.getInstance().filter(filePattern).count(char);
      


      1. Semenych
        14.03.2019 23:47

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

        int count = SearchAnalyzer.getInstance().findFilesAndCount(pattern,char);


        1. tuxi
          14.03.2019 23:51

          Сорри, я потом пару раз отредактировал свой пост, понял что не совсем правильно изложил свою мысль.


      1. 1c80
        15.03.2019 00:17

        А, ну значит я Вас не понял, согласен.


  1. StrangerInTheKy
    15.03.2019 03:31

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


    1. Semenych
      15.03.2019 09:27

      Уточню — перед методами со статическими классами. В комментарии я пожалуй мысль раскрыть не смогу, поля слишком узкие. Советую почитать про IoC контейнеры и зачем они нужны. Сейчас без них приложения пожалуй уже почти и не пишутся. Если коротко, то это жизненный цикл, связывание, конфигурирование в первую очередь и много других не менее полезных фишек во вторую и третью.


      1. StrangerInTheKy
        15.03.2019 17:10

        Советую почитать про IoC контейнеры и зачем они нужны.
        Что это, зачем и как работает, я примерно представляю. Но вопрос возник в связи с тем, что дальше по тексту у вас идет
        Действительно, в Java-мире сложилась традиция — вспомогательные функции работы со строками и потоками ввода-вывода выносить в статичные методы и собирать под зонтиком SomethingUtils-классов. Но мне такая традиция кажется достаточно замшелой.
        Хотелось бы понять, в чем именно замшелость.


        1. Quilin
          15.03.2019 18:07
          +1

          Позволю себе влезть в затевающуюся дискуссию со своими пятью копейками из смежного мира дотнета. В разных местах по-разному используют статические классы, на моей работе это как правило приводит к:
          1. Эпичный класс SomethingHelper, который наверное еще и partial, в котором смешалась в кучу вся хоть отчасти касающаяся некой сущности логика. Это спагетти, в котором невозможно найти нужный метод.
          2. Поскольку это статический класс, то он не умеет управлять циклом жизни своих зависимостей, например, если кто-то триста лет назад использовал валидацию через БД, то теперь некий метод принимает там стратегию работы с БД, и это уже не просто Helper/Utils, а полноценная помесь Singleton и Strategy. Ухх.
          3. Написание юнит-тестов на классы, которые пользуются этими Utils превращается в извращение, потому что какой-нибудь из методов ConvertNameToPhone наверняка валидирует то, что в имени есть три слова, каждое с большой буквы, и ваши юнит-тесты, которые должны проверять, что медиатор прочитал данные, и если ничего не нашел, то отправил сообщение в MQ, начинают пестреть «Ивановым Николаем Петровичем» и десятком других полей, которые для теста не имеют никакого значения, но ужасно необходимы Utils-методу.

          К сожалению, уже подгорает и продолжить список не могу, но уверен, что в других компаниях свои антипаттерны хелперов.


          1. StrangerInTheKy
            16.03.2019 01:33

            Позволю себе влезть в затевающуюся дискуссию
            Да нету никакой дискуссии! Дискуссия была бы, если бы я в этом что-то понимал и у меня была бы своя точка зрения. А я действительно только учусь (и считаю, что этот момент в статье раскрыт плохо).
            Но большое спасибо вам и отписавшимся ниже, теперь стало намного понятнее.


        1. Semenych
          15.03.2019 19:10
          +1

          говоря о замшелости — есть грубо говоря два способа связывания частей приложения.
          1. из одной функции или процедуры вызвать другую, а из нее третью.
          2. связывание компонент (в нашем случае классов) с помощь какого-то декларативного способа.

          второй способ в целом удобнее, заодно он позволяет делать еще две очень удобные вещи
          1. конфигурирование
          2. управление жизненным циклом. Если компонентам для начала работы необходимо как-то разогреться, то Spring IoC очень удобен.


        1. ads83
          15.03.2019 21:55
          +1

          Сначала абстрактно.
          Статические методы — это использование процедурного подхода. Если вы пишете в ОО парадигме с функциональной примесью (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 экрана в худшем случае. Код становится "шумным": показывает детали, неважные на этом уровне.


    1. Ryppka
      15.03.2019 10:09

      Тарантулы тем полезны, что, будучи настоянными на спирту, служат противоядием от своих же укусов...

      Из древнего сборника лайфхаков…


  1. ads83
    15.03.2019 11:24

    Например очень хороший <...> контракт класса может выглядеть так: «Класс имеет один метод, который <...> сочиняет качественный рассказ». Его реализация крайне сложна, но сложность скрыта внутри класса.
    Я правильно понимаю, что вы считаете, что чем выше внутренняя сложность класса, тем лучше?


    1. Semenych
      15.03.2019 12:33

      соотношение сложность реализации / сложность контракта должно быть высоким. Когда много простых классов образуют сложную систему — получается github.com/EnterpriseQualityCoding/FizzBuzzEnterpriseEdition


      1. ads83
        15.03.2019 13:20
        +1

        Если буквально следовать этому тезису, то класс на 3000+ строчек — это хорошо и ваша мечта. Это прямо противоречит

        Совсем хорошо, когда код, <...>, умещается на одном экране внутри одного метода с понятным названием
        Метод с каллбэком на 1000+ строк кода мало кого радует, это очевидно. Но вы упустили эту крайность.

        IMO, среди очевидных и банальных советов должно быть место для ограничения на количество строк в одном методе/классе.


        1. Semenych
          15.03.2019 13:27

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

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