Добровольно-принудительный Spring
и принципы SOLID
Как быть с практиками использования фреймворка не по стандартам и минимизировать ущерб


Доклад Ильи и Фёдора Сазоновых для Podlodka Java Crew
Привет! Это Фёдор и Илья Сазоновы. С вами Podlodka Java Crew, и сегодня мы расскажем, как современные Java-фреймворки и инструменты помогают писать поддерживаемый, красивый, тестируемый код — нам, простым Java-разработчикам.
Послушать больше докладов про микросервисную архитектуру можно в новом сезоне Podlodka Java Crew. Стартуем уже 20 марта. Присоединяйтесь!
Конференции Podlodka Crew отличает глубокое погружение и максимальный фокус на практику. В новом сезоне речь пойдёт о том, как реализовать в микросервисах согласованность данных, распределённые транзакции, протоколы передачи данных и observability — tracing, logging, metrics.
Java-разработчики в последнее время много говорят про Kotlin, React, Big Data. Но всё это время параллельно был Spring. В какую компанию не придёшь — там будут использовать Spring. Сообщество выбрало его добровольно, а теперь нам всем приходится заниматься Spring принудительно, потому что выбора сейчас уже и нет :)

Существует множество замечательных технологий, с помощью которых мы, разработчики, зарабатываем на свой хлеб с маслом: Mockito, Hibernate, MapStruct, Lombok и множество других. Все они работают под управлением Spring.
Но, когда Spring и всех этих технологий не было, мы выживали как могли. Даже сервлеты были только у особо везучих. И тогда крутые программисты стали думать, как вообще писать код в таких случаях. И разработали SOLID — принципы, которым нужно следовать, для того чтобы у вас были хорошие, белые и пушистые программы.
Принципы SOLID
SOLID нужен для обеспечения качественной модульности, уменьшающей когнитивную сложность кода и способствующей большей переиспользуемости. Если вы хотите говорить не по-человечески, а как на собеседованиях, тогда заучите этот ответ. И вполне возможно, что на собеседовании от вас отстанут и SOLID вы пройдете. :)

Но в этой статье мы хотим посмотреть на принципы SOLID сквозь призму современной разработки: как она эти принципы преломила. Напомним про важность юнит-тестирования, поделимся своим подходом, расскажем, на что современному разработчику прежде всего нужно обращать внимание.
Принципы SOLID появились в далёком 2000 году. А перечисленные технологии — относительно новые и должны помогать писать код, который соответствует принципам SOLID. А код, который не соответствует, должно быть сложно и неудобно писать.
Принципы SOLID
Когда-то давно этот принцип был введён под названием Single Reason to Change, и у него был мощный смысл. Он был нужен на случай, когда два разработчика пишут две разных фичи: чтобы у них не было merge-конфликта на одном классе.

Если у каждого класса только одна фича может быть причиной для изменений, то когда разработчики занимаются двумя разными фичами, они никогда не будут редактировать один и тот же класс. Ещё очень удобно такие классы запаковывать в джарки (от Jar — банка) — такая джарка повлияет только на одну фичу. Это очень круто — можно эти джарки совершенно спокойно подлить на сервер, не боясь что-нибудь сломать.
Но большинство современных разработчиков уже не подкладывает джарки ни руками, ни через автоматику. Мы сами последний раз так делали лет 10 назад.

Что касается merge-конфликтов, сейчас мы используем Git — это практически машинка для их разруливания. А кому не хватает Git, у тех есть IDEA. В IDEA эти merge-конфликты и их разруливание заключаются в том, чтобы вовремя нажать кнопочку All и об этом не забыть.

В результате этот принцип Single Reason to Change перестаёт приносить пользу. И когда людей на собеседованиях спрашивают, что такое Single Responsibility, они объясняют обычно на примерах: мол, у нас есть юзер-сервис, и его единственная ответственность — делать всё, что связано с юзером.
У всех методов только одна ответственность на всех. Самое главное, часто кажется, что от этого ужаса можно убежать. Например, больше не писать на Spring, а начать использовать другой новый и прикольный фреймворк, который так не делает.

Правда, если посмотреть детально на аналоги, выясняется, что весь код везде работает и отличается лишь одной аннотацией. Причём они поддерживают стандартные спринговые аннотации и отличий в коде может и вообще не быть.
Разговор о том, какой фреймворк лучше, просто превращается в спор о том, какой из них каноничный. Можно вывезти программиста со спрингового проекта, но спринговый проект вывезти из разработчика уже нельзя.
Принцип Dependency Inversion.
Когда-то давно принцип Dependency Inversion был о том, что наше приложение или код должны быть построены на абстракциях. И стрела зависимости должна быть от более абстрактного к более конкретному. На эту тему была целая философия: как надо писать приложение.

Когда пишешь код, удобно, чтобы ядро было написано на чистой Java без использования эзотерических фреймворков. Тогда можно совершенно спокойно делать всё, что захочешь, не обращая внимания на дурацкие детали реализации. Но тогда нужно где-то хранить данные. В ядре объявляешь интерфейс, в котором есть методы для сохранения и загрузки данных. Потом начинаешь этот интерфейс реализовывать уже в другом модуле. Это может делать уже другой программист. Он берёт и сохраняет данные, например, в PostgreSQL. А потом мы собираем две джарки, и всё это работает.
Вот об этом был этот могучий принцип, когда говорили, что стрела зависимости — от более абстрактных к более конкретным. Имелось в виду, что это модуль из PostgreSQL зависит от ядра, а не наоборот. Обычно до этого модуль содержал конкретную технологию, и ядро на него завязывалось. В том, чтобы так не делать, и заключался принцип. Сейчас у нас этого нет, но олды это очень одобряют. В текущей ситуации: даже если бы у нас все модули были на Spring, в этом не было бы ничего страшного. Ведь ядро ничего не знает о реализации, поэтому ядро в любом случае сохранит чистоту.

Но у нас ситуация другая. Весь наш микросервис — зелёненький, спринговый. Наше ядро на молекулярном уровне уже слилось с этими модулями. Вот так: от Dependency Inversion осталась только аббревиатура DI, которая нынче расшифровывается как Dependency Injection.

Фактически это только способ подключить зависимости.

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

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

Но на самом деле, разработчики, которые советуют так делать, рассчитывают, что, когда человек начнёт через конструктор подключать зависимости, он опишет огромный 28-сантиметровый конструктор. И поймёт, что 28 сантиметров кода — это много. И ему захочется зависимости убрать. Когда он будет эти зависимости убирать, чтобы кода стало меньше, получится, что и ответственности у класса станет меньше, и он станет ближе к принципу Single Responsibility, Single Reason to Change.

Но комьюнити поработало: зачем нам вообще всё это писать, давайте-ка сгенерируем библиотеку, которая будет брать и за нас генерировать конструктор. Сверху RequiredArgsConstructor вы конструктор не увидите, а он есть. И в связке со Spring Lombok позволяет расслабляться и ничего не делать, потому что за вас всё уже сделано.

Получается, современные разработчики использовали две технологии. Это было не так просто. Они специально их разработали, лишь бы облегчить себе подключение большого количества зависимостей. Оставалась, правда, надежда, что они увидят, что в тестах эти зависимости надо мокать, но коммьюнити это не остановило.
Разработчики сделали Mockito. Теперь его можно весело использовать и мокировать вообще всё, что нас заинтересует в больших количествах.
Так уже не две, а три технологии использованы, чтобы было легче обойти Dependency Inversion. Никогда ещё игнорировать принципы SOLID не было так просто и интересно. Это триумф формы над содержанием. Квинтэссенция того, что у нас происходит в современной разработке.
Принцип Interface Segregation.
Принцип про то, что у интерфейсов должно быть мало методов. Но зачем это нужно? Какой в этом смысл? Смысл в том, что, когда ты будешь реализовать этот интерфейс, тебе придётся писать меньше методов. Ведь чем больше реализаций, тем больше устанешь на галере.
У нас, скорее всего, будет не public interface UserService, а public class UserServiсe. Потому что программа на модули не делится, это огромный зелёненький монолит. И разбивка на модули не нужна, а значит, и интерфейс объявлять не нужно. Но, даже если мы пишем в таком странном стиле, мы всё равно забыли про юнит-тесты.

Когда мы пишем юнит-тесты, нам нужны тестовые реализации. Значит, нам нужен интерфейс, и как минимум два раза мы его будем реализовывать, это важно. Но разработчики Mockito решили два принципа решить за раз. Они, надо сказать, молодцы. Очень любят работать. Вот мы выяснили, зачем нам нужна ударная дрель — для того, чтобы помножить на 0 уже второй принцип.
Принцип Liskov's Substitution Principle.
Принцип, названный в честь великого русского писателя Лескова. Это, конечно, шутка — его придумала Барбара Лисков. Но достаточно часто на собеседованиях мы сталкиваемся с ударением на второй слог, такое бывает. Это потому что принцип мало кто помнит, а он гениальный, и в то же время очень простой.
Суть в том, что, если у нас есть UserService, то все реализации этого сервиса должны вести себя одинаково. То есть мы должны понимать, что в любой момент в зависимости, где у нас UserService, могут заинжектить любой из этих интерфейсов.

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

Spring предлагает вам использовать такой подход, когда делаешь один интерфейс, несколько реализаций, а потом в runtime это меняешь. В этом случае получается приятная композиция кода. Люди об этом знают, но не понимают, как использовать. На всех конференциях говорят про паттерны типа registry, visitor'а и так далее, однако воз и ныне там.
Принцип Open/Closed Principle.
Это принцип про то, что класс должен быть закрыт для изменений и открыт для расширения.
Как-то на собеседовании один кандидат сказал: «Этот принцип заключается в том, что, когда пилишь новую фичу в class UserService, нужно добавить новый метод, а не менять старый». Но, если подумать, современные разработчики так и делают. Этот принцип выполняется неукоснительно: если нужна новая фича, для неё делают новый метод. Который, кстати, не то что бы совсем метод, а скорее процедура, потому что у нас все классы stateless.
Процедурный Spring
А если под этим углом посмотреть на все остальные принципы SOLID? Окажется, что Single Reason to Change тоже соблюдается потому, что есть единственная ответственность, но не у класса, а у метода.
Даже если в принципе посмотреть на несколько этих вещей, получается: Single Responsibility тоже про эти самые stateless-методы, то есть про Open/Closed, про процедуры.

Казалось бы, Dependency Inversion не про процедуры, а про подключение зависимостей. А для чего подключаем зависимости? Чтобы использовать их методы. Которые практически процедуры. А как называется парадигма программирования, где единицей измерения кода является процедура? Процедурная. То есть Spring стал из добровольно-принудительного добровольно-процедурным?

Акцент на слово «добровольно». Сообщество сделало это безо всякого нажима и давления. Разработали множество инструментов, чтобы процедурный код было удобнее писать, и теперь мы пишем процедурный код.
На конференциях мы рассказываем про ООП, про разные технологии. И уверены, что пишем на крутом ООП Spring'e, а на самом деле, если присмотреться, пишем на старом добром Pascal. Это поразительно. Нам кажется, что это какой-то заговор. Шапочки из фольги где-то забыли, но хотим сказать, что заговоры в разработке программного обеспечения — вещь известная. Например, все знают, что DevOps — это заговор сисадминов с целью поработить разработчиков.

Когда разработчики друг с другом разговаривают, они такие: «Я проектирую и разрабатываю сложные системы, я придумываю, как всё будет работать». Но в глубине души все мы знаем, что на самом деле придумывает, как всё будет работать, не разработчик, а аналитик. Аналитик на русском языке всё пишет, и только потом ты это перерабатываешь в Java-код.

Известно, что аналитиков сейчас не хватает, хотелось бы побольше.

Хотя некоторые компании в принципе отказываются от использования услуг аналитиков, потому что у них есть разработчики. Они же сильные, смелые, ловкие, умелые и вообще инженеры — могут всё. Тем более зачастую разработчик и есть аналитик, а код не меняется. А ещё Spring придумали не аналитики, а какие-то разработчики написали. И тоже специально для того, чтобы мы писали так, как сейчас пишем.

Да, с помощью Spring вполне можно писать объектно-ориентированный код. Но разработчики почему-то не пишут. Это сейчас аналитики такие белые и пушистые, а при первой встрече шокировали сообщество разработчиков так, что те до сих пор не могут оправиться. Да, действительно, сейчас компании от аналитиков отказываются, потому что знают: разработчик без аналитика будет писать точно такой же код, как с аналитиком. И разработчик уверен, что ТЗ на написание кода — это на русском языке что-то процедурное.

Или что можно родить, кроме глубоко процедурного кода, из вот такой схемы?
Посмотрим на примере: сейчас слева будет код, справа будет вот эта диаграмма. И если вот так посмотреть, то один к одному мапится: код на диаграмму и диаграмма на код.
Для примера этот блок, где «разделся», мы окрасим в красный. Представьте, что кода для него пока нет, и его нужно добавить. Получается, я беру задачу, смотрю на эту диаграмму, потом смотрю вход, иду сверху вниз и понимаю, что isDressed у меня нет. Значит, надо его добавить. Вот она развилка, и получается один к одному. А если бы я этот код слева написал в объектно-ориентированном стиле, то он был бы совсем другой структуры. Мне пришлось бы мапить одно на другое…

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

Что с этим делать? В принципе, это извечный русский вопрос, ещё больше ста лет назад люди этим интересовались. На этот простой древний вопрос есть очень простой и понятный ответ: сдаться.

А что можно ещё сделать с бесконечным потоком ТЗ от аналитиков? Ты не можешь сказать: «Горшочек, не вари». Бизнес с аналитиками вместе, ему эти схемы более понятны, он их поддерживает. Даже если бы можно было диктовать аналитикам свои условия, то что им сказать? «Не пишите так»? Они скажут: «Как писать?» Разработчики зачастую не знают или давно забыли, как выглядят объектные описания. А ещё разработчикам удобно: схема в код и код в схему перерабатываются красиво.

Вместо поиска ответа на вопрос «Что делать?» лучше просто подумать, как нам себя вести, чтобы эта ситуация меньше нам вредила и было не так больно.
Основная фича Spring — что можно мокать процедуры.
Если задуматься и написать матрицу сравнения Pascal и Spring, то мы увидим, что между ними есть только одна существенная разница. В Pascal нельзя мокать процедуры, а в Spring можно. Это и есть важное отличие между Pascal и Spring, про которое нужно помнить, когда пишем код.
Это важное отличие никак нельзя упускать. Нужно, чтобы эта возможность сохранялась. Иначе, действительно, мы можем вернуться на Pascal, и будет непонятно, зачем были все эти 40 лет. Что это было, какой-то страшный объектно-ориентированный сон?

Мы хотим поделиться теми принципами, которые предлагаем выполнять, чтобы у нас эта возможность сохранилась.
Нельзя использовать статические методы.
Часто люди с Python приходят в Java и говорят: «Keep it simple, stupid». Давайте, говорят, будем использовать статические методы. Ну признайте уже, что эти ваши бины — это синглтоны. Чем они лучше статических методов? А это приводит к проблемам, если вы в Spring используете статические методы.
Есть статический контекст и есть спринговый контекст. Понятно, что это не одно и то же. Почему-то, каким-то образом, когда начинаешь использовать статические методы, то спринговый контекст пытается перетечь в статический.

Откуда взялся этот objectMapper. Ему сделали getBean — из контекста взяли статическим методом, или там new object mapper? Ещё теоретически в сигнатуре метода может передаваться какой-то сервис и так далее.

Зачастую статические методы используют для того, чтобы вообще отрицать Dependency Inversion. Пишут простыни кода со статическими методами, а когда им говоришь: «А оберни-ка это в какой-нибудь другой бин или что-нибудь такое», отвечают: «Нет, так мы делать не будем». Потому что, если сейчас завернуть это в бин, то тогда сделаем инъекцию в конструктор. А много инъекций — это плохо, так нельзя.

Самое важное, что статические методы нельзя мокать. Технически у нас такая возможность есть, мы можем использовать PowerMock, но в чём смысл тестов, которые прогоняются полдня? Только последняя версия Mockito умеет мокировать статические зависимости. Но с поправкой: по возможности, пожалуйста, этой функциональностью не пользуйтесь.

Так вот… Делать новые бины — это бесплатно :) Не стесняйтесь :)

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

Вот кусок кода. Внизу приватные методы, сверху публичный addNew. Не важно, что там в приватных методах. Если бы тесты были написаны на методах addNew, то пришлось бы в них учитывать внутреннее устройство методов createRoom и createTopRoom. Стоит вынести в отдельный бин createRoom и createTopRoom, замокать их и потом уже спокойно писать тесты.
Это правильно. Мы не платим деньги за то, чтобы делать новые классы, и не платим деньги за то, чтобы делать новые бины.

Если на эти методы посмотреть, они хорошо кладутся под один интерфейс с разными реализациями. И сюда можно заинжектить их в виде мапы или листа — стандартными способами. И код более лаконичный, и замокать его просто, красота. Но, к сожалению, эту красоту делают не все.
Не нужно вызывать в бине его собственные методы.
Если нашу логику повести дальше, то окажется, что не только приватные методы лучше не использовать, потому что их не получится замокать, а ещё не нужно вызывать в бине его собственные методы. Это тоже неправильно.
Пример. Сверху есть метод changeUserName, а снизу — changeNameIfEvening, в котором метод changeUserName вызывается. И замокать вызов этого метода нельзя. Поэтому при написании тестов придётся учитывать эту внутреннюю сложность метода changeUserName. Это не говоря о том, что он помечен аннотацией transactional, которая, скорее всего, не будет работать, потому что мы вызвали внутри бина его собственный метод.

Обратите внимание: это соображение не главное, потому что можно было бы сделать self inject. Самое главное, что при вызове внутри бина его собственных методов ты не можешь их замокать.

LocalDateTime.now() тоже не замокается, а в юнит-тестах вам достаточно часто нужно сделать так, чтобы система думала, что у вас там какое-то конкретное время. Поэтому так тоже нужно делать очень осторожно. Если в принципе вообще стоит это делать.

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

Если вы делаете что-то чуть-чуть, и это вам не сильно мешает, ничего страшного. Путь разработчика — это путь по тонкой линии между under engineering и over engineering. Если для того, чтобы избежать первого, вам приходится делать очень много второго, вы отклонитесь и упадёте. Во всём нужен баланс, мера. Их важно соблюдать. Помните об этом, когда будете читать текст дальше.
Есть у нас такой класс AppUserDto, который собирает данные, связанные с юзером. Здесь есть метод isAdult, в котором просто проверяется: возраст больше 18 или нет. Это бизнес-логика.
С ней работают те же самые соображения, что с бизнес-логикой, которую кладём в приватные или статические методы. Вызов метода isAdult не замокается, поэтому лучше так не делать.
Это не такая уж простая бизнес-логика, потому что возраст совершеннолетия может отличаться в зависимости от страны. Если метод простой, можно поступиться вышеперечисленными принципами. Но не обязательно метод isAdult будет простой. Вполне возможно, нужен будет специальный сервис, который по стране проживания определяет возраст совершеннолетия. Тогда в методе появится дополнительный параметр. Как это решать, разработчики уже знают.

Но мы не будем делать дополнительный параметр, а будем инжектить этот сервис в DTO. И нам придётся во все места, где DTO делается через new, добавить в конструктор дополнительный аргумент.
Никто не ожидает, что метод isAdult направит куда-то REST. Библиотеки тоже. Например, в случае применения MapStruct надо будет разбираться, как же сделать так, чтобы всё нормально мапилось и чтобы про метод isAdult не подумали, что там есть какое-то поле под названием adult. Инжектить какие-то сервисы в DTO с помощью MapStruct, наверное, можно. Но не понятно — как.

Поэтому давайте запретим DDD!

Подождите, не уходите, мы всё объясним. В сознании среднестатистического разработчика концепция DDD намертво привязана к ООП. Если ему надо внедрить DDD, он начинает всё делать ООП-шным. Когда нам рассказывают про ООП в институте или на курсах, нам говорят: «Есть класс Животные, от Животных наследуются Котики. Ты можешь сделать объект Котик. У всех Котиков есть свойства. А ещё у них есть методы, с помощью которых ты можешь свойства Котика менять. Котиков много, каждый уникален».

А потом человек приходит в разработку, и есть CatService, который один, синглтон. И есть много DTO, которые инкапсулируют все данные. Он думает: объекты — это явно Котики. CatService — это вообще ерунда. Я буду бизнес-логику внедрять в котика. А на самом деле приёмы ООП хорошо ложатся на сервисы, а на Котиков они в современной парадигме ложатся хуже. Но каждый хочет их затащить в Котиков. В принципе это можно сделать. Если у вас хорошо сыгранная команда, если вы хорошо знаете, как делать ваш сорт DDD, то спокойно можете этим заниматься. Но обычно получается так, что в команде есть пара энтузиастов, а все остальные — за всё хорошее, против всего плохого.

Энтузиасты начинают внедрять бизнес-логику в DTO, и это, как правило, не заканчивается хорошо. Вместо крутого, белого-пушистого кода получается сумятица. Поэтому сейчас DDD — штука сомнительная.

Давайте хотя бы соблюдать принципы, чтобы у нас осталась возможность мокать процедуры, иначе будет совсем плохо. Эта возможность нужна нам не просто так. Она нужна для юнит-тестов. Я сделаю сильное утверждение: ничто в современной разработке не имеет смысла, кроме как в свете юнит-тестирования. Это наша версия фразы учёного-эволюциониста Ф. Добжанского: «Ничто в биологии не имеет смысла, кроме как в свете эволюции». Кстати, под юнит-тестами я имею ввиду и то, что помечено аннотациями SpringBootTest, DataJpaTest и даже то, что работает с Testcontainers.

Если хотите, можете юнит-тестирование заменить юнит-интеграционно-автоматизированным тестированием. К сожалению, цитата становится не такой крутой. Казалось бы, все знают, что это круто, все это делают. Но, если присмотреться, то нет. Были времена, когда у нас был огромный монолит, который стартовал 12 минут. А потом нужно было найти поиск, ввести Voldemort'а, узнать, что он живёт в Петербурге, и только тогда понять, что ты правильно написал код. Либо 15 минут копаешься в этом интерфейсе, который написан ещё на JSP, либо пишешь один тестовый метод, и он отрабатывает достаточно быстро. Ясное дело, нужно писать юнит-тесты.
Но сейчас всё не так. Сейчас у нас есть микросервисы, которые стартуют 12 секунд. Поэтому разработчик берёт, немножко правит свой код, потом стартует микросервис, потом заходит в Swagger, туда впечатывает параметр поиска, смотрит, что у него получилось. Видит: не совсем так. Немножко правит, перезапускает…
Мы стремимся к тому, чтобы сервисы запускались маленькое количество времени. Но на практике остались места, где это всё по-прежнему не так. Поэтому бывает и 10 минут, и 20. В зависимости от того, насколько плохая ситуация. Но даже в таком случае иногда есть ограничения на среде. Несмотря на то, что мы живём в современности, юнит-тесты, которые проверяют твою логику, написать значительно быстрее. А Swagger — для проверки интеграций.

Мы встречали в сети мнение, с которым категорически не согласны, что Swagger — для одного, юнит-тесты — для другого. На самом деле нужно правильно это всё применять, потому что Swagger — для разработки и отладки кода, а юнит-тесты — чтобы фиксировать требования. И поэтому в Swagger мы всё отлаживаем, а потом пишем несколько юнит-тестов и закрываем отрицательные кейсы.

Кстати, не всегда нужно запускать локально. Это не проблема, люди берут и подключаются напрямую к среде, и там начинают делать эти фокусы. Это проекты, где на тестовой среде много данных и от них многое зависит. А другие нужно запускать локально, поэтому мы будем подключаться к тестовой среде. Swagger, дебаггер уже включён, и мы начинаем всё это проходить.

Это Debug Driven Development, и этот подход разрушителен для психики программиста.

Мы пишем микросервисы, где должно быть мало кода, и код должен быть простым, а в голове нужно примерно представлять, как это использовать.
Использовать дебаг для разработки нельзя. Только для отладки, когда уже последний рубеж. Когда ничего не помогает, юнит-тесты не отлавливают ничего. Это внутренняя штука фреймворка, которую кто-то писал на коленке. Ещё когда у вас простое приложение, не надо использовать дебаг. Многие джуны сразу начинают разрабатывать через него, потому что им кто-то показал, что можно не думать.
С нашей точки зрения современный разработчик должен очень плохо уметь пользоваться дебаггером, потому что он его и не должен использовать.

Когда ты живёшь в мире, где Swagger победил, ты постоянно в депрессии. Когда занимаешься Debug Driven Development или Swagger Driven Development, то, когда ты уже закончил задачу, уже сделал pull request, CI говорит, что нужно поднять процент покрытия. К счастью, процент покрытия фиксируется. Человек такой: «Я же уже всё через Swagger протестил, зачем мне ещё юнит-тесты писать?» А выбора нет. Необходимость писать тесты вводит тебя в депрессивное состояние. Потому что нет ничего более грустного, чем продолжать возиться с задачей, которую ты уже закончил.

Никому не снился сон, что нужно какой-то экзамен досдать, иначе тебе не засчитают институт или даже школу? И ты понимаешь, что уже не хочешь этим заниматься, а надо. Страшный сон. Многие программисты в этом сне живут. Поэтому нужно сказать «нет» этому Swagger Driven Development. Гораздо лучше использовать TDD. С ним, когда закончили задачу, тесты у вас есть по определению и жить вам гораздо легче.
Тестируйте фреймворки!
Мы в разработке используем большое количество фреймворков. В первом слайде мы их показывали, но их гораздо больше. Когда вы используете какой-то Hibernate, мы просим написать тест, который сохраняет Entity в базе данных. Потом загружает её из базы данных и проверяет, что там всё нормально загрузилось и работает, как ожидается. Казалось бы, фреймворк уже протестировали, можешь прочитать документацию и убедиться, что всё будет нормально работать.

Это не совсем правда. Мало того, по каждому фреймворку такое количество документации, что её невозможно вычитать. А ещё и фреймворков много. И вы не знаете, как они работают, сплетясь в клубок. Если есть какая-то функциональность, завязанная конкретно на то, как работают фреймворки, то надо обязательно написать интеграционный или юнит-тест, который будет проверять, что у вас эта функциональность работает. Что вы правильно интерпретировали документацию к фреймворкам.
Вот пример. Есть Entity. Что с ней не так? Проблема в самом низу с Basic. И Аннотация Data — не очень хорошо на Entity. Есть проблема и с EqualsAndHashCode. А айдишник не генерируется здесь на стороне приложения. Но все эти проблемы, когда у нас будет тестирование, выявит тестер. Почему люди не хотят тестировать фреймворки и писать очевидные тесты? Когда будет тестирование, приёмочно, им скажут: это не работает. Если ошиблись, ошибку быстро найдут.

Но в этом коде есть проблема, которую не выявит даже ручное тестирование. Есть аннотация Basic. Она делает так, что поле additional_info начинает лениво грузиться. Это нужно потому, что в этом поле полтора гига данных, и эти данные нужны нам очень редко. Взяли, поставили эту аннотацию и поле лениво грузится. На самом деле, чтобы это работало, нужно ещё добавить зависимость в maven. И только тогда это будет работать. Но на функциональном тестировании мы не узнаем, что что-то не так. Все будет работать отлично, тестировщик задачу закроет. И о том, что у нас есть какие-то проблемы, мы узнаем, только когда DBA нежно похлопает нас по плечу и скажет, что он сейчас нас убьёт. Потому что у нас постоянно между базой и приложением гигабайтами ходит информация.
Осторожнее с KISS и YAGNI!
Люди, пришедшие с других языков, по-своему интерпретируют правила разработки. Они могут сказать не только KISS, могут сказать You ain't gonna need it, что слои не нужны, и хорошо бы их упразднить.

Дальше логика такая. Репозитории упростить нельзя, они автоматически генерируют нам код. Контроллеры упразднить нельзя, потому что они позволяют нам связываться с вебом. А вот сервисы вроде и не нужны. Человек такой: «Отлично, сервисный слой мы уберём. Просто будем инжектить репозиторий прямо в контроллер, тогда нам и DTO не нужны, будет вот это Entity возвращать».

Если внутри будет какая-нибудь связанная коллекция, будет ошибка. Транзакция уже закрыта? Скорее всего, нет, потому что в Spring Boot есть специальная настройка, которая делает так, что транзакция не закроется, пока сериализация не закончится. Всё будет нормально работать, просто везде будет знаменитый N+1.

Скорее всего, какой-то толстый партнёр пришёл в организацию, которая занимается Spring Boot, и сказал: «У нас везде lazy, не хочется всё править. Просто сделайте настройку, чтобы транзакция не закрывалась, и всё». Ему говорят: «Это плохо, антипаттерн, коннекшенов не напасёшься». Отвечает: «У нас просто админка такая, три с половиной пользователя. Основная проблема — эксепшен везде. Сделайте! А все остальные просто будут эту настройку отключать». И действительно: большинство туториалов начинаются со слов: «Отключите open session in view».

С другой стороны, если у вас ситуация такая, как у этих жирных партнёров, можно эту настройку оставить. Есть вероятность внезапно начать передавать прямо на фронт мегасекретную информацию, которую в этой Entity добавил где-то, так как она же уже в контроллере. Это действительно эпичная дыра в безопасности, может кончиться плохо. Поэтому Entity надо мапить в DTO, и из контроллера возвращать DTO с фиксированным количеством полей. Что мы хотели сказать? Осторожнее с KISS и YAGNI.
Итоги
Будущее наступило, Spring теперь везде, и мы сделали его процедурным. Единственное отличие его от старого доброго Pascal в том, что эти процедуры можно мокать. Это преимущество нам нужно сохранить любой ценой.

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

Если у вас есть критическая функциональность, на которой завязано ваше приложение, и она опирается на то, как документированы фреймворки, лучше напишите юнит-тесты. И вообще ничто в современной разработке не имеет смысла, кроме как в свете юнит-тестирования. Поэтому тесты всё равно нужны, практикуйте TDD. От этого станет только лучше, и жизнь заиграет новыми красками.
Статья подготовлена по докладу "Добровольно-принудительный Spring" Ильи и Фёдора Сазоновых для Podlodka Java Crew.
Послушать больше докладов об особенностях построения микросервисной архитектуры в новом сезоне
Podlodka Java Crew.
Присоединяйтесь! Стартуем 20 марта!